(changelog update)
[nikiroo-utils.git] / src / be / nikiroo / utils / test / TestLauncher.java
... / ...
CommitLineData
1package be.nikiroo.utils.test;
2
3import java.io.PrintWriter;
4import java.io.StringWriter;
5import java.util.ArrayList;
6import 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 */
14public 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(Exception 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(Exception 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 Exception ex = null;
189 try {
190 try {
191 test.setUp();
192 } catch (Exception e) {
193 throw new SetupException(e);
194 }
195 test.test();
196 try {
197 test.tearDown();
198 } catch (Exception e) {
199 throw new TearDownException(e);
200 }
201 } catch (Exception 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 test
282 * the {@link TestCase}
283 */
284 protected void print(int depth, String name) {
285 name = prefix(depth, false)
286 + (name == null ? "" : name).replace("\t", " ");
287
288 while (name.length() < columns - 11) {
289 name += ".";
290 }
291
292 System.out.print(name);
293 }
294
295 /**
296 * Print the result of the {@link TestCase} we just ran.
297 *
298 * @param depth
299 * the level at which is the launcher (0 = main launcher)
300 * @param error
301 * the {@link Exception} it ran into if any
302 */
303 private void print(int depth, Exception error) {
304 if (error != null) {
305 System.out.println(" " + koString);
306 StringWriter sw = new StringWriter();
307 PrintWriter pw = new PrintWriter(sw);
308 error.printStackTrace(pw);
309 String lines = sw.toString();
310 for (String line : lines.split("\n")) {
311 System.out.println(prefix(depth, false) + "\t\t" + line);
312 }
313 } else {
314 System.out.println(" " + okString);
315 }
316 }
317
318 /**
319 * Print the total result for this test suite.
320 *
321 * @param depth
322 * the level at which is the launcher (0 = main launcher)
323 * @param executed
324 * the number of tests actually ran
325 * @param errors
326 * the number of errors encountered
327 * @param total
328 * the total number of tests in the suite
329 */
330 private void print(int depth, int executed, int errors, int total) {
331 int ok = executed - errors;
332 int pc = (int) ((100.0 * ok) / executed);
333 if (pc == 0 && ok > 0) {
334 pc = 1;
335 }
336 int pcTotal = (int) ((100.0 * ok) / total);
337 if (pcTotal == 0 && ok > 0) {
338 pcTotal = 1;
339 }
340
341 String resume = "Tests passed: " + ok + "/" + executed + " (" + pc
342 + "%) on a total of " + total + " (" + pcTotal + "% total)";
343 if (depth == 0) {
344 System.out.println(resume);
345 } else {
346 String arrow = "┗▶ ";
347 System.out.println(prefix(depth, currentSeries == 0) + arrow
348 + resume);
349 System.out.println(prefix(depth, currentSeries == 0));
350 }
351 }
352
353 private int last = -1;
354
355 /**
356 * Return the prefix to print before the current line.
357 *
358 * @param depth
359 * the current depth
360 * @param first
361 * this line is the first of its tabulation level
362 *
363 * @return the prefix
364 */
365 private String prefix(int depth, boolean first) {
366 String space = tabs(depth - 1);
367
368 String line = "";
369 if (depth > 0) {
370 if (depth > 1) {
371 if (depth != last && first) {
372 line = "╻"; // first line
373 } else {
374 line = "┃"; // continuation
375 }
376 }
377
378 space += line + tabs(1);
379 }
380
381 last = depth;
382 return space;
383 }
384
385 /**
386 * Return the given number of space-converted tabs in a {@link String}.
387 *
388 * @param depth
389 * the number of tabs to return
390 *
391 * @return the string
392 */
393 private String tabs(int depth) {
394 StringBuilder builder = new StringBuilder();
395 for (int i = 0; i < depth; i++) {
396 builder.append(" ");
397 }
398 return builder.toString();
399 }
400}