Add some tests
authorNiki Roo <niki@nikiroo.be>
Mon, 13 Feb 2017 20:58:15 +0000 (21:58 +0100)
committerNiki Roo <niki@nikiroo.be>
Mon, 13 Feb 2017 20:58:15 +0000 (21:58 +0100)
Makefile.base
configure.sh
src/be/nikiroo/utils/resources/Bundle.java
src/be/nikiroo/utils/resources/TransBundle.java
src/be/nikiroo/utils/test/BundleTest.java [new file with mode: 0644]
src/be/nikiroo/utils/test/Test.java [new file with mode: 0644]
src/be/nikiroo/utils/test/TestCase.java [new file with mode: 0644]
src/be/nikiroo/utils/test/TestLauncher.java [new file with mode: 0644]
src/be/nikiroo/utils/test/bundle_test.properties [new file with mode: 0644]

index 559b89d9020a53df2b6212600233c68cd88841a2..9901644035cee9e0610447751befe4b1e28cb7cc 100644 (file)
@@ -7,6 +7,7 @@
 #TEST = path to main test source to compile
 #JAR_FLAGS += a list of things to pack, each usually prefixed with "-C bin/"
 #SJAR_FLAGS += a list of things to pack, each usually prefixed with "-C src/", for *-sources.jar files
+#TEST_PARAMS = any parameter to pass to the test runnable when "test-run"
 
 JAVAC = javac
 JAVAC_FLAGS += -encoding UTF-8 -d ./bin/ -cp ./src/
@@ -34,7 +35,7 @@ RJAR_FLAGS += -jar
 
 all: build jar
 
-.PHONY: all clean mrproper mrpropre build run jrun jar resources install libs love 
+.PHONY: all clean mrproper mrpropre build run jrun jar resources test-resources install libs love 
 
 bin:
        @mkdir -p bin
@@ -50,7 +51,7 @@ build: resources
                $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \
        done
 
-test: 
+test: test-resources
        @[ -e bin/$(MAIN).class ] || echo You need to build the sources
        @[ -e bin/$(MAIN).class ]
        @echo Compiling test program...
@@ -86,7 +87,17 @@ love:
 
 resources: libs
        @echo Copying resources into bin/...
-       @cd src && find . | grep -v '\.java$$' | while read -r ln; do \
+       @cd src && find . | grep -v '\.java$$' | grep -v '/test/' | while read -r ln; do \
+               if [ -f "$$ln" ]; then \
+                       dir="`dirname "$$ln"`"; \
+                       mkdir -p "../bin/$$dir" ; \
+                       cp "$$ln" "../bin/$$ln" ; \
+               fi ; \
+       done
+
+test-resources: resources
+       @echo Copying test resources into bin/...
+       @cd src && find . | grep -v '\.java$$' | grep '/test/' | while read -r ln; do \
                if [ -f "$$ln" ]; then \
                        dir="`dirname "$$ln"`"; \
                        mkdir -p "../bin/$$dir" ; \
@@ -134,7 +145,7 @@ run-test:
        @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ]
        @echo Running tests for "$(NAME)"...
        @[ "$(TEST)" != "" ] || echo No test sources defined.
-       [ "$(TEST)"  = "" ] || $(JAVA) $(JAVA_FLAGS) $(TEST)
+       [ "$(TEST)"  = "" ] || ( clear ; $(JAVA) $(JAVA_FLAGS) $(TEST) $(TEST_PARAMS) )
 
 install:
        @[ -e $(NAME).jar ] || echo You need to build the jar
index ed494b7d9d4e6a60494d870ed38a440cbcab68da..026db922830dffab71676b498eea2070223c771d 100755 (executable)
@@ -33,8 +33,21 @@ done
 
 [ $valid = false ] && exit 2
 
+if [ "`whereis tput`" = "tput:" ]; then
+       ok='"[ ok ]"';
+       ko='"[ !! ]"';
+       cols=80;
+else
+       ok='"`tput bold`[`tput setf 2` OK `tput init``tput bold`]`tput init`"';
+       ko='"`tput bold`[`tput setf 4` !! `tput init``tput bold`]`tput init`"';
+       cols='"`tput cols`"';
+fi;
+
+
 echo "MAIN = be/nikiroo/utils/resources/TransBundle" > Makefile
