Merge commit 'e992c260c059c53c4aabc980db85efd58f190205'
[fanfix.git] / test / TestLauncher.java
1 package be.nikiroo.utils.test;
2
3 import java.io.PrintWriter;
4 import java.io.StringWriter;
5 import java.util.ArrayList;
6 import java.util.List;
7
8 /**
9 * A {@link TestLauncher} starts a series of {@link TestCase}s and displays the
10 * result to the user.
11 *
12 * @author niki
13 */
14 public class TestLauncher {
15 /**
16 * {@link Exception} happening during the setup process.
17 *
18 * @author niki
19 */
20 private class SetupException extends Exception {
21 private static final long serialVersionUID = 1L;
22
23 public SetupException(Throwable e) {
24 super(e);
25 }
26 }
27
28 /**
29 * {@link Exception} happening during the tear-down process.
30 *
31 * @author niki
32 */
33 private class TearDownException extends Exception {
34 private static final long serialVersionUID = 1L;
35
36 public TearDownException(Throwable e) {
37 super(e);
38 }
39 }
40
41 private List<TestLauncher> series;
42 private List<TestCase> tests;
43 private TestLauncher parent;
44
45 private int columns;
46 private String okString;
47 private String koString;
48 private String name;
49 private boolean cont;
50
51 protected int executed;
52 protected int total;
53
54 private int currentSeries = 0;
55 private boolean details = false;
56
57 /**
58 * Create a new {@link TestLauncher} with default parameters.
59 *
60 * @param name
61 * the test suite name
62 * @param args
63 * the arguments to configure the number of columns and the ok/ko
64 * {@link String}s
65 */
66 public TestLauncher(String name, String[] args) {
67 this.name = name;
68
69 int cols = 80;
70 if (args != null && args.length >= 1) {
71 try {
72 cols = Integer.parseInt(args[0]);
73 } catch (NumberFormatException e) {
74 System.err.println("Test configuration: given number "
75 + "of columns is not parseable: " + args[0]);
76 }
77 }
78
79 setColumns(cols);
80
81 String okString = "[ ok ]";
82 String koString = "[ !! ]";
83 if (args != null && args.length >= 3) {
84 okString = args[1];
85 koString = args[2];
86 }
87
88 setOkString(okString);
89 setKoString(koString);
90
91 series = new ArrayList<TestLauncher>();
92 tests = new ArrayList<TestCase>();
93 cont = true;
94 }
95
96 /**
97 * Display the details of the errors
98 *
99 * @return TRUE to display them, false to simply mark the test as failed
100 */
101 public boolean isDetails() {
102 if (parent != null) {
103 return parent.isDetails();
104 }
105
106 return details;
107 }
108
109 /**
110 * Display the details of the errors
111 *
112 * @param details
113 * TRUE to display them, false to simply mark the test as failed
114 */
115 public void setDetails(boolean details) {
116 if (parent != null) {
117 parent.setDetails(details);
118 }
119
120 this.details = details;
121 }
122
123 /**
124 * Called before actually starting the tests themselves.
125 *
126 * @throws Exception
127 * in case of error
128 */
129 protected void start() throws Exception {
130 }
131
132 /**
133 * Called when the tests are passed (or failed to do so).
134 *
135 * @throws Exception
136 * in case of error
137 */
138 protected void stop() throws Exception {
139 }
140
141 protected void addTest(TestCase test) {
142 tests.add(test);
143 }
144
145 protected void addSeries(TestLauncher series) {
146 this.series.add(series);
147 series.parent = this;
148 }
149
150 /**
151 * Launch the series of {@link TestCase}s and the {@link TestCase}s.
152 *
153 * @return the number of errors
154 */
155 public int launch() {
156 return launch(0);
157 }
158
159 /**
160 * Launch the series of {@link TestCase}s and the {@link TestCase}s.
161 *
162 * @param depth
163 * the level at which is the launcher (0 = main launcher)
164 *
165 * @return the number of errors
166 */
167 public int launch(int depth) {
168 int errors = 0;
169 executed = 0;
170 total = tests.size();
171
172 print(depth);
173
174 try {
175 start();
176
177 errors += launchTests(depth);
178 if (tests.size() > 0 && depth == 0) {
179 System.out.println("");
180 }
181
182 currentSeries = 0;
183 for (TestLauncher serie : series) {
184 errors += serie.launch(depth + 1);
185 executed += serie.executed;
186 total += serie.total;
187 currentSeries++;
188 }
189 } catch (Exception e) {
190 print(depth, "__start");
191 print(depth, e);
192 } finally {
193 try {
194 stop();
195 } catch (Exception e) {
196 print(depth, "__stop");
197 print(depth, e);
198 }
199 }
200
201 print(depth, executed, errors, total);
202
203 return errors;
204 }
205
206 /**
207 * Launch the {@link TestCase}s.
208 *
209 * @param depth
210 * the level at which is the launcher (0 = main launcher)
211 *
212 * @return the number of errors
213 */
214 protected int launchTests(int depth) {
215 int errors = 0;
216 for (TestCase test : tests) {
217 print(depth, test.getName());
218
219 Throwable ex = null;
220 try {
221 try {
222 test.setUp();
223 } catch (Throwable e) {
224 throw new SetupException(e);
225 }
226 test.test();
227 try {
228 test.tearDown();
229 } catch (Throwable e) {
230 throw new TearDownException(e);
231 }
232 } catch (Throwable e) {
233 ex = e;
234 }
235
236 if (ex != null) {
237 errors++;
238 }
239
240 print(depth, ex);
241
242 executed++;
243
244 if (ex != null && !cont) {
245 break;
246 }
247 }
248
249 return errors;
250 }
251
252 /**
253 * Specify a custom number of columns to use for the display of messages.
254 *
255 * @param columns
256 * the number of columns
257 */
258 public void setColumns(int columns) {
259 this.columns = columns;
260 }
261
262 /**
263 * Continue to run the tests when an error is detected.
264 *
265 * @param cont
266 * yes or no
267 */
268 public void setContinueAfterFail(boolean cont) {
269 this.cont = cont;
270 }
271
272 /**
273 * Set a custom "[ ok ]" {@link String} when a test passed.
274 *
275 * @param okString
276 * the {@link String} to display at the end of a success
277 */
278 public void setOkString(String okString) {
279 this.okString = okString;
280 }
281
282 /**
283 * Set a custom "[ !! ]" {@link String} when a test failed.
284 *
285 * @param koString
286 * the {@link String} to display at the end of a failure
287 */
288 public void setKoString(String koString) {
289 this.koString = koString;
290 }
291
292 /**
293 * Print the test suite header.
294 *
295 * @param depth
296 * the level at which is the launcher (0 = main launcher)
297 */
298 protected void print(int depth) {
299 if (depth == 0) {
300 System.out.println("[ Test suite: " + name + " ]");
301 System.out.println("");
302 } else {
303 System.out.println(prefix(depth, false) + name + ":");
304 }
305 }
306
307 /**
308 * Print the name of the {@link TestCase} we will start immediately after.
309 *
310 * @param depth
311 * the level at which is the launcher (0 = main launcher)
312 * @param name
313 * the {@link TestCase} name
314 */
315 protected void print(int depth, String name) {
316 name = prefix(depth, false)
317 + (name == null ? "" : name).replace("\t", " ");
318
319 StringBuilder dots = new StringBuilder();
320 while ((name.length() + dots.length()) < columns - 11) {
321 dots.append('.');
322 }
323
324 System.out.print(name + dots.toString());
325 }
326
327 /**
328 * Print the result of the {@link TestCase} we just ran.
329 *
330 * @param depth
331 * the level at which is the launcher (0 = main launcher)
332 * @param error
333 * the {@link Exception} it ran into if any
334 */
335 private void print(int depth, Throwable error) {
336 if (error != null) {
337 System.out.println(" " + koString);
338 if (isDetails()) {
339 StringWriter sw = new StringWriter();
340 PrintWriter pw = new PrintWriter(sw);
341 error.printStackTrace(pw);
342 String lines = sw.toString();
343 for (String line : lines.split("\n")) {
344 System.out.println(prefix(depth, false) + "\t\t" + line);
345 }
346 }
347 } else {
348 System.out.println(" " + okString);
349 }
350 }
351
352 /**
353 * Print the total result for this test suite.
354 *
355 * @param depth
356 * the level at which is the launcher (0 = main launcher)
357 * @param executed
358 * the number of tests actually ran
359 * @param errors
360 * the number of errors encountered
361 * @param total
362 * the total number of tests in the suite
363 */
364 private void print(int depth, int executed, int errors, int total) {
365 int ok = executed - errors;
366 int pc = (int) ((100.0 * ok) / executed);
367 if (pc == 0 && ok > 0) {
368 pc = 1;
369 }
370 int pcTotal = (int) ((100.0 * ok) / total);
371 if (pcTotal == 0 && ok > 0) {
372 pcTotal = 1;
373 }
374
375 String resume = "Tests passed: " + ok + "/" + executed + " (" + pc
376 + "%) on a total of " + total + " (" + pcTotal + "% total)";
377 if (depth == 0) {
378 System.out.println(resume);
379 } else {
380 String arrow = "┗▶ ";
381 System.out.println(prefix(depth, currentSeries == 0) + arrow
382 + resume);
383 System.out.println(prefix(depth, currentSeries == 0));
384 }
385 }
386
387 private int last = -1;
388
389 /**
390 * Return the prefix to print before the current line.
391 *
392 * @param depth
393 * the current depth
394 * @param first
395 * this line is the first of its tabulation level
396 *
397 * @return the prefix
398 */
399 private String prefix(int depth, boolean first) {
400 String space = tabs(depth - 1);
401
402 String line = "";
403 if (depth > 0) {
404 if (depth > 1) {
405 if (depth != last && first) {
406 line = "╻"; // first line
407 } else {
408 line = "┃"; // continuation
409 }
410 }
411
412 space += line + tabs(1);
413 }
414
415 last = depth;
416 return space;
417 }
418
419 /**
420 * Return the given number of space-converted tabs in a {@link String}.
421 *
422 * @param depth
423 * the number of tabs to return
424 *
425 * @return the string
426 */
427 private String tabs(int depth) {
428 StringBuilder builder = new StringBuilder();
429 for (int i = 0; i < depth; i++) {
430 builder.append(" ");
431 }
432 return builder.toString();
433 }
434 }