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