+package be.nikiroo.utils.test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link TestLauncher} starts a series of {@link TestCase}s and displays the
+ * result to the user.
+ *
+ * @author niki
+ */
+public class TestLauncher {
+ /**
+ * {@link Exception} happening during the setup process.
+ *
+ * @author niki
+ */
+ private class SetupException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SetupException(Exception e) {
+ super(e);
+ }
+ }
+
+ /**
+ * {@link Exception} happening during the tear-down process.
+ *
+ * @author niki
+ */
+ private class TearDownException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public TearDownException(Exception e) {
+ super(e);
+ }
+ }
+
+ private List<TestLauncher> series;
+ private List<TestCase> tests;
+ private int columns;
+ private String okString;
+ private String koString;
+ private String name;
+ private boolean cont;
+
+ protected int executed;
+ protected int total;
+
+ /**
+ * Create a new {@link TestLauncher} with default parameters.
+ *
+ * @param name
+ * the test suite name
+ * @param args
+ * the arguments to configure the number of columns and the ok/ko
+ * {@link String}s
+ */
+ public TestLauncher(String name, String[] args) {
+ this.name = name;
+
+ int cols = 80;
+ if (args != null && args.length >= 1) {
+ try {
+ cols = Integer.parseInt(args[0]);
+ } catch (NumberFormatException e) {
+ System.err.println("Test configuration: given number "
+ + "of columns is not parseable: " + args[0]);
+ }
+ }
+
+ setColumns(cols);
+
+ String okString = "[ ok ]";
+ String koString = "[ !! ]";
+ if (args != null && args.length >= 3) {
+ okString = args[1];
+ koString = args[2];
+ }
+
+ setOkString(okString);
+ setKoString(koString);
+
+ series = new ArrayList<TestLauncher>();
+ tests = new ArrayList<TestCase>();
+ cont = true;
+ }
+
+ /**
+ * Called before actually starting the tests themselves.
+ *
+ * @throws Exception
+ * in case of error
+ */
+ protected void start() throws Exception {
+ }
+
+ /**
+ * Called when the tests are passed (or failed to do so).
+ *
+ * @throws Exception
+ * in case of error
+ */
+ protected void stop() throws Exception {
+ }
+
+ protected void addTest(TestCase test) {
+ tests.add(test);
+ }
+
+ protected void addSeries(TestLauncher series) {
+ this.series.add(series);
+ }
+
+ /**
+ * Launch the series of {@link TestCase}s and the {@link TestCase}s.
+ *
+ * @return the number of errors
+ */
+ public int launch() {
+ return launch(0);
+ }
+
+ /**
+ * Launch the series of {@link TestCase}s and the {@link TestCase}s.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ *
+ * @return the number of errors
+ */
+ public int launch(int depth) {
+ int errors = 0;
+ executed = 0;
+ total = tests.size();
+
+ print(depth);
+
+ try {
+ start();
+
+ errors += launchTests(depth);
+ if (tests.size() > 0 && depth == 0) {
+ System.out.println("");
+ }
+
+ for (TestLauncher serie : series) {
+ errors += serie.launch(depth + 1);
+ executed += serie.executed;
+ total += serie.total;
+ }
+ } catch (Exception e) {
+ print(depth, "__start");
+ print(depth, e);
+ } finally {
+ try {
+ stop();
+ } catch (Exception e) {
+ print(depth, "__stop");
+ print(depth, e);
+ }
+ }
+
+ print(depth, executed, errors, total);
+
+ return errors;
+ }
+
+ /**
+ * Launch the {@link TestCase}s.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ *
+ * @return the number of errors
+ */
+ protected int launchTests(int depth) {
+ int errors = 0;
+ for (TestCase test : tests) {
+ print(depth, test.getName());
+
+ Exception ex = null;
+ try {
+ try {
+ test.setUp();
+ } catch (Exception e) {
+ throw new SetupException(e);
+ }
+ test.test();
+ try {
+ test.tearDown();
+ } catch (Exception e) {
+ throw new TearDownException(e);
+ }
+ } catch (Exception e) {
+ ex = e;
+ }
+
+ if (ex != null) {
+ errors++;
+ }
+
+ print(depth, ex);
+
+ executed++;
+
+ if (ex != null && !cont) {
+ break;
+ }
+ }
+
+ return errors;
+ }
+
+ /**
+ * Specify a custom number of columns to use for the display of messages.
+ *
+ * @param columns
+ * the number of columns
+ */
+ public void setColumns(int columns) {
+ this.columns = columns;
+ }
+
+ /**
+ * Continue to run the tests when an error is detected.
+ *
+ * @param cont
+ * yes or no
+ */
+ public void setContinueAfterFail(boolean cont) {
+ this.cont = cont;
+ }
+
+ /**
+ * Set a custom "[ ok ]" {@link String} when a test passed.
+ *
+ * @param okString
+ * the {@link String} to display at the end of a success
+ */
+ public void setOkString(String okString) {
+ this.okString = okString;
+ }
+
+ /**
+ * Set a custom "[ !! ]" {@link String} when a test failed.
+ *
+ * @param koString
+ * the {@link String} to display at the end of a failure
+ */
+ public void setKoString(String koString) {
+ this.koString = koString;
+ }
+
+ /**
+ * Print the test suite header.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ */
+ protected void print(int depth) {
+ if (depth == 0) {
+ System.out.println("[ Test suite: " + name + " ]");
+ System.out.println("");
+ } else {
+ System.out.println(prefix(depth) + name + ":");
+ }
+ }
+
+ /**
+ * Print the name of the {@link TestCase} we will start immediately after.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param test
+ * the {@link TestCase}
+ */
+ protected void print(int depth, String name) {
+ name = prefix(depth) + (name == null ? "" : name).replace("\t", " ");
+
+ while (name.length() < columns - 11) {
+ name += ".";
+ }
+
+ System.out.print(name);
+ }
+
+ /**
+ * Print the result of the {@link TestCase} we just ran.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param error
+ * the {@link Exception} it ran into if any
+ */
+ private void print(int depth, Exception error) {
+ if (error != null) {
+ System.out.println(" " + koString);
+ for (String line : (error.getMessage() + "").split("\n")) {
+ System.out.println(prefix(depth) + "\t\t" + line);
+ }
+ } else {
+ System.out.println(" " + okString);
+ }
+ }
+
+ /**
+ * Print the total result for this test suite.
+ *
+ * @param depth
+ * the level at which is the launcher (0 = main launcher)
+ * @param executed
+ * the number of tests actually ran
+ * @param errors
+ * the number of errors encountered
+ * @param total
+ * the total number of tests in the suite
+ */
+ private void print(int depth, int executed, int errors, int total) {
+ int ok = executed - errors;
+ int pc = (int) ((100.0 * ok) / executed);
+ if (pc == 0 && ok > 0) {
+ pc = 1;
+ }
+ int pcTotal = (int) ((100.0 * ok) / total);
+ if (pcTotal == 0 && ok > 0) {
+ pcTotal = 1;
+ }
+
+ String resume = "Tests passed: " + ok + "/" + executed + " (" + pc
+ + "%) on a total of " + total + " (" + pcTotal + "% total)";
+ if (depth == 0) {
+ System.out.println(resume);
+ } else {
+ String arrow = "┗▶";
+ if (series.isEmpty()) {
+ arrow = "━▶";
+ }
+ System.out.println(prefix(depth) + arrow + resume);
+ }
+ }
+
+ private int last = -1;
+
+ /**
+ * Return the prefix to print before the current line.
+ *
+ * @param depth
+ * the current depth
+ *
+ * @return the prefix
+ */
+ private String prefix(int depth) {
+ String space = tabs(depth - 1);
+
+ String line = "";
+ if (depth > 0) {
+ if (depth > 1) {
+ if (depth != last) {
+ line = "╻"; // first line
+ } else {
+ line = "┃"; // continuation
+ }
+ }
+ space = space + line + tabs(1);
+ }
+
+ last = depth;
+ return space;
+ }
+
+ /**
+ * Return the given number of space-converted tabs in a {@link String}.
+ *
+ * @param depth
+ * the number of tabs to return
+ *
+ * @return the string
+ */
+ private String tabs(int depth) {
+
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ builder.append(" ");
+ }
+ return builder.toString();
+ }
+}