-echo "MORE = be/nikiroo/utils/StringUtils be/nikiroo/utils/IOUtils be/nikiroo/utils/MarkableFileInputStream" >> Makefile
+echo "MORE = be/nikiroo/utils/StringUtils be/nikiroo/utils/IOUtils be/nikiroo/utils/MarkableFileInputStream be/nikiroo/utils/test/TestLauncher" >> Makefile
+echo "TEST = be/nikiroo/utils/test/Test" >> Makefile
+echo "TEST_PARAMS = $cols $ok $ko" >> Makefile
 echo "NAME = nikiroo-utils" >> Makefile
 echo "PREFIX = $PREFIX" >> Makefile
 echo "JAR_FLAGS += -C bin/ be" >> Makefile
index 78d73a8148088619180caa59aa50fffcc2d50d40..2039d578e3c581a98efabad7369dbd0c879ed626 100644 (file)
@@ -57,7 +57,7 @@ public class Bundle<E extends Enum<E>> {
         *         resource file)
         */
        public String getString(E id) {
-               return getStringX(id, "");
+               return getStringX(id, null);
        }
 
        /**
@@ -74,8 +74,7 @@ public class Bundle<E extends Enum<E>> {
         */
        public String getStringX(E id, String suffix) {
                String key = id.name()
-                               + ((suffix == null || suffix.isEmpty()) ? "" : "_"
-                                               + suffix.toUpperCase());
+                               + (suffix == null ? "" : "_" + suffix.toUpperCase());
 
                if (containsKey(key)) {
                        return getString(key).trim();
@@ -180,10 +179,11 @@ public class Bundle<E extends Enum<E>> {
        }
 
        /**
-        * Create/update the .properties file. Will use the most likely candidate as
-        * base if the file does not already exists and this resource is
-        * translatable (for instance, "en_US" will use "en" as a base if the
-        * resource is a translation file).
+        * Create/update the .properties file.
+        * <p>
+        * Will use the most likely candidate as base if the file does not already
+        * exists and this resource is translatable (for instance, "en_US" will use
+        * "en" as a base if the resource is a translation file).
         * 
         * @param path
         *            the path where the .properties files are
@@ -219,6 +219,13 @@ public class Bundle<E extends Enum<E>> {
                writer.close();
        }
 
+       /**
+        * Reload the {@link Bundle} data files.
+        */
+       public void reload() {
+               setBundle(name, null);
+       }
+
        /**
         * Check if the internal map contains the given key.
         * 
@@ -367,7 +374,7 @@ public class Bundle<E extends Enum<E>> {
                        value = "";
                }
 
-               String[] lines = value.replaceAll("\\\t", "\\\\\\t").split("\n");
+               String[] lines = value.replaceAll("\t", "\\t").split("\n");
                for (int i = 0; i < lines.length; i++) {
                        writer.write(lines[i]);
                        if (i < lines.length - 1) {
index 61bc92259de48c9a90164b69df8c4e93799f2253..a3804401034692909302f04c50c39cdcad828e6d 100644 (file)
@@ -174,6 +174,11 @@ public class TransBundle<E extends Enum<E>> extends Bundle<E> {
                setBundle(name, locale);
        }
 
+       @Override
+       public void reload() {
+               setBundle(name, locale);
+       }
+
        @Override
        public String getString(E id) {
                return getString(id, (Object[]) null);
diff --git a/src/be/nikiroo/utils/test/BundleTest.java b/src/be/nikiroo/utils/test/BundleTest.java
new file mode 100644 (file)
index 0000000..6a2fdaa
--- /dev/null
@@ -0,0 +1,135 @@
+package be.nikiroo.utils.test;
+
+import java.io.File;
+
+import be.nikiroo.utils.IOUtils;
+import be.nikiroo.utils.resources.Bundle;
+import be.nikiroo.utils.resources.Bundles;
+import be.nikiroo.utils.resources.Meta;
+
+public class BundleTest extends TestLauncher {
+       private File tmp;
+       private B b = new B();
+
+       protected boolean isMain() {
+               return true;
+       }
+
+       public BundleTest(String[] args) {
+               this("Bundle test", args);
+       }
+
+       protected BundleTest(String name, String[] args) {
+               super(name, args);
+
+               addTests();
+
+               if (isMain()) {
+                       addSeries(new BundleTest("After saving/reloading the resources",
+                                       args) {
+                               @Override
+                               protected void start() throws Exception {
+                                       tmp = File.createTempFile("nikiroo-utils", ".test");
+                                       tmp.delete();
+                                       tmp.mkdir();
+                                       b.updateFile(tmp.getAbsolutePath());
+                                       Bundles.setDirectory(tmp.getAbsolutePath());
+                                       b.reload();
+                               }
+
+                               @Override
+                               protected void stop() {
+                                       IOUtils.deltree(tmp);
+                               }
+
+                               @Override
+                               protected boolean isMain() {
+                                       return false;
+                               }
+                       });
+               }
+       }
+
+       private void addTests() {
+               String pre = "";
+
+               addTest(new TestCase(pre + "getString simple") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un", b.getString(E.ONE));
+                       }
+               });
+
+               addTest(new TestCase(pre + "getStringX with null suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un", b.getStringX(E.ONE, null));
+                       }
+               });
+
+               addTest(new TestCase(pre + "getStringX with empty suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(null, b.getStringX(E.ONE, ""));
+                       }
+               });
+
+               addTest(new TestCase(pre + "getStringX with existing suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("un + suffix", b.getStringX(E.ONE, "suffix"));
+                       }
+               });
+
+               addTest(new TestCase(pre + "getStringX with not existing suffix") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals(null, b.getStringX(E.ONE, "fake"));
+                       }
+               });
+
+               addTest(new TestCase(pre + "getString with UTF-8 content") {
+                       @Override
+                       public void test() throws Exception {
+                               assertEquals("日本語 Nihongo", b.getString(E.JAPANESE));
+                       }
+               });
+       }
+
+       /**
+        * {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private class B extends Bundle<E> {
+               protected B() {
+                       super(E.class, N.bundle_test);
+               }
+
+       }
+
+       /**
+        * Key enum for the {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private enum E {
+               @Meta(what = "", where = "", format = "", info = "")
+               ONE, //
+               @Meta(what = "", where = "", format = "", info = "")
+               ONE_SUFFIX, //
+               @Meta(what = "", where = "", format = "", info = "")
+               TWO, //
+               @Meta(what = "", where = "", format = "", info = "")
+               JAPANESE
+       }
+
+       /**
+        * Name enum for the {@link Bundle}.
+        * 
+        * @author niki
+        */
+       private enum N {
+               bundle_test
+       }
+}
diff --git a/src/be/nikiroo/utils/test/Test.java b/src/be/nikiroo/utils/test/Test.java
new file mode 100644 (file)
index 0000000..0804039
--- /dev/null
@@ -0,0 +1,24 @@
+package be.nikiroo.utils.test;
+
+/**
+ * Tests for nikiroo-utils.
+ * 
+ * @author niki
+ */
+public class Test extends TestLauncher {
+       public Test(String[] args) {
+               super("Nikiroo-utils", args);
+
+               addSeries(new BundleTest(args));
+       }
+
+       /**
+        * Main entry point of the program.
+        * 
+        * @param args
+        *            the arguments passed to the {@link TestLauncher}s.
+        */
+       static public void main(String[] args) {
+               System.exit(new Test(args).launch());
+       }
+}
diff --git a/src/be/nikiroo/utils/test/TestCase.java b/src/be/nikiroo/utils/test/TestCase.java
new file mode 100644 (file)
index 0000000..0349bc0
--- /dev/null
@@ -0,0 +1,238 @@
+package be.nikiroo.utils.test;
+
+/**
+ * A {@link TestCase} that can be run with {@link TestLauncher}.
+ * 
+ * @author niki
+ */
+abstract public class TestCase {
+       /**
+        * The type of {@link Exception} used to signal a failed assertion or a
+        * force-fail.
+        * 
+        * @author niki
+        */
+       class AssertException extends Exception {
+               private static final long serialVersionUID = 1L;
+
+               public AssertException(String reason) {
+                       super(reason);
+               }
+       }
+
+       private String name;
+
+       /**
+        * Create a new {@link TestCase}.
+        * 
+        * @param name
+        *            the test name
+        */
+       public TestCase(String name) {
+               this.name = name;
+       }
+
+       /**
+        * Setup the test (called before the test is run).
+        * 
+        * @throws Exception
+        *             in case of error
+        */
+       public void setUp() throws Exception {
+       }
+
+       /**
+        * Tear-down the test (called when the test has been ran).
+        * 
+        * @throws Exception
+        *             in case of error
+        */
+       public void tearDown() throws Exception {
+       }
+
+       /**
+        * The test name.
+        * 
+        * @return the name
+        */
+       public String getName() {
+               return name;
+       }
+
+       /**
+        * Actually do the test.
+        * 
+        * @throws Exception
+        *             in case of error
+        */
+       abstract public void test() throws Exception;
+
+       /**
+        * Force a failure.
+        * 
+        * @throws AssertException
+        *             every time
+        */
+       public void fail() throws AssertException {
+               fail(null);
+       }
+
+       /**
+        * Force a failure.
+        * 
+        * @param reason
+        *            the failure reason
+        * @throws AssertException
+        *             every time
+        */
+       public void fail(String reason) throws AssertException {
+               throw new AssertException("Failed!" + //
+                               reason != null ? "\n" + reason : "");
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(Object expected, Object actual)
+                       throws AssertException {
+               assertEquals(null, expected, actual);
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param the
+        *            error message to display if they differ
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(String errorMessage, Object expected, Object actual)
+                       throws AssertException {
+
+               if (errorMessage == null) {
+                       errorMessage = String.format("" //
+                                       + "Assertion failed!\n" //
+                                       + "Expected value: [%s]\n" //
+                                       + "Actual value: [%s]", expected, actual);
+               }
+
+               if ((expected == null && actual != null)
+                               || (expected != null && !expected.equals(actual))) {
+                       throw new AssertException(errorMessage);
+               }
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(long expected, long actual) throws AssertException {
+               assertEquals(new Long(expected), new Long(actual));
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param the
+        *            error message to display if they differ
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(String errorMessage, long expected, long actual)
+                       throws AssertException {
+               assertEquals(errorMessage, new Long(expected), new Long(actual));
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(boolean expected, boolean actual)
+                       throws AssertException {
+               assertEquals(new Boolean(expected), new Boolean(actual));
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param the
+        *            error message to display if they differ
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(String errorMessage, boolean expected,
+                       boolean actual) throws AssertException {
+               assertEquals(errorMessage, new Boolean(expected), new Boolean(actual));
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(double expected, double actual)
+                       throws AssertException {
+               assertEquals(new Double(expected), new Double(actual));
+       }
+
+       /**
+        * Check that 2 {@link Object}s are equals.
+        * 
+        * @param the
+        *            error message to display if they differ
+        * @param expected
+        *            the expected value
+        * @param actual
+        *            the actual value
+        * 
+        * @throws AssertException
+        *             in case they differ
+        */
+       public void assertEquals(String errorMessage, double expected, double actual)
+                       throws AssertException {
+               assertEquals(errorMessage, new Double(expected), new Double(actual));
+       }
+}
diff --git a/src/be/nikiroo/utils/test/TestLauncher.java b/src/be/nikiroo/utils/test/TestLauncher.java
new file mode 100644 (file)
index 0000000..ba4e81d
--- /dev/null
@@ -0,0 +1,388 @@
+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();
+       }
+}
diff --git a/src/be/nikiroo/utils/test/bundle_test.properties b/src/be/nikiroo/utils/test/bundle_test.properties
new file mode 100644 (file)
index 0000000..5222c59
--- /dev/null
@@ -0,0 +1,3 @@
+ONE = un
+ONE_SUFFIX = un + suffix
+JAPANESE = 日本語 Nihongo
\ No newline at end of file