From 99d9998c34ebf54cb558fd15b2f61d0ad8645150 Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 15 Aug 2012 17:29:54 +0200 Subject: [PATCH 01/73] workaround for issue #1: Same class names in different modules --- dunit.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dunit.d b/dunit.d index 7c23373..78f0ff3 100644 --- a/dunit.d +++ b/dunit.d @@ -444,7 +444,7 @@ mixin template TestMixin() { } //Register UnitTest class: - string className = typeof(this).stringof; + string className = this.classinfo.name; testClasses ~= className; testNamesByClass[className] = _testMethods.dup; testCallers[className] = &runTest; From 16a9e7384e0cf32c05cac055e18eee335302d5cd Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 15 Aug 2012 21:37:48 +0200 Subject: [PATCH 02/73] fix for issue #2: exit status should be non zero for error --- dunit.d | 29 +++++++++++++++++++++-------- exampleTests.d | 4 ++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/dunit.d b/dunit.d index 78f0ff3..81414ae 100644 --- a/dunit.d +++ b/dunit.d @@ -99,23 +99,23 @@ public static void assertWithTimeout(bool delegate() condition, mixin template DUnitMain() { int main (string[] args) { - runTests(); - return 0; + int result = runTests(); + return result; } } /** * Runs all the unit tests. */ -public static void runTests() { - runTests_Progress(); - //debug: runTests_Tree(); +public static int runTests() { + return runTests_Progress(); + //debug: return runTests_Tree(); } /** * Runs all the unit tests, showing progress dots, and the results at the end. */ -public static void runTests_Progress() { +public static int runTests_Progress() { struct TestError { string testClass; string testName; @@ -225,7 +225,7 @@ public static void runTests_Progress() { writeln(); printOk(); writefln(" (%d Test%s)", testsRun, ((testsRun == 1) ? "" : "s")); - return; + return 0; } /* Errors */ @@ -271,6 +271,7 @@ public static void runTests_Progress() { writeln(); printFailures(); writefln("Tests run: %d, Failures: %d, Errors: %d", testsRun, failedCount, errorCount); + return (errorCount > 0) ? 2 : (failedCount > 0) ? 1 : 0; } version (Posix) { @@ -346,7 +347,12 @@ private static void printFailures() { /** * Runs all the unit tests, showing the test tree as the tests run. */ -public static void runTests_Tree() { +public static int runTests_Tree() { + // FIXME runTests_Progress reports an error for any Throwable that is not an AssertError + // FIXME runTests_Tree reports an error for any Throwable thrown by the test fixture + int failedCount = 0; + int errorCount = 0; + //List Test classes: writeln("Unit tests: "); foreach (string className; testClasses) { @@ -359,6 +365,7 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" ERROR IN CONSTRUCTOR: " ~ className ~ ".this(): " ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++errorCount; } if (testObject is null) { continue; @@ -370,6 +377,7 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" ERROR IN setUpClass: " ~ className ~ ".setUpClass(): " ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++errorCount; } //Run each test of the class: @@ -382,6 +390,7 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" ERROR: setUp" ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++errorCount; } if (!setUpOk) { continue; @@ -396,6 +405,7 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" FAILED: " ~ testName ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++failedCount; } //tearDown (call anyways if test failed) @@ -404,6 +414,7 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" ERROR: tearDown" ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++errorCount; } } @@ -413,8 +424,10 @@ public static void runTests_Tree() { } catch (Throwable t) { writefln(" ERROR IN tearDownClass: " ~ className ~ ".tearDownClass(): " ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + ++errorCount; } } + return (errorCount > 0) ? 2 : (failedCount > 0) ? 1 : 0; } diff --git a/exampleTests.d b/exampleTests.d index f686477..bf66f92 100755 --- a/exampleTests.d +++ b/exampleTests.d @@ -162,10 +162,10 @@ version(DUnit) { //from your main function. mixin DUnitMain; - //void main() {dunit.runTests_Tree();} + //int main() {return dunit.runTests_Tree();} } else { - int main (string[] args) { + void main (string[] args) { writeln("production"); } } From 31090b2f894e78821169598379435be533cbe5b5 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 26 Aug 2012 18:26:30 +0200 Subject: [PATCH 03/73] renamed dunit.d into dunit/frameword.d and added xUnit assertions --- dunit/assertion.d | 259 +++++++++++++++++++++++++++++++++++ dunit.d => dunit/framework.d | 69 +--------- exampleTests.d | 10 +- 3 files changed, 267 insertions(+), 71 deletions(-) create mode 100644 dunit/assertion.d rename dunit.d => dunit/framework.d (87%) diff --git a/dunit/assertion.d b/dunit/assertion.d new file mode 100644 index 0000000..b72aca6 --- /dev/null +++ b/dunit/assertion.d @@ -0,0 +1,259 @@ +module dunit.assertion; + +import core.exception; +import core.thread; +import std.algorithm; +import std.array; +import std.conv; +version (unittest) import std.exception; + +/** + * Asserts that a condition is true. + * Throws: AssertError otherwise + */ +void assertTrue(bool condition, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (condition) + return; + + fail(msg, file, line); +} + +/** + * Asserts that a condition is false. + * Throws: AssertError otherwise + */ +void assertFalse(bool condition, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (!condition) + return; + + fail(msg, file, line); +} + +unittest +{ + assertTrue(true); + assertEquals("Assertion failure", + collectExceptionMsg!AssertError(assertTrue(false))); + + assertFalse(false); + assertEquals("Assertion failure", + collectExceptionMsg!AssertError(assertFalse(true))); +} + +/** + * Asserts that the values are equal. + * Throws: AssertError otherwise + */ +void assertEquals(T, U)(T expected, U actual, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (expected == actual) + return; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ "expected: <" ~ to!string(expected) ~ "> but was: <"~ to!string(actual) ~ ">", + file, line); +} + +unittest +{ + assertEquals("foo", "foo"); + assertEquals("expected: but was: ", + collectExceptionMsg!AssertError(assertEquals("foo", "bar"))); + + assertEquals(42, 42); + assertEquals("expected: <42> but was: <23>", + collectExceptionMsg!AssertError(assertEquals(42, 23))); + + assertEquals(42.0, 42.0); + + Object foo = new Object(); + Object bar = null; + + assertEquals(foo, foo); + assertEquals(bar, bar); + assertEquals("expected: but was: ", + collectExceptionMsg!AssertError(assertEquals(foo, bar))); +} + +/** + * Asserts that the arrays are equal. + * Throws: AssertError otherwise + */ +void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + string header = (msg.empty) ? null : msg ~ "; "; + + for (size_t index = 0; index < min(expecteds.length, actuals.length); ++index) + { + assertEquals(expecteds[index], actuals[index], + header ~ "array mismatch at index " ~ to!string(index), + file, line); + } + assertEquals(expecteds.length, actuals.length, + header ~ "array length mismatch", + file, line); +} + +unittest +{ + int[] expecteds = [1, 2, 3]; + double[] actuals = [1, 2, 3]; + + assertArrayEquals(expecteds, actuals); + assertEquals("array mismatch at index 1; expected: <2> but was: <2.3>", + collectExceptionMsg!AssertError(assertArrayEquals(expecteds, [1, 2.3]))); + assertEquals("array length mismatch; expected: <3> but was: <2>", + collectExceptionMsg!AssertError(assertArrayEquals(expecteds, [1, 2]))); + assertEquals("array mismatch at index 2; expected: but was: ", + collectExceptionMsg!AssertError(assertArrayEquals("bar", "baz"))); +} + +/** + * Asserts that the value is null. + * Throws: AssertError otherwise + */ +void assertNull(T)(T actual, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (actual is null) + return; + + fail(msg, file, line); +} + +/** + * Asserts that the value is not null. + * Throws: AssertError otherwise + */ +void assertNotNull(T)(T actual, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (actual !is null) + return; + + fail(msg, file, line); +} + +unittest +{ + Object foo = new Object(); + + assertNull(null); + assertEquals("Assertion failure", + collectExceptionMsg!AssertError(assertNull(foo))); + + assertNotNull(foo); + assertEquals("Assertion failure", + collectExceptionMsg!AssertError(assertNotNull(null))); +} + +/** + * Asserts that the values are the same. + * Throws: AssertError otherwise + */ +void assertSame(T, U)(T expected, U actual, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (expected is actual) + return; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ "expected same: <" ~ to!string(expected) ~ "> was not: <"~ to!string(actual) ~ ">", + file, line); +} + +/** + * Asserts that the values are not the same. + * Throws: AssertError otherwise + */ +void assertNotSame(T, U)(T expected, U actual, string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (expected !is actual) + return; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ "expected not same", + file, line); +} + +unittest +{ + Object foo = new Object(); + Object bar = new Object(); + + assertSame(foo, foo); + assertEquals("expected same: was not: ", + collectExceptionMsg!AssertError(assertSame(foo, bar))); + + assertNotSame(foo, bar); + assertEquals("expected not same", + collectExceptionMsg!AssertError(assertNotSame(foo, foo))); +} + +/** + * Fails a test. + * Throws: AssertError + */ +void fail(string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + throw (msg.empty) ? new AssertError(file, line) : new AssertError(msg, file, line); +} + +unittest +{ + assertEquals("Assertion failure", + collectExceptionMsg!AssertError(fail())); +} + +/** Checks a delegate until the timeout expires. The assert error is produced + * if the delegate fails to return 'true' before the timeout. + * + * The parameter timeoutMsecs determines the maximum timeout to wait before + * asserting a failure (default is 500ms). + * + * The parameter recheckTimeMsecs determines how often the predicate will + * be checked (default is 10ms). + * + * This kind of assertion is very useful to check on code that runs in another + * thread. For instance, the thread that listens to a socket. + */ +public static void assertWithTimeout(bool delegate() condition, + int timeoutMsecs = 500, int recheckTimeMsecs = 10, + string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + int count = 0; + + while (!condition()) { + if (recheckTimeMsecs * count > timeoutMsecs) { + if (msg is null) { + msg = "Timeout elapsed for condition."; + } + throw new core.exception.AssertError(msg, file, line); + } + + Thread.sleep(dur!"msecs"(recheckTimeMsecs)); + ++count; + } +} diff --git a/dunit.d b/dunit/framework.d similarity index 87% rename from dunit.d rename to dunit/framework.d index 81414ae..fafb0e4 100644 --- a/dunit.d +++ b/dunit/framework.d @@ -18,85 +18,20 @@ * http://www.boost.org/LICENSE_1_0.txt) */ -module dunit; +module dunit.framework; +public import dunit.assertion; import std.stdio; import std.conv; -import core.thread; import core.time; - string[] testClasses; string[][string] testNamesByClass; void function(Object o, string testName)[string] testCallers; Object function()[string] testCreators; -/** Asserts that both values are equal. */ -public static void assertEquals(T)(T s, T t, - string file = __FILE__, - size_t line = __LINE__) - if (!__traits(isScalar, T)) -{ - if (s is t) { - return; - } - if (s != t) { - throw new core.exception.AssertError( - "Expected: '"~to!string(s)~"', but was: '"~to!string(t)~"'", - file, line); - } -} - -/** Asserts that both values are equal. This function checks values of - * different scalar types, that, though being different types, they - * can be compared */ -public static void assertEquals(S, T)(S s, T t, - string file = __FILE__, - size_t line = __LINE__) - if (__traits(isScalar, T) && __traits(isScalar, S)) -{ - if (s != t) { - throw new core.exception.AssertError( - "Expected: '"~to!string(s)~"', but was: '"~to!string(t)~"'", - file, line); - } -} - -/** Checks a delegate until the timeout expires. The assert error is produced - * if the delegate fails to return 'true' before the timeout. - * - * The parameter timeoutMsecs determines the maximum timeout to wait before - * asserting a failure (default is 500ms). - * - * The parameter recheckTimeMsecs determines how often the predicate will - * be checked (default is 10ms). - * - * This kind of assertion is very useful to check on code that runs in another - * thread. For instance, the thread that listens to a socket. - */ -public static void assertWithTimeout(bool delegate() condition, - int timeoutMsecs = 500, int recheckTimeMsecs = 10, - string msg = null, - string file = __FILE__, - size_t line = __LINE__) -{ - int count = 0; - - while (!condition()) { - if (recheckTimeMsecs * count > timeoutMsecs) { - if (msg is null) { - msg = "Timeout elapsed for condition."; - } - throw new core.exception.AssertError(msg, file, line); - } - - Thread.sleep(dur!"msecs"(recheckTimeMsecs)); - ++count; - } -} - mixin template DUnitMain() { int main (string[] args) { int result = runTests(); diff --git a/exampleTests.d b/exampleTests.d index bf66f92..5773c1e 100755 --- a/exampleTests.d +++ b/exampleTests.d @@ -1,4 +1,4 @@ -#!/usr/bin/rdmd +#!/usr/bin/env rdmd /** Unit testing framework ('dunit') * @@ -20,9 +20,11 @@ * http://www.boost.org/LICENSE_1_0.txt) */ module ExampleTests; -import std.stdio, std.string; + +import dunit.framework; import core.thread; -import dunit; +import std.stdio; +import std.string; //Minimal example: @@ -178,7 +180,7 @@ version(DUnit) { * application starts: */ unittest { - dunit.runTests(); + runTests(); } /* From 7f22b9df6055bacdb0e54cf59e711df7194eacf9 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 26 Aug 2012 21:33:48 +0200 Subject: [PATCH 04/73] changed assertWithTimeout into assertEventually --- README | 227 +--------------------------------------------- dunit/assertion.d | 56 +++++++----- exampleTests.d | 5 +- 3 files changed, 38 insertions(+), 250 deletions(-) diff --git a/README b/README index 6a3334e..1764b67 100644 --- a/README +++ b/README @@ -4,229 +4,4 @@ Allows to define unittests simply as methods which names start with 'test'. The only thing necessary to create a unit test class, is to declare the mixin TestMixin inside the class. This will register -the class and its test methods for the test runner. - -License: Boost License 1.0. -Authors: Juan Manuel Cabo -Version: 0.6 -Source: dunit.d -Last update: 2012-03-21 - - Copyright Juan Manuel Cabo 2012. -Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at - http://www.boost.org/LICENSE_1_0.txt) ---------------------------------------------------------------------------------- - -module ExampleTests; -import std.stdio, std.string; -import core.thread; -import dunit; - - -//Minimal example: -class ATestClass { - mixin TestMixin; - - void testExample() { - assertEquals("bla", "b"~"la"); - } -} - - -/** - * Look!! no test base class needed!! - */ -class AbcTest { - //This declaration here is the only thing needed to mark a class - //as a unit test class: - mixin TestMixin; - - //Variable members that start with 'test' are allowed. - public int testN = 3; - public int testM = 4; - - //Any method whose name starts with 'test' is run as a unit test: - //(NOTE: this is bound at compile time, there is no overhead). - public void test1() { - assert(true); - } - - public void test2() { - //You can use D's assert() function: - assert(1 == 2 / 2); - //Or dunit convenience asserts (just edit dunit.d to add more): - assertEquals(1, 2/2); - //The expected and actual values will be shown in the output: - assertEquals("my string looks dazzling", "my dtring looks sazzling"); - } - - //Test methods with default arguments work, as long as they can - //be called without arguments, ie: as testDefaultArguments() for instance: - public void testDefaultArguments(int a=4, int b=3) { - } - - //Even if the method is private to the unit test class, it is still run. - private void test5(int a=4) { - } - - //This test was disabled just by adding an underscore to the name: - public void _testAnother() { - assert(false, "fails"); - } - - //Optional initialization and de-initialization. - // setUp() and tearDown() are called around each individual test. - // setUpClass() and tearDownClass() are called once around the whole unit test. - public void setUp() { - } - public void tearDown() { - } - public void setUpClass() { - } - public void tearDownClass() { - } -} - - -class DerivedTest : AbcTest { - mixin TestMixin; - - //Base class tests will be run!!!!!! - // - //You can for instance override setUpClass() and change the target - //implementation of a family of classes that you are testing. - // - //For instance: Run a set of tests against all derived classes of - //the Stream class. You do this by keeping all the tests in a parent - //test class, and creating a derived TestFixture for each one, - //that all it has to do is instantiate the instance under test in the - //overriden setUpClass(). -} - - -/** - * You can write asynchronous tests too!! Test those socket listeners of - * yours, or your active thread objects, etc.!! - */ -class AsynchronousTestExample { - mixin TestMixin; - Thread theThread; - bool threadDidItsThing; - - //Prepare the test: - void setUp() { - threadDidItsThing = false; - theThread = new Thread(&threadFunction); - } - - //Cleanup: - void tearDown() { - theThread.join(); - theThread = null; - } - - void threadFunction() { - threadDidItsThing = true; - } - - void testThreadActuallyRuns() { - assertEquals(false, threadDidItsThing); - - //Start the thread - theThread.start(); - - //Assert that within a period of time (500ms by default), the variable - //threadDidItsThing gets toggled: - assertWithTimeout({return threadDidItsThing;}); - } -} - - -version = DUnit; - -version(DUnit) { - - //-All you need to run the tests, is to declare - // - // mixin DUnitMain. - // - //-You can alternatively call - // - // dunit.runTests_Progress(); for java style results - // output (SHOWS COLORS IF IN UNIX !!!) - // or - // dunit.runTests_Tree(); for a more verbose output - // - //from your main function. - - mixin DUnitMain; - //void main() {dunit.runTests_Tree();} - -} else { - int main (string[] args) { - writeln("production"); - } -} - - -/* - * Alternatively, you can run your DUnit tests when passing -unittest - * to the compiler. This only needs to be declared once for the whole - * application, and will run all the tests in all modules before the - * application starts: - */ -unittest { - dunit.runTests(); -} - -/* - -Run this file with (works in Windows/Linux): - - - dmd exampleTests.d dunit.d - ./exampleTests - - -The output will be (java style): - - - ...F....F... - There were 2 failures: - 1) test2(AbcTest)core.exception.AssertError@exampleTests.d(61): - Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - 2) test2(DerivedTest)core.exception.AssertError@exampleTests.d(61): - Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - - FAILURES!!! - Tests run: 10, Failures: 2, Errors: 0 - - -If you use the more verbose method dunit.runTests_Tree(), then the output is: - - - Unit tests: - ATestClass - OK: 0.01 ms testExample() - AbcTest - OK: 0.00 ms test1() - FAILED: test2(): core.exception.AssertError@exampleTests.d(62): - Expected: 'my string looks dazzling', but was: - 'my dtring looks sazzling' - OK: 0.00 ms testDefaultArguments() - OK: 0.00 ms test5() - DerivedTest - OK: 0.01 ms test1() - FAILED: test2(): core.exception.AssertError@exampleTests.d(62): - Expected: 'my string looks dazzling', but was: - 'my dtring looks sazzling' - OK: 0.00 ms testDefaultArguments() - OK: 0.00 ms test5() - AsynchronousTestExample - OK: 11.00 ms testThreadActuallyRuns() - - -HAVE FUN! - -*/ +the class and its test methods for the test runner. \ No newline at end of file diff --git a/dunit/assertion.d b/dunit/assertion.d index b72aca6..d45c4e3 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -225,35 +225,47 @@ unittest collectExceptionMsg!AssertError(fail())); } -/** Checks a delegate until the timeout expires. The assert error is produced - * if the delegate fails to return 'true' before the timeout. +/** + * Checks a probe until the timeout expires. The assert error is produced + * if the probe fails to return 'true' before the timeout. * - * The parameter timeoutMsecs determines the maximum timeout to wait before + * The parameter timeout determines the maximum timeout to wait before * asserting a failure (default is 500ms). * - * The parameter recheckTimeMsecs determines how often the predicate will - * be checked (default is 10ms). + * The parameter delay determines how often the predicate will be + * checked (default is 10ms). * * This kind of assertion is very useful to check on code that runs in another * thread. For instance, the thread that listens to a socket. + * + * Throws: AssertError when the probe fails to become true before timeout */ -public static void assertWithTimeout(bool delegate() condition, - int timeoutMsecs = 500, int recheckTimeMsecs = 10, - string msg = null, - string file = __FILE__, - size_t line = __LINE__) +public static void assertEventually(bool delegate() probe, + Duration timeout = dur!"msecs"(500), Duration delay = dur!"msecs"(10), + string msg = null, + string file = __FILE__, + size_t line = __LINE__) { - int count = 0; + TickDuration startTime = TickDuration.currSystemTick(); - while (!condition()) { - if (recheckTimeMsecs * count > timeoutMsecs) { - if (msg is null) { - msg = "Timeout elapsed for condition."; - } - throw new core.exception.AssertError(msg, file, line); - } - - Thread.sleep(dur!"msecs"(recheckTimeMsecs)); - ++count; - } + while (!probe()) { + Duration elapsedTime = cast(Duration)(TickDuration.currSystemTick() - startTime); + + if (elapsedTime >= timeout) { + if (msg.empty) { + msg = "timed out"; + } + fail(msg, file, line); + } + + Thread.sleep(delay); + } +} + +unittest +{ + assertEventually({ static count = 0; return ++count > 42; }); + + assertEquals("timed out", + collectExceptionMsg!AssertError(assertEventually({ return false; }))); } diff --git a/exampleTests.d b/exampleTests.d index 5773c1e..4ffb971 100755 --- a/exampleTests.d +++ b/exampleTests.d @@ -141,7 +141,7 @@ class AsynchronousTestExample { //Assert that within a period of time (500ms by default), the variable //threadDidItsThing gets toggled: - assertWithTimeout({return threadDidItsThing;}); + assertEventually({ return threadDidItsThing; }); } } @@ -180,7 +180,8 @@ version(DUnit) { * application starts: */ unittest { - runTests(); + //runTests(); + runTests_Tree(); } /* From 644dd501370c3b971fc9f790f9494475899d850f Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 26 Aug 2012 21:38:24 +0200 Subject: [PATCH 05/73] removed unused variable --- dunit/framework.d | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dunit/framework.d b/dunit/framework.d index fafb0e4..d9f5cae 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -68,8 +68,6 @@ public static int runTests_Progress() { TestError[] errors; int testsRun = 0; - startDots(); - foreach (string className; testClasses) { //Create the class: Object testObject = null; @@ -138,8 +136,6 @@ public static int runTests_Progress() { errors ~= TestError(className, "tearDownClass", t); } } - - endDots(); /* Count how many problems where asserts, and how many other exceptions. */ @@ -247,26 +243,16 @@ version (Posix) { } } -private static bool showingRed = false; -private static void startDots() { - showingRed = false; -} private static void printDot() { startColorGreen(); write("."); stdout.flush(); endColors(); } private static void printF() { - if (!showingRed) { - showingRed = true; - } startColorRed(); write("F"); stdout.flush(); endColors(); } -private static void endDots() { - showingRed = false; -} private static void printOk() { startColorGreen(); write("OK"); From e96966495d218aaa16365f7fb42f1a9cd070694d Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 2 Sep 2012 22:22:11 +0200 Subject: [PATCH 06/73] added filter option --- dunit/framework.d | 57 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/dunit/framework.d b/dunit/framework.d index d9f5cae..9203389 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -21,8 +21,10 @@ module dunit.framework; public import dunit.assertion; -import std.stdio; import std.conv; +import std.getopt; +import std.regex; +import std.stdio; import core.time; @@ -34,23 +36,60 @@ Object function()[string] testCreators; mixin template DUnitMain() { int main (string[] args) { - int result = runTests(); - return result; + return dunit_main(args); + } +} + +public int dunit_main(string[] args) +{ + string[] filters = null; + bool verbose = false; + + getopt(args, "filter|f", &filters, "verbose|v", &verbose); + + string[][string] pickedTestNamesByClass = null; + + if (filters is null) + { + filters = [null]; + } + foreach (className; testNamesByClass.keys) + { + foreach (testName; testNamesByClass[className]) + { + string fullyQualifiedName = className ~ '.' ~ testName; + + foreach (filter; filters) + { + if (match(fullyQualifiedName, filter)) + { + pickedTestNamesByClass[className] ~= testName; + break; + } + } + } + } + if (verbose) + { + return runTests_Tree(pickedTestNamesByClass); } + else + { + return runTests_Progress(pickedTestNamesByClass); + } } /** * Runs all the unit tests. */ public static int runTests() { - return runTests_Progress(); - //debug: return runTests_Tree(); + return runTests_Progress(testNamesByClass); } /** * Runs all the unit tests, showing progress dots, and the results at the end. */ -public static int runTests_Progress() { +public static int runTests_Progress(string[][string] testNamesByClass) { struct TestError { string testClass; string testName; @@ -68,7 +107,7 @@ public static int runTests_Progress() { TestError[] errors; int testsRun = 0; - foreach (string className; testClasses) { + foreach (string className; testNamesByClass.keys) { //Create the class: Object testObject = null; try { @@ -268,7 +307,7 @@ private static void printFailures() { /** * Runs all the unit tests, showing the test tree as the tests run. */ -public static int runTests_Tree() { +public static int runTests_Tree(string[][string] testNamesByClass) { // FIXME runTests_Progress reports an error for any Throwable that is not an AssertError // FIXME runTests_Tree reports an error for any Throwable thrown by the test fixture int failedCount = 0; @@ -276,7 +315,7 @@ public static int runTests_Tree() { //List Test classes: writeln("Unit tests: "); - foreach (string className; testClasses) { + foreach (string className; testNamesByClass.keys) { writeln(" " ~ className); //Create the class: From 2c6554882cdde84fe60b07f733d9c564f22fdba7 Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 5 Feb 2013 22:36:26 +0100 Subject: [PATCH 07/73] updated copyright --- dunit/assertion.d | 10 ++++++++++ dunit/framework.d | 20 ++++++++------------ exampleTests.d | 21 +++++++++------------ 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/dunit/assertion.d b/dunit/assertion.d index d45c4e3..6c57a57 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -1,3 +1,13 @@ +/** + * Unit testing framework ('dunit') + */ + +// Copyright Juan Manuel Cabo 2012. +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + module dunit.assertion; import core.exception; diff --git a/dunit/framework.d b/dunit/framework.d index 9203389..60d0d6a 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -1,23 +1,19 @@ -/** Unit testing framework ('dunit') +/** + * Unit testing framework ('dunit') * * Allows to define unittests simply as methods which names start * with 'test'. * The only thing necessary to create a unit test class, is to * declare the mixin TestMixin inside the class. This will register * the class and its test methods for the test runner. - * - * License: Boost License 1.0. - * Authors: Juan Manuel Cabo - * Version: 0.6 - * Source: dunit.d - * Last update: 2012-03-21 - */ -/* Copyright Juan Manuel Cabo 2012. - * Distributed under the Boost Software License, Version 1.0. - * (See accompanying file LICENSE_1_0.txt or copy at - * http://www.boost.org/LICENSE_1_0.txt) */ +// Copyright Juan Manuel Cabo 2012. +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + module dunit.framework; public import dunit.assertion; diff --git a/exampleTests.d b/exampleTests.d index 4ffb971..9b44356 100755 --- a/exampleTests.d +++ b/exampleTests.d @@ -1,24 +1,21 @@ #!/usr/bin/env rdmd -/** Unit testing framework ('dunit') +/** + * Unit testing framework ('dunit') * * Allows to define unittests simply as methods which names start * with 'test'. * The only thing necessary to create a unit test class, is to * declare the mixin TestMixin inside the class. This will register * the class and its test methods for the test runner. - * - * License: Boost License 1.0. - * Authors: Juan Manuel Cabo - * Version: 0.6 - * Source: dunit.d - * Last update: 2012-03-21 - */ -/* Copyright Juan Manuel Cabo 2012. - * Distributed under the Boost Software License, Version 1.0. - * (See accompanying file LICENSE_1_0.txt or copy at - * http://www.boost.org/LICENSE_1_0.txt) */ + +// Copyright Juan Manuel Cabo 2012. +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + module ExampleTests; import dunit.framework; From 09f934637d7920e320f453bf1c0fc09d6b16274f Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 5 Feb 2013 22:41:12 +0100 Subject: [PATCH 08/73] straightened method name comparisons --- dunit/framework.d | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dunit/framework.d b/dunit/framework.d index 60d0d6a..5d5e0b1 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -17,6 +17,7 @@ module dunit.framework; public import dunit.assertion; +import std.algorithm; import std.conv; import std.getopt; import std.regex; @@ -426,7 +427,7 @@ mixin template TestMixin() { } else { //Skip strings that don't start with "test": - static if (args[0].length < 4 || args[0][0..4] != "test" + static if (!startsWith(args[0], "test") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { static if(args.length == 1) { @@ -470,11 +471,9 @@ mixin template TestMixin() { immutable(string) ret = ""; } else { //Skip method names that don't start with 'test': - static if (((args[0].length < 4 || args[0][0..4] != "test") - && (args[0].length < 5 || args[0][0..5] != "setUp") - && (args[0].length < 8 || args[0][0..8] != "tearDown") - && (args[0].length < 10 || args[0][0..10] != "setUpClass") - && (args[0].length < 13 || args[0][0..13] != "tearDownClass")) + static if (!(startsWith(args[0], "test") + || args[0] == "setUp" || args[0] == "tearDown" + || args[0] == "setUpClass" || args[0] == "tearDownClass") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { static if (args.length == 1) { From 2bdff8a167cd701e59e3673f2b561a561318141e Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 6 Feb 2013 22:30:04 +0100 Subject: [PATCH 09/73] UFCS --- dunit/framework.d | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dunit/framework.d b/dunit/framework.d index 5d5e0b1..a151beb 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -427,7 +427,7 @@ mixin template TestMixin() { } else { //Skip strings that don't start with "test": - static if (!startsWith(args[0], "test") + static if (!args[0].startsWith("test") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { static if(args.length == 1) { @@ -470,8 +470,7 @@ mixin template TestMixin() { static if (args.length == 0) { immutable(string) ret = ""; } else { - //Skip method names that don't start with 'test': - static if (!(startsWith(args[0], "test") + static if (!(args[0].startsWith("test") || args[0] == "setUp" || args[0] == "tearDown" || args[0] == "setUpClass" || args[0] == "tearDownClass") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) @@ -482,7 +481,6 @@ mixin template TestMixin() { immutable(string) ret = generateRunTestImpl!(T, args[1..$]).ret; } } else { - //Create the case statement that calls that test: static if (args.length == 1) { immutable(string) ret = From 9fe9156f2b2bc4bea8bb9316f0346975355b8d86 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 16 Mar 2013 22:53:52 +0100 Subject: [PATCH 10/73] changed to distinguish between failures and errors --- README => README.md | 5 +- dunit/assertion.d | 63 +++-- dunit/framework.d | 618 ++++++++++++++++++++++++++++---------------- exampleTests.d | 114 ++++---- 4 files changed, 500 insertions(+), 300 deletions(-) rename README => README.md (65%) diff --git a/README b/README.md similarity index 65% rename from README rename to README.md index 1764b67..cbdd1ee 100644 --- a/README +++ b/README.md @@ -1,7 +1,8 @@ Unit testing framework ('dunit') +================================ + +Allows to define unittests simply as methods which names start with 'test'. -Allows to define unittests simply as methods which names start -with 'test'. The only thing necessary to create a unit test class, is to declare the mixin TestMixin inside the class. This will register the class and its test methods for the test runner. \ No newline at end of file diff --git a/dunit/assertion.d b/dunit/assertion.d index 6c57a57..adc7625 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -17,9 +17,22 @@ import std.array; import std.conv; version (unittest) import std.exception; +/** + * Thrown on an assertion failure. + */ +class AssertException : Exception +{ + this(string msg = null, + string file = __FILE__, + size_t line = __LINE__) + { + super(msg.empty ? "Assertion failure" : msg, file, line); + } +} + /** * Asserts that a condition is true. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertTrue(bool condition, string msg = null, string file = __FILE__, @@ -33,7 +46,7 @@ void assertTrue(bool condition, string msg = null, /** * Asserts that a condition is false. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertFalse(bool condition, string msg = null, string file = __FILE__, @@ -49,16 +62,16 @@ unittest { assertTrue(true); assertEquals("Assertion failure", - collectExceptionMsg!AssertError(assertTrue(false))); + collectExceptionMsg!AssertException(assertTrue(false))); assertFalse(false); assertEquals("Assertion failure", - collectExceptionMsg!AssertError(assertFalse(true))); + collectExceptionMsg!AssertException(assertFalse(true))); } /** * Asserts that the values are equal. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertEquals(T, U)(T expected, U actual, string msg = null, string file = __FILE__, @@ -77,11 +90,11 @@ unittest { assertEquals("foo", "foo"); assertEquals("expected: but was: ", - collectExceptionMsg!AssertError(assertEquals("foo", "bar"))); + collectExceptionMsg!AssertException(assertEquals("foo", "bar"))); assertEquals(42, 42); assertEquals("expected: <42> but was: <23>", - collectExceptionMsg!AssertError(assertEquals(42, 23))); + collectExceptionMsg!AssertException(assertEquals(42, 23))); assertEquals(42.0, 42.0); @@ -91,12 +104,12 @@ unittest assertEquals(foo, foo); assertEquals(bar, bar); assertEquals("expected: but was: ", - collectExceptionMsg!AssertError(assertEquals(foo, bar))); + collectExceptionMsg!AssertException(assertEquals(foo, bar))); } /** * Asserts that the arrays are equal. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, string msg = null, string file = __FILE__, @@ -122,16 +135,16 @@ unittest assertArrayEquals(expecteds, actuals); assertEquals("array mismatch at index 1; expected: <2> but was: <2.3>", - collectExceptionMsg!AssertError(assertArrayEquals(expecteds, [1, 2.3]))); + collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2.3]))); assertEquals("array length mismatch; expected: <3> but was: <2>", - collectExceptionMsg!AssertError(assertArrayEquals(expecteds, [1, 2]))); + collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2]))); assertEquals("array mismatch at index 2; expected: but was: ", - collectExceptionMsg!AssertError(assertArrayEquals("bar", "baz"))); + collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); } /** * Asserts that the value is null. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertNull(T)(T actual, string msg = null, string file = __FILE__, @@ -145,7 +158,7 @@ void assertNull(T)(T actual, string msg = null, /** * Asserts that the value is not null. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertNotNull(T)(T actual, string msg = null, string file = __FILE__, @@ -163,16 +176,16 @@ unittest assertNull(null); assertEquals("Assertion failure", - collectExceptionMsg!AssertError(assertNull(foo))); + collectExceptionMsg!AssertException(assertNull(foo))); assertNotNull(foo); assertEquals("Assertion failure", - collectExceptionMsg!AssertError(assertNotNull(null))); + collectExceptionMsg!AssertException(assertNotNull(null))); } /** * Asserts that the values are the same. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertSame(T, U)(T expected, U actual, string msg = null, string file = __FILE__, @@ -189,7 +202,7 @@ void assertSame(T, U)(T expected, U actual, string msg = null, /** * Asserts that the values are not the same. - * Throws: AssertError otherwise + * Throws: AssertException otherwise */ void assertNotSame(T, U)(T expected, U actual, string msg = null, string file = __FILE__, @@ -211,28 +224,28 @@ unittest assertSame(foo, foo); assertEquals("expected same: was not: ", - collectExceptionMsg!AssertError(assertSame(foo, bar))); + collectExceptionMsg!AssertException(assertSame(foo, bar))); assertNotSame(foo, bar); assertEquals("expected not same", - collectExceptionMsg!AssertError(assertNotSame(foo, foo))); + collectExceptionMsg!AssertException(assertNotSame(foo, foo))); } /** * Fails a test. - * Throws: AssertError + * Throws: AssertException */ void fail(string msg = null, string file = __FILE__, size_t line = __LINE__) { - throw (msg.empty) ? new AssertError(file, line) : new AssertError(msg, file, line); + throw new AssertException(msg, file, line); } unittest { assertEquals("Assertion failure", - collectExceptionMsg!AssertError(fail())); + collectExceptionMsg!AssertException(fail())); } /** @@ -248,7 +261,7 @@ unittest * This kind of assertion is very useful to check on code that runs in another * thread. For instance, the thread that listens to a socket. * - * Throws: AssertError when the probe fails to become true before timeout + * Throws: AssertException when the probe fails to become true before timeout */ public static void assertEventually(bool delegate() probe, Duration timeout = dur!"msecs"(500), Duration delay = dur!"msecs"(10), @@ -277,5 +290,5 @@ unittest assertEventually({ static count = 0; return ++count > 42; }); assertEquals("timed out", - collectExceptionMsg!AssertError(assertEventually({ return false; }))); + collectExceptionMsg!AssertException(assertEventually({ return false; }))); } diff --git a/dunit/framework.d b/dunit/framework.d index a151beb..540e3fa 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -1,11 +1,10 @@ /** * Unit testing framework ('dunit') * - * Allows to define unittests simply as methods which names start - * with 'test'. - * The only thing necessary to create a unit test class, is to - * declare the mixin TestMixin inside the class. This will register - * the class and its test methods for the test runner. + * Allows to define unittests simply as methods which names start with "test". + * The only thing necessary to create a unit test class, is to declare + * the mixin TestMixin inside the class. This will register the class + * and its test methods for the test runner. */ // Copyright Juan Manuel Cabo 2012. @@ -18,21 +17,22 @@ module dunit.framework; public import dunit.assertion; import std.algorithm; +import std.array; import std.conv; import std.getopt; import std.regex; import std.stdio; import core.time; - string[] testClasses; string[][string] testNamesByClass; void function(Object o, string testName)[string] testCallers; Object function()[string] testCreators; - -mixin template DUnitMain() { - int main (string[] args) { +mixin template DUnitMain() +{ + int main (string[] args) + { return dunit_main(args); } } @@ -40,17 +40,17 @@ mixin template DUnitMain() { public int dunit_main(string[] args) { string[] filters = null; + bool list = false; bool verbose = false; - getopt(args, "filter|f", &filters, "verbose|v", &verbose); + getopt(args, "filter|f", &filters, "list|l", &list, "verbose|v", &verbose); - string[][string] pickedTestNamesByClass = null; + string[][string] selectedTestNamesByClass = null; if (filters is null) - { filters = [null]; - } - foreach (className; testNamesByClass.keys) + + foreach (className; testNamesByClass.byKey) { foreach (testName; testNamesByClass[className]) { @@ -60,241 +60,318 @@ public int dunit_main(string[] args) { if (match(fullyQualifiedName, filter)) { - pickedTestNamesByClass[className] ~= testName; + selectedTestNamesByClass[className] ~= testName; break; } } } } - if (verbose) + + if (list) { - return runTests_Tree(pickedTestNamesByClass); + foreach (className; selectedTestNamesByClass.byKey) + { + foreach (testName; selectedTestNamesByClass[className]) + { + string fullyQualifiedName = className ~ '.' ~ testName; + + writeln(fullyQualifiedName); + } + } + return 0; } + + if (verbose) + return runTests_Tree(selectedTestNamesByClass); else - { - return runTests_Progress(pickedTestNamesByClass); - } + return runTests_Progress(selectedTestNamesByClass); } /** * Runs all the unit tests. */ -public static int runTests() { +public static int runTests() +{ return runTests_Progress(testNamesByClass); } /** * Runs all the unit tests, showing progress dots, and the results at the end. */ -public static int runTests_Progress(string[][string] testNamesByClass) { - struct TestError { +public static int runTests_Progress(string[][string] testNamesByClass) +{ + struct Entry + { string testClass; string testName; - Throwable error; - bool isAssertError; + Throwable throwable; - this(string tc, string tn, Throwable er) { - this.testClass = tc; - this.testName = tn; - this.error = er; - this.isAssertError = (typeid(er) is typeid(core.exception.AssertError)); + this(string testClass, string testName, Throwable throwable) + { + this.testClass = testClass; + this.testName = testName; + this.throwable = throwable; } } - TestError[] errors; - int testsRun = 0; + Entry[] failures = null; + Entry[] errors = null; + int count = 0; - foreach (string className; testNamesByClass.keys) { - //Create the class: + foreach (string className; testNamesByClass.byKey) + { + // create test object Object testObject = null; - try { + + try + { testObject = testCreators[className](); - } catch (Throwable t) { - errors ~= TestError(className, "CONSTRUCTOR", t); + } + catch (AssertException exception) + { + failures ~= Entry(className, "this", exception); printF(); } - if (testObject is null) { - continue; + catch (Throwable throwable) + { + errors ~= Entry(className, "this", throwable); + printF(); } - //setUpClass - try { + if (testObject is null) + continue; + + // setUpClass + try + { testCallers[className](testObject, "setUpClass"); - } catch (Throwable t) { - errors ~= TestError(className, "setUpClass", t); + } + catch (AssertException exception) + { + failures ~= Entry(className, "setUpClass", exception); + continue; + } + catch (Throwable throwable) + { + errors ~= Entry(className, "setUpClass", throwable); + continue; } - //Run each test of the class: - foreach (string testName; testNamesByClass[className]) { - ++testsRun; + // run each test function of the class + foreach (string testName; testNamesByClass[className]) + { + ++count; - printDot(); + // setUp + bool success = true; - //setUp - bool setUpOk = false; - bool allOk = true; - try { + try + { testCallers[className](testObject, "setUp"); - setUpOk = true; - } catch (Throwable t) { - errors ~= TestError(className, "setUp", t); + } + catch (AssertException exception) + { + failures ~= Entry(className, "setUp", exception); printF(); + continue; } - if (!setUpOk) { + catch (Throwable throwable) + { + errors ~= Entry(className, "setUp", throwable); + printF(); continue; } - //test - try { + // test + try + { testCallers[className](testObject, testName); - } catch (Throwable t) { - errors ~= TestError(className, testName, t); - allOk = false; + } + catch (AssertException exception) + { + failures ~= Entry(className, testName, exception); + success = false; + } + catch (Throwable throwable) + { + errors ~= Entry(className, testName, throwable); + success = false; } - //tearDown (call anyways if test failed) - try { + // tearDown (even if test failed) + try + { testCallers[className](testObject, "tearDown"); - } catch (Throwable t) { - errors ~= TestError(className, "tearDown", t); - allOk = false; + } + catch (AssertException exception) + { + failures ~= Entry(className, "tearDown", exception); + success = false; + } + catch (Throwable throwable) + { + errors ~= Entry(className, "tearDown", throwable); + success = false; } - if (!allOk) { + if (success) + printDot(); + else printF(); - } } - //tearDownClass - try { + // tearDownClass + try + { testCallers[className](testObject, "tearDownClass"); - } catch (Throwable t) { - errors ~= TestError(className, "tearDownClass", t); } - } - - /* Count how many problems where asserts, and how many other exceptions. - */ - int failedCount = 0; - int errorCount = 0; - foreach (TestError te; errors) { - if (te.isAssertError) { - ++failedCount; - } else { - ++errorCount; + catch (AssertException exception) + { + failures ~= Entry(className, "tearDownClass", exception); + } + catch (Throwable throwable) + { + errors ~= Entry(className, "tearDownClass", throwable); } } - - /* Display results - */ + + // report results writeln(); - if (failedCount == 0 && errorCount == 0) { + if (failures.empty && errors.empty) + { writeln(); printOk(); - writefln(" (%d Test%s)", testsRun, ((testsRun == 1) ? "" : "s")); + writefln(" (%d %s)", count, (count == 1) ? "Test" : "Tests"); return 0; } - /* Errors - */ - if (errorCount != 0) { - if (errorCount == 1) { + + // report errors + if (!errors.empty) + { + if (errors.length == 1) writeln("There was 1 error:"); - } else { - writefln("There were %d errors:", errorCount); - } - int i = 0; - foreach (TestError te; errors) { - //Errors are any exception except AssertError; - if (te.isAssertError) { - continue; - } - Throwable t = te.error; - writefln("%d) %s(%s)%s@%s(%d): %s", ++i, - te.testName, te.testClass, - typeid(t).name, t.file, t.line, t.msg); + else + writefln("There were %d errors:", errors.length); + + foreach (i, entry; errors) + { + Throwable throwable = entry.throwable; + + writefln("%d) %s(%s)%s@%s(%d): %s", i + 1, + entry.testName, entry.testClass, typeid(throwable).name, + throwable.file, throwable.line, throwable.toString); } } - /* Failures - */ - if (failedCount != 0) { - if (failedCount == 1) { + + // report failures + if (!failures.empty) + { + if (failures.length == 1) writeln("There was 1 failure:"); - } else { - writefln("There were %d failures:", failedCount); - } - int i = 0; - foreach (TestError te; errors) { - //Failures are only AssertError exceptions. - if (!te.isAssertError) { - continue; - } - Throwable t = te.error; - writefln("%d) %s(%s)%s@%s(%d): %s", ++i, - te.testName, te.testClass, - typeid(t).name, t.file, t.line, t.msg); + else + writefln("There were %d failures:", failures.length); + + foreach (i, entry; failures) + { + Throwable throwable = entry.throwable; + + writefln("%d) %s(%s)%s@%s(%d): %s", i + 1, + entry.testName, entry.testClass, typeid(throwable).name, + throwable.file, throwable.line, throwable.msg); } } writeln(); printFailures(); - writefln("Tests run: %d, Failures: %d, Errors: %d", testsRun, failedCount, errorCount); - return (errorCount > 0) ? 2 : (failedCount > 0) ? 1 : 0; + writefln("Tests run: %d, Failures: %d, Errors: %d", count, failures.length, errors.length); + return (errors.length > 0) ? 2 : (failures.length > 0) ? 1 : 0; } -version (Posix) { +version (Posix) +{ private static bool _useColor = false; + private static bool _useColorWasComputed = false; - private static bool canUseColor() { - if (!_useColorWasComputed) { - //Disable colors if the results output is written - //to a file or pipe instead of a tty: + + private static bool canUseColor() + { + if (!_useColorWasComputed) + { + // disable colors if the results output is written to a file or pipe instead of a tty import core.sys.posix.unistd; + _useColor = (isatty(stdout.fileno()) != 0); _useColorWasComputed = true; } return _useColor; } - private static void startColorGreen() { - if (canUseColor()) { - write("\x1B[1;37;42m"); stdout.flush(); + private static void startColorGreen() + { + if (canUseColor()) + { + write("\x1B[1;37;42m"); + stdout.flush(); } } - private static void startColorRed() { - if (canUseColor()) { - write("\x1B[1;37;41m"); stdout.flush(); + + private static void startColorRed() + { + if (canUseColor()) + { + write("\x1B[1;37;41m"); + stdout.flush(); } } - private static void endColors() { - if (canUseColor()) { - write("\x1B[0;;m"); stdout.flush(); + + private static void endColors() + { + if (canUseColor()) + { + write("\x1B[0;;m"); + stdout.flush(); } } -} else { - private static void startColorGreen() { +} +else +{ + private static void startColorGreen() + { } - private static void startColorRed() { + + private static void startColorRed() + { } - private static void endColors() { + + private static void endColors() + { } } -private static void printDot() { +private static void printDot() +{ startColorGreen(); - write("."); stdout.flush(); + write("."); + stdout.flush(); endColors(); } -private static void printF() { + +private static void printF() +{ startColorRed(); - write("F"); stdout.flush(); + write("F"); + stdout.flush(); endColors(); } -private static void printOk() { +private static void printOk() +{ startColorGreen(); write("OK"); endColors(); } -private static void printFailures() { + +private static void printFailures() +{ startColorRed(); write("FAILURES!!!"); endColors(); @@ -304,116 +381,173 @@ private static void printFailures() { /** * Runs all the unit tests, showing the test tree as the tests run. */ -public static int runTests_Tree(string[][string] testNamesByClass) { - // FIXME runTests_Progress reports an error for any Throwable that is not an AssertError - // FIXME runTests_Tree reports an error for any Throwable thrown by the test fixture - int failedCount = 0; +public static int runTests_Tree(string[][string] testNamesByClass) +{ + int failureCount = 0; int errorCount = 0; - //List Test classes: writeln("Unit tests: "); - foreach (string className; testNamesByClass.keys) { + foreach (string className; testNamesByClass.byKey) + { writeln(" " ~ className); - //Create the class: + // create test object Object testObject = null; - try { + + try + { testObject = testCreators[className](); - } catch (Throwable t) { - writefln(" ERROR IN CONSTRUCTOR: " ~ className ~ ".this(): " - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + } + catch (AssertException exception) + { + writefln(" FAILURE: this(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + } + catch (Throwable throwable) + { + writefln(" ERROR: this(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); ++errorCount; } - if (testObject is null) { + if (testObject is null) continue; - } - //setUpClass - try { + // setUpClass + try + { testCallers[className](testObject, "setUpClass"); - } catch (Throwable t) { - writefln(" ERROR IN setUpClass: " ~ className ~ ".setUpClass(): " - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + } + catch (AssertException exception) + { + writefln(" FAILURE: setUpClass(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + continue; + } + catch (Throwable throwable) + { + writefln(" ERROR: setUpClass(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); ++errorCount; + continue; } - //Run each test of the class: - foreach (string testName; testNamesByClass[className]) { - //setUp - bool setUpOk = false; - try { + // Run each test of the class: + foreach (string testName; testNamesByClass[className]) + { + // setUp + try + { testCallers[className](testObject, "setUp"); - setUpOk = true; - } catch (Throwable t) { - writefln(" ERROR: setUp" - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); - ++errorCount; } - if (!setUpOk) { + catch (AssertException exception) + { + writefln(" FAILURE: setUp(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + continue; + } + catch (Throwable throwable) + { + writefln(" ERROR: setUp(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + ++errorCount; continue; } - //test - try { + // test + try + { TickDuration startTime = TickDuration.currSystemTick(); testCallers[className](testObject, testName); double elapsedMs = (TickDuration.currSystemTick() - startTime).usecs() / 1000.0; writefln(" OK: %6.2f ms %s()", elapsedMs, testName); - } catch (Throwable t) { - writefln(" FAILED: " ~ testName - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); - ++failedCount; + } + catch (AssertException exception) + { + writefln(" FAILURE: " ~ testName ~ "(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + } + catch (Throwable throwable) + { + writefln(" ERROR: " ~ testName ~ "(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + ++errorCount; } - //tearDown (call anyways if test failed) - try { + // tearDown (call anyways if test failed) + try + { testCallers[className](testObject, "tearDown"); - } catch (Throwable t) { - writefln(" ERROR: tearDown" - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + } + catch (AssertException exception) + { + writefln(" FAILURE: tearDown(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + } + catch (Throwable throwable) + { + writefln(" ERROR: tearDown(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); ++errorCount; } } - //tearDownClass - try { + // tearDownClass + try + { testCallers[className](testObject, "tearDownClass"); - } catch (Throwable t) { - writefln(" ERROR IN tearDownClass: " ~ className ~ ".tearDownClass(): " - ~ "(): %s@%s(%d): %s", typeid(t).name, t.file, t.line, t.msg); + } + catch (AssertException exception) + { + writefln(" FAILURE: tearDownClass(): %s@%s(%d): %s", + typeid(exception).name, exception.file, exception.line, exception.msg); + ++failureCount; + } + catch (Throwable throwable) + { + writefln(" ERROR: tearDownClass(): %s@%s(%d): %s", + typeid(throwable).name, throwable.file, throwable.line, throwable.toString); ++errorCount; } } - return (errorCount > 0) ? 2 : (failedCount > 0) ? 1 : 0; + return (errorCount > 0) ? 2 : (failureCount > 0) ? 1 : 0; } /** * Registers a class as a unit test. */ -mixin template TestMixin() { - public static this() { - //Names of test methods: +mixin template TestMixin() +{ + + public static this() + { + // Names of test methods: immutable(string[]) _testMethods = _testMethodsList!( typeof(this), __traits(allMembers, typeof(this)) ).ret; - //Factory method: - static Object createFunction() { + // Factory method: + static Object createFunction() + { mixin("return (new " ~ typeof(this).stringof ~ "());"); } - //Run method: - //Generate a switch statement, that calls the method that matches the testName: - static void runTest(Object o, string testName) { + // Run method: + // Generate a switch statement, that calls the method that matches the testName: + static void runTest(Object o, string testName) + { mixin( - generateRunTest!(typeof(this), - __traits(allMembers, typeof(this))) + generateRunTest!(typeof(this), __traits(allMembers, typeof(this))) ); } - //Register UnitTest class: + // Register UnitTest class: string className = this.classinfo.name; testClasses ~= className; testNamesByClass[className] = _testMethods.dup; @@ -421,29 +555,42 @@ mixin template TestMixin() { testCreators[className] = &createFunction; } - private template _testMethodsList(T, args...) { - static if (args.length == 0) { + private template _testMethodsList(T, args...) + { + static if (args.length == 0) + { immutable(string[]) ret = []; - } else { - - //Skip strings that don't start with "test": + } + else + { + // Skip strings that don't start with "test": static if (!args[0].startsWith("test") - || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) + || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { - static if(args.length == 1) { + static if(args.length == 1) + { immutable(string[]) ret = []; - } else { + } + else + { immutable(string[]) ret = _testMethodsList!(T, args[1..$]).ret; } - } else { - - //Return the first argument and the rest: - static if (args.length == 1) { + } + else + { + // Return the first argument and the rest: + static if (args.length == 1) + { immutable(string[]) ret = [args[0]]; - } else { - static if (args.length > 1) { + } + else + { + static if (args.length > 1) + { immutable(string[]) ret = [args[0]] ~ _testMethodsList!(T, args[1..$]).ret; - } else { + } + else + { immutable(string[]) ret = []; } } @@ -454,7 +601,8 @@ mixin template TestMixin() { /** * Generates the function that runs a method from its name. */ - private template generateRunTest(T, args...) { + private template generateRunTest(T, args...) + { immutable(string) generateRunTest = T.stringof ~ " testObject = cast("~T.stringof~")o; " ~"switch (testName) { " @@ -466,26 +614,38 @@ mixin template TestMixin() { /** * Generates the case statements. */ - private template generateRunTestImpl(T, args...) { - static if (args.length == 0) { + private template generateRunTestImpl(T, args...) + { + static if (args.length == 0) + { immutable(string) ret = ""; - } else { + } + else + { static if (!(args[0].startsWith("test") || args[0] == "setUp" || args[0] == "tearDown" || args[0] == "setUpClass" || args[0] == "tearDownClass") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { - static if (args.length == 1) { + static if (args.length == 1) + { immutable(string) ret = ""; - } else { + } + else + { immutable(string) ret = generateRunTestImpl!(T, args[1..$]).ret; } - } else { - //Create the case statement that calls that test: - static if (args.length == 1) { + } + else + { + // Create the case statement that calls that test: + static if (args.length == 1) + { immutable(string) ret = "case \"" ~ args[0] ~ "\": testObject." ~ args[0] ~ "(); break; "; - } else { + } + else + { immutable(string) ret = "case \"" ~ args[0] ~ "\": testObject." ~ args[0] ~ "(); break; " ~ generateRunTestImpl!(T, args[1..$]).ret; diff --git a/exampleTests.d b/exampleTests.d index 9b44356..29d36ce 100755 --- a/exampleTests.d +++ b/exampleTests.d @@ -23,12 +23,13 @@ import core.thread; import std.stdio; import std.string; - -//Minimal example: -class ATestClass { +// Minimal example: +class ATestClass +{ mixin TestMixin; - void testExample() { + void testExample() + { assertEquals("bla", "b"~"la"); } } @@ -37,59 +38,72 @@ class ATestClass { /** * Look!! no test base class needed!! */ -class AbcTest { - //This declaration here is the only thing needed to mark a class - //as a unit test class: +class AbcTest +{ + + // This declaration here is the only thing needed to mark a class + // as a unit test class: mixin TestMixin; - //Variable members that start with 'test' are allowed. + // Variable members that start with 'test' are allowed. public int testN = 3; public int testM = 4; - //Any method whose name starts with 'test' is run as a unit test: - //(NOTE: this is bound at compile time, there is no overhead). - public void test1() { + // Any method whose name starts with 'test' is run as a unit test: + // (NOTE: this is bound at compile time, there is no overhead). + public void test1() + { assert(true); } - public void test2() { - //You can use D's assert() function: + public void test2() + { + // You can use D's assert() function: assert(1 == 2 / 2); - //Or dunit convenience asserts (just edit dunit.d to add more): + // Or dunit convenience asserts (just edit dunit.d to add more): assertEquals(1, 2/2); - //The expected and actual values will be shown in the output: + // The expected and actual values will be shown in the output: assertEquals("my string looks dazzling", "my dtring looks sazzling"); } - //Test methods with default arguments work, as long as they can - //be called without arguments, ie: as testDefaultArguments() for instance: - public void testDefaultArguments(int a=4, int b=3) { + // Test methods with default arguments work, as long as they can + // be called without arguments, ie: as testDefaultArguments() for instance: + public void testDefaultArguments(int a=4, int b=3) + { } - //Even if the method is private to the unit test class, it is still run. + // Even if the method is private to the unit test class, it is still run. private void test5(int a=4) { + assert(false); } - //This test was disabled just by adding an underscore to the name: + // This test was disabled just by adding an underscore to the name: public void _testAnother() { assert(false, "fails"); } - //Optional initialization and de-initialization. - // setUp() and tearDown() are called around each individual test. - // setUpClass() and tearDownClass() are called once around the whole unit test. - public void setUp() { + // Optional initialization and de-initialization. + // setUp() and tearDown() are called around each individual test. + // setUpClass() and tearDownClass() are called once around the whole unit test. + public void setUp() + { } + public void tearDown() { } + public void setUpClass() { } + public void tearDownClass() { } + } -class DerivedTest : AbcTest { +class DerivedTest : AbcTest +{ + mixin TestMixin; //Base class tests will be run!!!!!! @@ -102,6 +116,7 @@ class DerivedTest : AbcTest { //test class, and creating a derived TestFixture for each one, //that all it has to do is instantiate the instance under test in the //overriden setUpClass(). + } @@ -109,44 +124,52 @@ class DerivedTest : AbcTest { * You can write asynchronous tests too!! Test those socket listeners of * yours, or your active thread objects, etc.!! */ -class AsynchronousTestExample { +class AsynchronousTestExample +{ + mixin TestMixin; + Thread theThread; bool threadDidItsThing; - //Prepare the test: - void setUp() { + // Prepare the test: + void setUp() + { threadDidItsThing = false; theThread = new Thread(&threadFunction); } - //Cleanup: - void tearDown() { + // Cleanup: + void tearDown() + { theThread.join(); theThread = null; } - void threadFunction() { + void threadFunction() + { threadDidItsThing = true; } - void testThreadActuallyRuns() { + void testThreadActuallyRuns() + { assertEquals(false, threadDidItsThing); - //Start the thread + // Start the thread theThread.start(); - //Assert that within a period of time (500ms by default), the variable - //threadDidItsThing gets toggled: + // Assert that within a period of time (500ms by default), the variable + // threadDidItsThing gets toggled: assertEventually({ return threadDidItsThing; }); } + } version = DUnit; -version(DUnit) { - +version(DUnit) +{ //-All you need to run the tests, is to declare // // mixin DUnitMain. @@ -158,13 +181,15 @@ version(DUnit) { // or // dunit.runTests_Tree(); for a more verbose output // - //from your main function. + // from your main function. mixin DUnitMain; //int main() {return dunit.runTests_Tree();} - -} else { - void main (string[] args) { +} +else +{ + void main (string[] args) + { writeln("production"); } } @@ -176,9 +201,10 @@ version(DUnit) { * application, and will run all the DUnit tests in all modules before the * application starts: */ -unittest { - //runTests(); - runTests_Tree(); +unittest +{ + // runTests(); + // runTests_Tree(); } /* From ddffd6bb7bcd69a981f8992c82597306f888d2fa Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 17 Mar 2013 22:34:55 +0100 Subject: [PATCH 11/73] reworked examples --- README.md | 45 +++++++-- dunit/assertion.d | 5 +- dunit/framework.d | 14 +-- example.d | 186 ++++++++++++++++++++++++++++++++++ exampleTests.d | 253 ---------------------------------------------- 5 files changed, 233 insertions(+), 270 deletions(-) create mode 100755 example.d delete mode 100755 exampleTests.d diff --git a/README.md b/README.md index cbdd1ee..add470d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,41 @@ -Unit testing framework ('dunit') -================================ +xUnit Testing Framework for the D Programming Language +====================================================== -Allows to define unittests simply as methods which names start with 'test'. +D Programming Language: http://dlang.org +D(2) -The only thing necessary to create a unit test class, is to -declare the mixin TestMixin inside the class. This will register -the class and its test methods for the test runner. \ No newline at end of file +xUnit Test Patterns: http://xunitpatterns.com + +1. functions vs. interactions + +unittest functions: collection of one-liners documenting a function +Python: doctest + +xUnit tests: fixture setup, exercise SUT, result verification, fixture teardown +Python: unittest (PyUnit) + +2. reporting + + assert(answer == 42); +stops at first failed assert (?) + + assertEquals(42, answer); +names of all failed test methods (as helpful as the naming of the test methods) + +example run + ./example.d + ./example.d --verbose + ./example.d --list +unittest functions testing the assertions + dmd -unittest example.d dunit/assertion.d dunit/framework.d + +selective test execution + ./example.d --filter testEqualsFailure + + assertEquals(to!string(expected), to!string(actual)) + +forked from jmcabo; fixed issues; restructured + +not D(1)Unit - allows to call (passed) test methods during setup + +TODO: Hamcrest Matchers and assertThat diff --git a/dunit/assertion.d b/dunit/assertion.d index adc7625..e0a7d4b 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -1,5 +1,5 @@ /** - * Unit testing framework ('dunit') + * xUnit Testing Framework for the D Programming Language - assertions */ // Copyright Juan Manuel Cabo 2012. @@ -10,11 +10,12 @@ module dunit.assertion; -import core.exception; import core.thread; +import core.time; import std.algorithm; import std.array; import std.conv; + version (unittest) import std.exception; /** diff --git a/dunit/framework.d b/dunit/framework.d index 540e3fa..3862870 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -1,10 +1,5 @@ /** - * Unit testing framework ('dunit') - * - * Allows to define unittests simply as methods which names start with "test". - * The only thing necessary to create a unit test class, is to declare - * the mixin TestMixin inside the class. This will register the class - * and its test methods for the test runner. + * xUnit Testing Framework for the D Programming Language - framework */ // Copyright Juan Manuel Cabo 2012. @@ -16,13 +11,14 @@ module dunit.framework; public import dunit.assertion; + +import core.time; import std.algorithm; import std.array; import std.conv; import std.getopt; import std.regex; import std.stdio; -import core.time; string[] testClasses; string[][string] testNamesByClass; @@ -564,7 +560,7 @@ mixin template TestMixin() else { // Skip strings that don't start with "test": - static if (!args[0].startsWith("test") + static if (args[0].length < 4 || args[0][0 .. 4] != "test" || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) { static if(args.length == 1) @@ -622,7 +618,7 @@ mixin template TestMixin() } else { - static if (!(args[0].startsWith("test") + static if (!(args[0].length >= 4 && args[0][0 .. 4] == "test" || args[0] == "setUp" || args[0] == "tearDown" || args[0] == "setUpClass" || args[0] == "tearDownClass") || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) diff --git a/example.d b/example.d new file mode 100755 index 0000000..77a759c --- /dev/null +++ b/example.d @@ -0,0 +1,186 @@ +#!/usr/bin/env rdmd + +/** + * xUnit Testing Framework for the D Programming Language - examples + */ + +// Copyright Juan Manuel Cabo 2012. +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +module example; + +import dunit.framework; +import core.thread; +import core.time; +import std.stdio; + +/** + * This example demonstrates the reporting of test failures. + */ +class Test +{ + + mixin TestMixin; + + public void testEqualsFailure() + { + int expected = 42; + int actual = 4 * 6; + + assertEquals(expected, actual); + } + + public void testArrayEqualsFailure() + { + int[] expected = [0, 1, 1, 2, 3]; + int[] actual = [0, 1, 2, 3]; + + assertArrayEquals(expected, actual); + } +} + +/** + * This example demonstrates the order in which the fixture functions run. + * The functions 'setUp' and 'tearDown' run before and after each test. + * The functions 'setUpClass' and 'tearDownClass' run once before and after + * all tests in the class. + */ +class TestFixture +{ + + mixin TestMixin; + + public this() + { + writeln("this()"); + } + + public void setUpClass() + { + writeln("setUpClass()"); + } + + public void tearDownClass() + { + writeln("tearDownClass()"); + } + + public void setUp() + { + writeln("setUp()"); + } + + public void tearDown() + { + writeln("tearDown()"); + } + + public void test1() + { + writeln("test1()"); + } + + public void test2() + { + writeln("test2()"); + } + +} + +/** + * This example demonstrates how to reuse tests and a test fixture. + */ +class TestReuse : TestFixture +{ + + mixin TestMixin; + + public override void setUp() + { + writeln("different setUp()"); + } + +} + +/** + * This example demonstrates various things to know about the test framework. + */ +class TestingThisAndThat +{ + + mixin TestMixin; + + // field name may start with "test" + private int test; + + // test function can have default arguments + public void testResult(bool actual = true) + { + assertTrue(actual); + } + + // test function can even be private + private void testSuccess() + { + testResult(true); + } + + // disabled test function: name does not start with "test" + public void ignore_testFailure() + { + testResult(false); + } + + // failed contracts are errors, not failures + public void testError() + { + assert(false); + } + +} + +/** + * This example demonstrates how to test asynchronous code. + */ +class TestingAsynchronousCode +{ + + mixin TestMixin; + + private Thread thread; + + private bool done; + + public void setUp() + { + done = false; + thread = new Thread(&threadFunction); + } + + public void tearDown() + { + thread.join(); + } + + private void threadFunction() + { + Thread.sleep(dur!"msecs"(100)); + done = true; + } + + public void test() + { + assertFalse(done); + + thread.start(); + + assertEventually({ return done; }); + } + +} + +// either use the 'DUnitMain' mixin or call 'dunit_main(args)' +mixin DUnitMain; diff --git a/exampleTests.d b/exampleTests.d deleted file mode 100755 index 29d36ce..0000000 --- a/exampleTests.d +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env rdmd - -/** - * Unit testing framework ('dunit') - * - * Allows to define unittests simply as methods which names start - * with 'test'. - * The only thing necessary to create a unit test class, is to - * declare the mixin TestMixin inside the class. This will register - * the class and its test methods for the test runner. - */ - -// Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2013. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file LICENSE_1_0.txt or copy at -// http://www.boost.org/LICENSE_1_0.txt) - -module ExampleTests; - -import dunit.framework; -import core.thread; -import std.stdio; -import std.string; - -// Minimal example: -class ATestClass -{ - mixin TestMixin; - - void testExample() - { - assertEquals("bla", "b"~"la"); - } -} - - -/** - * Look!! no test base class needed!! - */ -class AbcTest -{ - - // This declaration here is the only thing needed to mark a class - // as a unit test class: - mixin TestMixin; - - // Variable members that start with 'test' are allowed. - public int testN = 3; - public int testM = 4; - - // Any method whose name starts with 'test' is run as a unit test: - // (NOTE: this is bound at compile time, there is no overhead). - public void test1() - { - assert(true); - } - - public void test2() - { - // You can use D's assert() function: - assert(1 == 2 / 2); - // Or dunit convenience asserts (just edit dunit.d to add more): - assertEquals(1, 2/2); - // The expected and actual values will be shown in the output: - assertEquals("my string looks dazzling", "my dtring looks sazzling"); - } - - // Test methods with default arguments work, as long as they can - // be called without arguments, ie: as testDefaultArguments() for instance: - public void testDefaultArguments(int a=4, int b=3) - { - } - - // Even if the method is private to the unit test class, it is still run. - private void test5(int a=4) { - assert(false); - } - - // This test was disabled just by adding an underscore to the name: - public void _testAnother() { - assert(false, "fails"); - } - - // Optional initialization and de-initialization. - // setUp() and tearDown() are called around each individual test. - // setUpClass() and tearDownClass() are called once around the whole unit test. - public void setUp() - { - } - - public void tearDown() { - } - - public void setUpClass() { - } - - public void tearDownClass() { - } - -} - - -class DerivedTest : AbcTest -{ - - mixin TestMixin; - - //Base class tests will be run!!!!!! - // - //You can for instance override setUpClass() and change the target - //implementation of a family of classes that you are testing. - // - //For instance: Run a set of tests against all derived classes of - //the Stream class. You do this by keeping all the tests in a parent - //test class, and creating a derived TestFixture for each one, - //that all it has to do is instantiate the instance under test in the - //overriden setUpClass(). - -} - - -/** - * You can write asynchronous tests too!! Test those socket listeners of - * yours, or your active thread objects, etc.!! - */ -class AsynchronousTestExample -{ - - mixin TestMixin; - - Thread theThread; - bool threadDidItsThing; - - // Prepare the test: - void setUp() - { - threadDidItsThing = false; - theThread = new Thread(&threadFunction); - } - - // Cleanup: - void tearDown() - { - theThread.join(); - theThread = null; - } - - void threadFunction() - { - threadDidItsThing = true; - } - - void testThreadActuallyRuns() - { - assertEquals(false, threadDidItsThing); - - // Start the thread - theThread.start(); - - // Assert that within a period of time (500ms by default), the variable - // threadDidItsThing gets toggled: - assertEventually({ return threadDidItsThing; }); - } - -} - - -version = DUnit; - -version(DUnit) -{ - //-All you need to run the tests, is to declare - // - // mixin DUnitMain. - // - //-You can alternatively call - // - // dunit.runTests_Progress(); for java style results - // output (SHOWS COLORS IF IN UNIX !!!) - // or - // dunit.runTests_Tree(); for a more verbose output - // - // from your main function. - - mixin DUnitMain; - //int main() {return dunit.runTests_Tree();} -} -else -{ - void main (string[] args) - { - writeln("production"); - } -} - - -/* - * Alternatively, you can run your DUnit tests when passing -unittest - * to the compiler. This only needs to be declared once for the whole - * application, and will run all the DUnit tests in all modules before the - * application starts: - */ -unittest -{ - // runTests(); - // runTests_Tree(); -} - -/* - -Run this file with (works in Windows/Linux): - - - dmd exampleTests.d dunit.d - ./exampleTests - - -The output will be (java style): - - - ...F....F... - There were 2 failures: - 1) test2(AbcTest)core.exception.AssertError@exampleTests.d(61): Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - 2) test2(DerivedTest)core.exception.AssertError@exampleTests.d(61): Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - - FAILURES!!! - Tests run: 10, Failures: 2, Errors: 0 - - -If you use the more verbose method dunit.runTests_Tree(), then the output is: - - - Unit tests: - ATestClass - OK: 0.01 ms testExample() - AbcTest - OK: 0.00 ms test1() - FAILED: test2(): core.exception.AssertError@exampleTests.d(62): Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - OK: 0.00 ms testDefaultArguments() - OK: 0.00 ms test5() - DerivedTest - OK: 0.01 ms test1() - FAILED: test2(): core.exception.AssertError@exampleTests.d(62): Expected: 'my string looks dazzling', but was: 'my dtring looks sazzling' - OK: 0.00 ms testDefaultArguments() - OK: 0.00 ms test5() - AsynchronousTestExample - OK: 11.00 ms testThreadActuallyRuns() - - -HAVE FUN! - -*/ From 28510d911036d14509ed04858c3730a934baa744 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 17 Mar 2013 22:54:30 +0100 Subject: [PATCH 12/73] reworked README --- README.md | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index add470d..ebdb888 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,56 @@ xUnit Testing Framework for the D Programming Language ====================================================== -D Programming Language: http://dlang.org +[D Programming Language](http://dlang.org) D(2) -xUnit Test Patterns: http://xunitpatterns.com +[xUnit Test Patterns](http://xunitpatterns.com) -1. functions vs. interactions +Testing Functions vs. Interactions +---------------------------------- -unittest functions: collection of one-liners documenting a function -Python: doctest +unittest functions: collection of one-liners documenting a function +Python: `doctest` -xUnit tests: fixture setup, exercise SUT, result verification, fixture teardown -Python: unittest (PyUnit) +xUnit tests: fixture setup, exercise SUT, result verification, fixture teardown +Python: `unittest` (PyUnit) -2. reporting +Reporting Failures and Errors +----------------------------- + +contracts assert(answer == 42); + stops at first failed assert (?) assertEquals(42, answer); + names of all failed test methods (as helpful as the naming of the test methods) +Examples +-------- + example run + ./example.d ./example.d --verbose - ./example.d --list + unittest functions testing the assertions + dmd -unittest example.d dunit/assertion.d dunit/framework.d selective test execution + + ./example.d --list ./example.d --filter testEqualsFailure +comparing representations + assertEquals(to!string(expected), to!string(actual)) -forked from jmcabo; fixed issues; restructured +forked from [jmcabo/dunit](https://github.com/jmcabo/dunit); fixed issues; restructured -not D(1)Unit - allows to call (passed) test methods during setup +not [D(1)Unit](http://www.dsource.org/projects/dmocks/wiki/DUnit) - allows to call (passed) test methods during setup -TODO: Hamcrest Matchers and assertThat +TODO: Hamcrest Matchers and `assertThat` From 3dc846a2e87435a5bf78f8e55652cf7d5400920e Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 10 Apr 2013 22:03:03 +0200 Subject: [PATCH 13/73] changed filtering so that permutations of tests can be run --- README.md | 1 + dunit/framework.d | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ebdb888..c0f9230 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ selective test execution ./example.d --list ./example.d --filter testEqualsFailure + ./example.d --filter testSuccess --filter testSuccess comparing representations diff --git a/dunit/framework.d b/dunit/framework.d index 3862870..49bea87 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -46,19 +46,16 @@ public int dunit_main(string[] args) if (filters is null) filters = [null]; - foreach (className; testNamesByClass.byKey) + foreach (filter; filters) { - foreach (testName; testNamesByClass[className]) + foreach (className; testNamesByClass.byKey) { - string fullyQualifiedName = className ~ '.' ~ testName; - - foreach (filter; filters) + foreach (testName; testNamesByClass[className]) { + string fullyQualifiedName = className ~ '.' ~ testName; + if (match(fullyQualifiedName, filter)) - { selectedTestNamesByClass[className] ~= testName; - break; - } } } } From 6c7043acad6f5c9d0487988a8bb60f6ae4870214 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 12 May 2013 18:05:08 +0200 Subject: [PATCH 14/73] fixes issue #1 --- dunit/assertion.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dunit/assertion.d b/dunit/assertion.d index e0a7d4b..414251e 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -118,7 +118,7 @@ void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, string msg = null, { string header = (msg.empty) ? null : msg ~ "; "; - for (size_t index = 0; index < min(expecteds.length, actuals.length); ++index) + foreach (index; 0 .. min(expecteds.length, actuals.length)) { assertEquals(expecteds[index], actuals[index], header ~ "array mismatch at index " ~ to!string(index), From 78651bf031f619dd3b2d3315fbacfa0428e44359 Mon Sep 17 00:00:00 2001 From: linkrope Date: Mon, 20 May 2013 22:14:58 +0200 Subject: [PATCH 15/73] introduced user-defined attributes --- README.md | 38 ++-- dunit/assertion.d | 45 ++--- dunit/attributes.d | 8 + dunit/framework.d | 450 +++++++++++++++++++++------------------------ example.d | 50 +++-- 5 files changed, 290 insertions(+), 301 deletions(-) create mode 100644 dunit/attributes.d diff --git a/README.md b/README.md index c0f9230..fe9d70c 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,43 @@ xUnit Testing Framework for the D Programming Language ====================================================== -[D Programming Language](http://dlang.org) -D(2) - -[xUnit Test Patterns](http://xunitpatterns.com) +This is a simple implementation of the xUnit Testing Framework +for the [D Programming Language](http://dlang.org). +It enables the use of the [xUnit Test Patterns](http://xunitpatterns.com). Testing Functions vs. Interactions ---------------------------------- -unittest functions: collection of one-liners documenting a function -Python: `doctest` +The built-in support for unit tests in D is best suited for testing functions, +when the individual test cases can be expressed as one-liners. -xUnit tests: fixture setup, exercise SUT, result verification, fixture teardown -Python: `unittest` (PyUnit) +For testing interactions of objects, however, more support is required, +for example, for setting up a test fixture. +This is the responsibility of a testing framework. Reporting Failures and Errors ----------------------------- -contracts - - assert(answer == 42); +With no additional effort, specialized assertion functions report failures +more helpful than violated contracts. -stops at first failed assert (?) +For example, assertEquals(42, answer); +will report something like + + expected: <42> but was: <24> + names of all failed test methods (as helpful as the naming of the test methods) +User Defined Attributes +----------------------- + +@Test, @Before, @After, @BeforeClass, @AfterClass, and @Ignore + +instead of naming convention testLikeThis + Examples -------- @@ -54,4 +64,6 @@ forked from [jmcabo/dunit](https://github.com/jmcabo/dunit); fixed issues; restr not [D(1)Unit](http://www.dsource.org/projects/dmocks/wiki/DUnit) - allows to call (passed) test methods during setup -TODO: Hamcrest Matchers and `assertThat` +won't fix [Issue 4653 - More unit test functions should be added](http://d.puremagic.com/issues/show_bug.cgi?id=4653) + +TODO: [Hamcrest](http://code.google.com/p/hamcrest/) Matchers and `assertThat` diff --git a/dunit/assertion.d b/dunit/assertion.d index 414251e..c04b624 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -1,7 +1,3 @@ -/** - * xUnit Testing Framework for the D Programming Language - assertions - */ - // Copyright Juan Manuel Cabo 2012. // Copyright Mario Kröplin 2013. // Distributed under the Boost Software License, Version 1.0. @@ -35,7 +31,7 @@ class AssertException : Exception * Asserts that a condition is true. * Throws: AssertException otherwise */ -void assertTrue(bool condition, string msg = null, +void assertTrue(bool condition, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -49,7 +45,7 @@ void assertTrue(bool condition, string msg = null, * Asserts that a condition is false. * Throws: AssertException otherwise */ -void assertFalse(bool condition, string msg = null, +void assertFalse(bool condition, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -74,7 +70,7 @@ unittest * Asserts that the values are equal. * Throws: AssertException otherwise */ -void assertEquals(T, U)(T expected, U actual, string msg = null, +void assertEquals(T, U)(T expected, U actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -112,7 +108,7 @@ unittest * Asserts that the arrays are equal. * Throws: AssertException otherwise */ -void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, string msg = null, +void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -147,7 +143,7 @@ unittest * Asserts that the value is null. * Throws: AssertException otherwise */ -void assertNull(T)(T actual, string msg = null, +void assertNull(T)(T actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -161,7 +157,7 @@ void assertNull(T)(T actual, string msg = null, * Asserts that the value is not null. * Throws: AssertException otherwise */ -void assertNotNull(T)(T actual, string msg = null, +void assertNotNull(T)(T actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -174,7 +170,7 @@ void assertNotNull(T)(T actual, string msg = null, unittest { Object foo = new Object(); - + assertNull(null); assertEquals("Assertion failure", collectExceptionMsg!AssertException(assertNull(foo))); @@ -188,7 +184,7 @@ unittest * Asserts that the values are the same. * Throws: AssertException otherwise */ -void assertSame(T, U)(T expected, U actual, string msg = null, +void assertSame(T, U)(T expected, U actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -205,7 +201,7 @@ void assertSame(T, U)(T expected, U actual, string msg = null, * Asserts that the values are not the same. * Throws: AssertException otherwise */ -void assertNotSame(T, U)(T expected, U actual, string msg = null, +void assertNotSame(T, U)(T expected, U actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -251,7 +247,7 @@ unittest /** * Checks a probe until the timeout expires. The assert error is produced - * if the probe fails to return 'true' before the timeout. + * if the probe fails to return 'true' before the timeout. * * The parameter timeout determines the maximum timeout to wait before * asserting a failure (default is 500ms). @@ -264,23 +260,20 @@ unittest * * Throws: AssertException when the probe fails to become true before timeout */ -public static void assertEventually(bool delegate() probe, - Duration timeout = dur!"msecs"(500), Duration delay = dur!"msecs"(10), - string msg = null, - string file = __FILE__, +public static void assertEventually(bool delegate() probe, + Duration timeout = dur!"msecs"(500), Duration delay = dur!"msecs"(10), + lazy string msg = null, + string file = __FILE__, size_t line = __LINE__) { TickDuration startTime = TickDuration.currSystemTick(); - - while (!probe()) { + + while (!probe()) + { Duration elapsedTime = cast(Duration)(TickDuration.currSystemTick() - startTime); - if (elapsedTime >= timeout) { - if (msg.empty) { - msg = "timed out"; - } - fail(msg, file, line); - } + if (elapsedTime >= timeout) + fail(msg.empty ? "timed out" : msg, file, line); Thread.sleep(delay); } diff --git a/dunit/attributes.d b/dunit/attributes.d new file mode 100644 index 0000000..b0902b2 --- /dev/null +++ b/dunit/attributes.d @@ -0,0 +1,8 @@ +module dunit.attributes; + +enum After; +enum AfterClass; +enum Before; +enum BeforeClass; +enum Ignore; +enum Test; diff --git a/dunit/framework.d b/dunit/framework.d index 49bea87..6144e51 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -1,7 +1,3 @@ -/** - * xUnit Testing Framework for the D Programming Language - framework - */ - // Copyright Juan Manuel Cabo 2012. // Copyright Mario Kröplin 2013. // Distributed under the Boost Software License, Version 1.0. @@ -11,6 +7,7 @@ module dunit.framework; public import dunit.assertion; +public import dunit.attributes; import core.time; import std.algorithm; @@ -19,13 +16,25 @@ import std.conv; import std.getopt; import std.regex; import std.stdio; +public import std.typetuple; // FIXME + +struct TestClass +{ + string[] tests; + string[] ignoredTests; + + Object function() create; + void function(Object o) beforeClass; + void function(Object o) before; + void function(Object o, string testName) test; + void function(Object o) after; + void function(Object o) afterClass; +} -string[] testClasses; -string[][string] testNamesByClass; -void function(Object o, string testName)[string] testCallers; -Object function()[string] testCreators; +string[] testClassOrder; +TestClass[string] testClasses; -mixin template DUnitMain() +mixin template Main() { int main (string[] args) { @@ -36,10 +45,21 @@ mixin template DUnitMain() public int dunit_main(string[] args) { string[] filters = null; + bool help = false; bool list = false; bool verbose = false; - getopt(args, "filter|f", &filters, "list|l", &list, "verbose|v", &verbose); + getopt(args, + "filter|f", &filters, + "help|h", &help, + "list|l", &list, + "verbose|v", &verbose); + + if (help) + { + // TODO display usage + return 0; + } string[][string] selectedTestNamesByClass = null; @@ -48,9 +68,9 @@ public int dunit_main(string[] args) foreach (filter; filters) { - foreach (className; testNamesByClass.byKey) + foreach (className; testClassOrder) { - foreach (testName; testNamesByClass[className]) + foreach (testName; testClasses[className].tests) { string fullyQualifiedName = className ~ '.' ~ testName; @@ -62,9 +82,9 @@ public int dunit_main(string[] args) if (list) { - foreach (className; selectedTestNamesByClass.byKey) + foreach (className; testClassOrder) { - foreach (testName; selectedTestNamesByClass[className]) + foreach (testName; selectedTestNamesByClass.get(className, null)) { string fullyQualifiedName = className ~ '.' ~ testName; @@ -80,14 +100,6 @@ public int dunit_main(string[] args) return runTests_Progress(selectedTestNamesByClass); } -/** - * Runs all the unit tests. - */ -public static int runTests() -{ - return runTests_Progress(testNamesByClass); -} - /** * Runs all the unit tests, showing progress dots, and the results at the end. */ @@ -111,24 +123,29 @@ public static int runTests_Progress(string[][string] testNamesByClass) Entry[] errors = null; int count = 0; - foreach (string className; testNamesByClass.byKey) + foreach (className; testClassOrder) { + if (className !in testNamesByClass) + continue; + // create test object Object testObject = null; try { - testObject = testCreators[className](); + testObject = testClasses[className].create(); } catch (AssertException exception) { failures ~= Entry(className, "this", exception); - printF(); + write(red("F")); + stdout.flush(); } catch (Throwable throwable) { errors ~= Entry(className, "this", throwable); - printF(); + write(red("E")); + stdout.flush(); } if (testObject is null) @@ -137,22 +154,29 @@ public static int runTests_Progress(string[][string] testNamesByClass) // setUpClass try { - testCallers[className](testObject, "setUpClass"); + testClasses[className].beforeClass(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "setUpClass", exception); + failures ~= Entry(className, "beforeClass", exception); continue; } catch (Throwable throwable) { - errors ~= Entry(className, "setUpClass", throwable); + errors ~= Entry(className, "beforeClass", throwable); continue; } // run each test function of the class - foreach (string testName; testNamesByClass[className]) + foreach (testName; testNamesByClass[className]) { + if (canFind(testClasses[className].ignoredTests, testName)) + { + write(yellow("I")); + stdout.flush(); + continue; + } + ++count; // setUp @@ -160,25 +184,27 @@ public static int runTests_Progress(string[][string] testNamesByClass) try { - testCallers[className](testObject, "setUp"); + testClasses[className].before(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "setUp", exception); - printF(); + failures ~= Entry(className, "before", exception); + write(red("F")); + stdout.flush(); continue; } catch (Throwable throwable) { - errors ~= Entry(className, "setUp", throwable); - printF(); + errors ~= Entry(className, "before", throwable); + write(red("E")); + stdout.flush(); continue; } // test try { - testCallers[className](testObject, testName); + testClasses[className].test(testObject, testName); } catch (AssertException exception) { @@ -194,47 +220,47 @@ public static int runTests_Progress(string[][string] testNamesByClass) // tearDown (even if test failed) try { - testCallers[className](testObject, "tearDown"); + testClasses[className].after(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "tearDown", exception); + failures ~= Entry(className, "after", exception); success = false; } catch (Throwable throwable) { - errors ~= Entry(className, "tearDown", throwable); + errors ~= Entry(className, "after", throwable); success = false; } if (success) - printDot(); + write(green(".")); else - printF(); + write(red("F")); // FIXME or "E"? + stdout.flush(); } // tearDownClass try { - testCallers[className](testObject, "tearDownClass"); + testClasses[className].afterClass(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "tearDownClass", exception); + failures ~= Entry(className, "afterClass", exception); } catch (Throwable throwable) { - errors ~= Entry(className, "tearDownClass", throwable); + errors ~= Entry(className, "afterClass", throwable); } } - + // report results writeln(); if (failures.empty && errors.empty) { writeln(); - printOk(); - writefln(" (%d %s)", count, (count == 1) ? "Test" : "Tests"); + writefln("%s (%d %s)", green("OK"), count, (count == 1) ? "Test" : "Tests"); return 0; } @@ -250,9 +276,8 @@ public static int runTests_Progress(string[][string] testNamesByClass) { Throwable throwable = entry.throwable; - writefln("%d) %s(%s)%s@%s(%d): %s", i + 1, - entry.testName, entry.testClass, typeid(throwable).name, - throwable.file, throwable.line, throwable.toString); + writefln("%d) %s(%s) %s", i + 1, + entry.testName, entry.testClass, throwable.toString); } } @@ -268,109 +293,72 @@ public static int runTests_Progress(string[][string] testNamesByClass) { Throwable throwable = entry.throwable; - writefln("%d) %s(%s)%s@%s(%d): %s", i + 1, + writefln("%d) %s(%s) %s@%s(%d): %s", i + 1, entry.testName, entry.testClass, typeid(throwable).name, throwable.file, throwable.line, throwable.msg); } } writeln(); - printFailures(); - writefln("Tests run: %d, Failures: %d, Errors: %d", count, failures.length, errors.length); + writeln(red("NOT OK")); + // FIXME Ignored + writefln("Tests run: %d, Failures: %d, Errors: %d", count, failures.length, errors.length); return (errors.length > 0) ? 2 : (failures.length > 0) ? 1 : 0; } version (Posix) { - private static bool _useColor = false; - - private static bool _useColorWasComputed = false; + private static string CSI = "\x1B["; - private static bool canUseColor() + private static string red(string source) { - if (!_useColorWasComputed) - { - // disable colors if the results output is written to a file or pipe instead of a tty - import core.sys.posix.unistd; - - _useColor = (isatty(stdout.fileno()) != 0); - _useColorWasComputed = true; - } - return _useColor; + return canUseColor() ? CSI ~ "37;41;1m" ~ source ~ CSI ~ "0m" : source; } - private static void startColorGreen() + private static string green(string source) { - if (canUseColor()) - { - write("\x1B[1;37;42m"); - stdout.flush(); - } + return canUseColor() ? CSI ~ "37;42;1m" ~ source ~ CSI ~ "0m" : source; } - private static void startColorRed() + private static string yellow(string source) { - if (canUseColor()) - { - write("\x1B[1;37;41m"); - stdout.flush(); - } + return canUseColor() ? CSI ~ "37;43;1m" ~ source ~ CSI ~ "0m" : source; } - private static void endColors() + private static bool canUseColor() { - if (canUseColor()) + static bool useColor = false; + static bool computed = false; + + if (!computed) { - write("\x1B[0;;m"); - stdout.flush(); + // disable colors if the results output is written to a file or pipe instead of a tty + import core.sys.posix.unistd; + + useColor = isatty(stdout.fileno()) != 0; + computed = true; } + return useColor; } } else { - private static void startColorGreen() + private static string red(string source) { + return source; } - private static void startColorRed() + private static string green(string source) { + return source; } - private static void endColors() + private static string yellow(string source) { + return source; } } -private static void printDot() -{ - startColorGreen(); - write("."); - stdout.flush(); - endColors(); -} - -private static void printF() -{ - startColorRed(); - write("F"); - stdout.flush(); - endColors(); -} -private static void printOk() -{ - startColorGreen(); - write("OK"); - endColors(); -} - -private static void printFailures() -{ - startColorRed(); - write("FAILURES!!!"); - endColors(); - writeln(); -} - /** * Runs all the unit tests, showing the test tree as the tests run. */ @@ -380,16 +368,19 @@ public static int runTests_Tree(string[][string] testNamesByClass) int errorCount = 0; writeln("Unit tests: "); - foreach (string className; testNamesByClass.byKey) + foreach (className; testClassOrder) { - writeln(" " ~ className); + if (className !in testNamesByClass) + continue; + + writeln(" ", className); // create test object Object testObject = null; try { - testObject = testCreators[className](); + testObject = testClasses[className].create(); } catch (AssertException exception) { @@ -399,8 +390,7 @@ public static int runTests_Tree(string[][string] testNamesByClass) } catch (Throwable throwable) { - writefln(" ERROR: this(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: this(): ", throwable.toString); ++errorCount; } if (testObject is null) @@ -409,42 +399,46 @@ public static int runTests_Tree(string[][string] testNamesByClass) // setUpClass try { - testCallers[className](testObject, "setUpClass"); + testClasses[className].beforeClass(testObject); } catch (AssertException exception) { - writefln(" FAILURE: setUpClass(): %s@%s(%d): %s", + writefln(" FAILURE: beforeClass(): %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; continue; } catch (Throwable throwable) { - writefln(" ERROR: setUpClass(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: beforeClass(): ", throwable.toString); ++errorCount; continue; } // Run each test of the class: - foreach (string testName; testNamesByClass[className]) + foreach (testName; testNamesByClass[className]) { + if (canFind(testClasses[className].ignoredTests, testName)) + { + writeln(" IGNORE: ", testName, "()"); + continue; + } + // setUp try { - testCallers[className](testObject, "setUp"); + testClasses[className].before(testObject); } catch (AssertException exception) { - writefln(" FAILURE: setUp(): %s@%s(%d): %s", + writefln(" FAILURE: before(): %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; continue; } catch (Throwable throwable) { - writefln(" ERROR: setUp(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: before(): ", throwable.toString); ++errorCount; continue; } @@ -453,7 +447,7 @@ public static int runTests_Tree(string[][string] testNamesByClass) try { TickDuration startTime = TickDuration.currSystemTick(); - testCallers[className](testObject, testName); + testClasses[className].test(testObject, testName); double elapsedMs = (TickDuration.currSystemTick() - startTime).usecs() / 1000.0; writefln(" OK: %6.2f ms %s()", elapsedMs, testName); } @@ -465,26 +459,24 @@ public static int runTests_Tree(string[][string] testNamesByClass) } catch (Throwable throwable) { - writefln(" ERROR: " ~ testName ~ "(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: ", testName, "(): ", throwable.toString); ++errorCount; } // tearDown (call anyways if test failed) try { - testCallers[className](testObject, "tearDown"); + testClasses[className].after(testObject); } catch (AssertException exception) { - writefln(" FAILURE: tearDown(): %s@%s(%d): %s", + writefln(" FAILURE: after(): %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; } catch (Throwable throwable) { - writefln(" ERROR: tearDown(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: after(): ", throwable.toString); ++errorCount; } } @@ -492,158 +484,132 @@ public static int runTests_Tree(string[][string] testNamesByClass) // tearDownClass try { - testCallers[className](testObject, "tearDownClass"); + testClasses[className].afterClass(testObject); } catch (AssertException exception) { - writefln(" FAILURE: tearDownClass(): %s@%s(%d): %s", + writefln(" FAILURE: afterClass(): %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; } catch (Throwable throwable) { - writefln(" ERROR: tearDownClass(): %s@%s(%d): %s", - typeid(throwable).name, throwable.file, throwable.line, throwable.toString); + writeln(" ERROR: afterClass(): ", throwable.toString); ++errorCount; } } return (errorCount > 0) ? 2 : (failureCount > 0) ? 1 : 0; } - /** * Registers a class as a unit test. */ -mixin template TestMixin() +mixin template UnitTest() { public static this() { - // Names of test methods: - immutable(string[]) _testMethods = _testMethodsList!( - typeof(this), - __traits(allMembers, typeof(this)) - ).ret; + TestClass testClass; + + testClass.tests = _memberFunctions!(typeof(this), Test, + __traits(allMembers, typeof(this))).result.dup; + testClass.ignoredTests = _memberFunctions!(typeof(this), Ignore, + __traits(allMembers, typeof(this))).result.dup; - // Factory method: - static Object createFunction() - { - mixin("return (new " ~ typeof(this).stringof ~ "());"); + static Object create() + { + mixin("return new " ~ typeof(this).stringof ~ "();"); } - // Run method: - // Generate a switch statement, that calls the method that matches the testName: - static void runTest(Object o, string testName) + static void beforeClass(Object o) { - mixin( - generateRunTest!(typeof(this), __traits(allMembers, typeof(this))) - ); + mixin(_sequence(_memberFunctions!(typeof(this), BeforeClass, + __traits(allMembers, typeof(this))).result)); } - // Register UnitTest class: - string className = this.classinfo.name; - testClasses ~= className; - testNamesByClass[className] = _testMethods.dup; - testCallers[className] = &runTest; - testCreators[className] = &createFunction; - } - - private template _testMethodsList(T, args...) - { - static if (args.length == 0) + static void before(Object o) { - immutable(string[]) ret = []; + mixin(_sequence(_memberFunctions!(typeof(this), Before, + __traits(allMembers, typeof(this))).result)); } - else + + static void test(Object o, string name) { - // Skip strings that don't start with "test": - static if (args[0].length < 4 || args[0][0 .. 4] != "test" - || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) - { - static if(args.length == 1) - { - immutable(string[]) ret = []; - } - else - { - immutable(string[]) ret = _testMethodsList!(T, args[1..$]).ret; - } - } - else - { - // Return the first argument and the rest: - static if (args.length == 1) - { - immutable(string[]) ret = [args[0]]; - } - else - { - static if (args.length > 1) - { - immutable(string[]) ret = [args[0]] ~ _testMethodsList!(T, args[1..$]).ret; - } - else - { - immutable(string[]) ret = []; - } - } - } + mixin(_choice(_memberFunctions!(typeof(this), Test, + __traits(allMembers, typeof(this))).result)); + } + + static void after(Object o) + { + mixin(_sequence(_memberFunctions!(typeof(this), After, + __traits(allMembers, typeof(this))).result)); + } + + static void afterClass(Object o) + { + mixin(_sequence(_memberFunctions!(typeof(this), AfterClass, + __traits(allMembers, typeof(this))).result)); + } + + testClass.create = &create; + testClass.beforeClass = &beforeClass; + testClass.before = &before; + testClass.test = &test; + testClass.after = &after; + testClass.afterClass = &afterClass; + + testClassOrder ~= this.classinfo.name; + testClasses[this.classinfo.name] = testClass; + } + + private static string _choice(const string[] memberFunctions) + { + string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; + + block ~= "switch (name)\n{\n"; + foreach (memberFunction; memberFunctions) + { + block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n"; } + block ~= "default: break;\n}\n"; + return block; } - /** - * Generates the function that runs a method from its name. - */ - private template generateRunTest(T, args...) + private static string _sequence(const string[] memberFunctions) { - immutable(string) generateRunTest = - T.stringof ~ " testObject = cast("~T.stringof~")o; " - ~"switch (testName) { " - ~generateRunTestImpl!(T, args).ret - ~" default: break; " - ~"}"; + string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; + + foreach (memberFunction; memberFunctions) + { + block ~= "testObject." ~ memberFunction ~ "();\n"; + } + return block; } - /** - * Generates the case statements. - */ - private template generateRunTestImpl(T, args...) + private template _memberFunctions(alias T, alias U, names...) { - static if (args.length == 0) + static if (names.length == 0) { - immutable(string) ret = ""; + immutable(string[]) result = []; } else { - static if (!(args[0].length >= 4 && args[0][0 .. 4] == "test" - || args[0] == "setUp" || args[0] == "tearDown" - || args[0] == "setUpClass" || args[0] == "tearDownClass") - || !(__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ args[0] ~ "()")) )) + static if (_hasAttribute!(T, names[0], U) && __traits(compiles, + mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) { - static if (args.length == 1) - { - immutable(string) ret = ""; - } - else - { - immutable(string) ret = generateRunTestImpl!(T, args[1..$]).ret; - } + immutable(string[]) result = [names[0]] ~ _memberFunctions!(T, U, names[1 .. $]).result; } else { - // Create the case statement that calls that test: - static if (args.length == 1) - { - immutable(string) ret = - "case \"" ~ args[0] ~ "\": testObject." ~ args[0] ~ "(); break; "; - } - else - { - immutable(string) ret = - "case \"" ~ args[0] ~ "\": testObject." ~ args[0] ~ "(); break; " - ~ generateRunTestImpl!(T, args[1..$]).ret; - } + immutable(string[]) result = _memberFunctions!(T, U, names[1 .. $]).result; } } } + + template _hasAttribute(alias T, string name, attribute) + { + enum _hasAttribute = staticIndexOf!(attribute, + __traits(getAttributes, __traits(getMember, T, name))) != -1; + } + } diff --git a/example.d b/example.d index 77a759c..09be51d 100755 --- a/example.d +++ b/example.d @@ -1,9 +1,5 @@ #!/usr/bin/env rdmd -/** - * xUnit Testing Framework for the D Programming Language - examples - */ - // Copyright Juan Manuel Cabo 2012. // Copyright Mario Kröplin 2013. // Distributed under the Boost Software License, Version 1.0. @@ -23,9 +19,10 @@ import std.stdio; class Test { - mixin TestMixin; + mixin UnitTest; - public void testEqualsFailure() + @Test + public void assertEqualsFailure() { int expected = 42; int actual = 4 * 6; @@ -33,7 +30,8 @@ class Test assertEquals(expected, actual); } - public void testArrayEqualsFailure() + @Test + public void assertArrayEqualsFailure() { int[] expected = [0, 1, 1, 2, 3]; int[] actual = [0, 1, 2, 3]; @@ -51,38 +49,44 @@ class Test class TestFixture { - mixin TestMixin; + mixin UnitTest; public this() { writeln("this()"); } + @BeforeClass public void setUpClass() { writeln("setUpClass()"); } + @AfterClass public void tearDownClass() { writeln("tearDownClass()"); } + @Before public void setUp() { writeln("setUp()"); } + @After public void tearDown() { writeln("tearDown()"); } + @Test public void test1() { writeln("test1()"); } + @Test public void test2() { writeln("test2()"); @@ -96,8 +100,9 @@ class TestFixture class TestReuse : TestFixture { - mixin TestMixin; + mixin UnitTest; + @Before public override void setUp() { writeln("different setUp()"); @@ -111,31 +116,33 @@ class TestReuse : TestFixture class TestingThisAndThat { - mixin TestMixin; - - // field name may start with "test" - private int test; + mixin UnitTest; // test function can have default arguments + @Test public void testResult(bool actual = true) { assertTrue(actual); } // test function can even be private - private void testSuccess() + @Test + private void success() { testResult(true); } - // disabled test function: name does not start with "test" - public void ignore_testFailure() + // disabled test function + @Test + @Ignore + public void failure() { testResult(false); } // failed contracts are errors, not failures - public void testError() + @Test + public void error() { assert(false); } @@ -148,18 +155,20 @@ class TestingThisAndThat class TestingAsynchronousCode { - mixin TestMixin; + mixin UnitTest; private Thread thread; private bool done; + @Before public void setUp() { done = false; thread = new Thread(&threadFunction); } + @After public void tearDown() { thread.join(); @@ -171,6 +180,7 @@ class TestingAsynchronousCode done = true; } + @Test public void test() { assertFalse(done); @@ -182,5 +192,5 @@ class TestingAsynchronousCode } -// either use the 'DUnitMain' mixin or call 'dunit_main(args)' -mixin DUnitMain; +// either use the 'Main' mixin or call 'dunit_main(args)' +mixin Main; From fb3312cac0bc64c423702e4d32cf35304d7104c4 Mon Sep 17 00:00:00 2001 From: linkrope Date: Thu, 23 May 2013 23:10:33 +0200 Subject: [PATCH 16/73] fixed problem with const fields --- README.md | 17 ++++++--- dunit/framework.d | 90 +++++++++++++++++++++++++---------------------- example.d | 14 ++++---- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index fe9d70c..8308ea9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -xUnit Testing Framework for the D Programming Language -====================================================== +xUnit Testing Framework for D +============================= This is a simple implementation of the xUnit Testing Framework for the [D Programming Language](http://dlang.org). -It enables the use of the [xUnit Test Patterns](http://xunitpatterns.com). +It's based on [JUnit](http://junit.org) and it allows to organize tests +according to the [xUnit Test Patterns](http://xunitpatterns.com). + +It's known to work with version D 2.062. Testing Functions vs. Interactions ---------------------------------- @@ -29,12 +32,12 @@ will report something like expected: <42> but was: <24> -names of all failed test methods (as helpful as the naming of the test methods) +names of all failed test methods (as helpful as the names of the test methods are expressive) User Defined Attributes ----------------------- -@Test, @Before, @After, @BeforeClass, @AfterClass, and @Ignore +`@Test`, `@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore` instead of naming convention testLikeThis @@ -56,6 +59,10 @@ selective test execution ./example.d --filter testEqualsFailure ./example.d --filter testSuccess --filter testSuccess +display usage + + ./example.d --help + comparing representations assertEquals(to!string(expected), to!string(actual)) diff --git a/dunit/framework.d b/dunit/framework.d index 6144e51..93cf569 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -14,9 +14,10 @@ import std.algorithm; import std.array; import std.conv; import std.getopt; +import std.path; import std.regex; import std.stdio; -public import std.typetuple; // FIXME +public import std.typetuple; struct TestClass { @@ -57,7 +58,15 @@ public int dunit_main(string[] args) if (help) { - // TODO display usage + writefln("Usage: %s [options]", args.empty ? "testrunner" : baseName(args[0])); + writeln("Run the functions with @Test attribute of all classes that mix in UnitTest."); + writeln(); + writeln("Options:"); + writeln(" -f, --filter REGEX Select test functions matching the regular expression"); + writeln(" Multiple selections are processed in sequence"); + writeln(" -h, --help Display usage information, then exit"); + writeln(" -l, --list Display the test functions, then exit"); + writeln(" -v, --verbose Display more information as the tests are run"); return 0; } @@ -110,13 +119,6 @@ public static int runTests_Progress(string[][string] testNamesByClass) string testClass; string testName; Throwable throwable; - - this(string testClass, string testName, Throwable throwable) - { - this.testClass = testClass; - this.testName = testName; - this.throwable = throwable; - } } Entry[] failures = null; @@ -140,30 +142,33 @@ public static int runTests_Progress(string[][string] testNamesByClass) failures ~= Entry(className, "this", exception); write(red("F")); stdout.flush(); + continue; } catch (Throwable throwable) { errors ~= Entry(className, "this", throwable); write(red("E")); stdout.flush(); - } - - if (testObject is null) continue; + } - // setUpClass + // set up class try { testClasses[className].beforeClass(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "beforeClass", exception); + failures ~= Entry(className, "BeforeClass", exception); + write(red("F")); + stdout.flush(); continue; } catch (Throwable throwable) { - errors ~= Entry(className, "beforeClass", throwable); + errors ~= Entry(className, "BeforeClass", throwable); + write(red("E")); + stdout.flush(); continue; } @@ -179,7 +184,7 @@ public static int runTests_Progress(string[][string] testNamesByClass) ++count; - // setUp + // set up bool success = true; try @@ -188,14 +193,14 @@ public static int runTests_Progress(string[][string] testNamesByClass) } catch (AssertException exception) { - failures ~= Entry(className, "before", exception); + failures ~= Entry(className, "Before", exception); write(red("F")); stdout.flush(); continue; } catch (Throwable throwable) { - errors ~= Entry(className, "before", throwable); + errors ~= Entry(className, "Before", throwable); write(red("E")); stdout.flush(); continue; @@ -209,49 +214,51 @@ public static int runTests_Progress(string[][string] testNamesByClass) catch (AssertException exception) { failures ~= Entry(className, testName, exception); + write(red("F")); success = false; } catch (Throwable throwable) { errors ~= Entry(className, testName, throwable); + write(red("E")); success = false; } - // tearDown (even if test failed) + // tear down (even if test failed) try { testClasses[className].after(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "after", exception); + failures ~= Entry(className, "After", exception); + write(red("F")); success = false; } catch (Throwable throwable) { - errors ~= Entry(className, "after", throwable); + errors ~= Entry(className, "After", throwable); + write(red("E")); success = false; } if (success) write(green(".")); - else - write(red("F")); // FIXME or "E"? stdout.flush(); } - // tearDownClass + // tear down class try { testClasses[className].afterClass(testObject); } catch (AssertException exception) { - failures ~= Entry(className, "afterClass", exception); + failures ~= Entry(className, "AfterClass", exception); } catch (Throwable throwable) { - errors ~= Entry(className, "afterClass", throwable); + errors ~= Entry(className, "AfterClass", throwable); } } @@ -301,7 +308,6 @@ public static int runTests_Progress(string[][string] testNamesByClass) writeln(); writeln(red("NOT OK")); - // FIXME Ignored writefln("Tests run: %d, Failures: %d, Errors: %d", count, failures.length, errors.length); return (errors.length > 0) ? 2 : (failures.length > 0) ? 1 : 0; } @@ -396,26 +402,26 @@ public static int runTests_Tree(string[][string] testNamesByClass) if (testObject is null) continue; - // setUpClass + // set up class try { testClasses[className].beforeClass(testObject); } catch (AssertException exception) { - writefln(" FAILURE: beforeClass(): %s@%s(%d): %s", + writefln(" FAILURE: BeforeClass: %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; continue; } catch (Throwable throwable) { - writeln(" ERROR: beforeClass(): ", throwable.toString); + writeln(" ERROR: BeforeClass: ", throwable.toString); ++errorCount; continue; } - // Run each test of the class: + // run each test of the class foreach (testName; testNamesByClass[className]) { if (canFind(testClasses[className].ignoredTests, testName)) @@ -424,21 +430,21 @@ public static int runTests_Tree(string[][string] testNamesByClass) continue; } - // setUp + // set up try { testClasses[className].before(testObject); } catch (AssertException exception) { - writefln(" FAILURE: before(): %s@%s(%d): %s", + writefln(" FAILURE: Before: %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; continue; } catch (Throwable throwable) { - writeln(" ERROR: before(): ", throwable.toString); + writeln(" ERROR: Before: ", throwable.toString); ++errorCount; continue; } @@ -463,38 +469,38 @@ public static int runTests_Tree(string[][string] testNamesByClass) ++errorCount; } - // tearDown (call anyways if test failed) + // tear down (call anyways if test failed) try { testClasses[className].after(testObject); } catch (AssertException exception) { - writefln(" FAILURE: after(): %s@%s(%d): %s", + writefln(" FAILURE: After: %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; } catch (Throwable throwable) { - writeln(" ERROR: after(): ", throwable.toString); + writeln(" ERROR: After: ", throwable.toString); ++errorCount; } } - // tearDownClass + // tear down class try { testClasses[className].afterClass(testObject); } catch (AssertException exception) { - writefln(" FAILURE: afterClass(): %s@%s(%d): %s", + writefln(" FAILURE: AfterClass: %s@%s(%d): %s", typeid(exception).name, exception.file, exception.line, exception.msg); ++failureCount; } catch (Throwable throwable) { - writeln(" ERROR: afterClass(): ", throwable.toString); + writeln(" ERROR: AfterClass: ", throwable.toString); ++errorCount; } } @@ -594,8 +600,8 @@ mixin template UnitTest() } else { - static if (_hasAttribute!(T, names[0], U) && __traits(compiles, - mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) + static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) + && _hasAttribute!(T, names[0], U)) { immutable(string[]) result = [names[0]] ~ _memberFunctions!(T, U, names[1 .. $]).result; } diff --git a/example.d b/example.d index 09be51d..5e0bce9 100755 --- a/example.d +++ b/example.d @@ -57,27 +57,27 @@ class TestFixture } @BeforeClass - public void setUpClass() + public static void setUpClass() { - writeln("setUpClass()"); + writeln("BeforeClass"); } @AfterClass - public void tearDownClass() + public static void tearDownClass() { - writeln("tearDownClass()"); + writeln("AfterClass"); } @Before public void setUp() { - writeln("setUp()"); + writeln("Before"); } @After public void tearDown() { - writeln("tearDown()"); + writeln("After"); } @Test @@ -105,7 +105,7 @@ class TestReuse : TestFixture @Before public override void setUp() { - writeln("different setUp()"); + writeln("Before override"); } } From 1a54ff5016bb68dc2fe967a627181a12286d7427 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 31 May 2013 19:47:49 +0200 Subject: [PATCH 17/73] fix for issue #3: color support for Windows introduced TestListener to avoid code duplication --- README.md | 26 +-- dunit/assertion.d | 58 ++++-- dunit/color.d | 136 +++++++++++++ dunit/framework.d | 483 ++++++++++++++++++++-------------------------- example.d | 16 +- 5 files changed, 406 insertions(+), 313 deletions(-) create mode 100644 dunit/color.d diff --git a/README.md b/README.md index 8308ea9..4aa17bb 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,23 @@ xUnit Testing Framework for D This is a simple implementation of the xUnit Testing Framework for the [D Programming Language](http://dlang.org). -It's based on [JUnit](http://junit.org) and it allows to organize tests +Being based on [JUnit](http://junit.org) it allows to organize tests according to the [xUnit Test Patterns](http://xunitpatterns.com). -It's known to work with version D 2.062. +It's known to work with versions D 2.062 and D 2.063. Testing Functions vs. Interactions ---------------------------------- -The built-in support for unit tests in D is best suited for testing functions, -when the individual test cases can be expressed as one-liners. +D's built-in support for unit tests is best suited for testing functions, +when the test cases can be expressed as one-liners. For testing interactions of objects, however, more support is required, for example, for setting up a test fixture. This is the responsibility of a testing framework. -Reporting Failures and Errors ------------------------------ +Failures vs. Errors +------------------- With no additional effort, specialized assertion functions report failures more helpful than violated contracts. @@ -44,28 +44,30 @@ instead of naming convention testLikeThis Examples -------- -example run +Run the included example to see the xUnit Testing Framework in action: ./example.d - ./example.d --verbose + +(When you get one error and two failures, everything works fine.) unittest functions testing the assertions - dmd -unittest example.d dunit/assertion.d dunit/framework.d + dmd -debug example.d dunit/assertion.d dunit/attributes.d dunit/framework.d + ./example --verbose selective test execution ./example.d --list ./example.d --filter testEqualsFailure - ./example.d --filter testSuccess --filter testSuccess display usage ./example.d --help -comparing representations +TODO +---- - assertEquals(to!string(expected), to!string(actual)) +more helpful string difference forked from [jmcabo/dunit](https://github.com/jmcabo/dunit); fixed issues; restructured diff --git a/dunit/assertion.d b/dunit/assertion.d index c04b624..9712cc8 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -41,6 +41,14 @@ void assertTrue(bool condition, lazy string msg = null, fail(msg, file, line); } +/// +unittest +{ + assertTrue(true); + assertEquals("Assertion failure", + collectExceptionMsg!AssertException(assertTrue(false))); +} + /** * Asserts that a condition is false. * Throws: AssertException otherwise @@ -55,12 +63,9 @@ void assertFalse(bool condition, lazy string msg = null, fail(msg, file, line); } +/// unittest { - assertTrue(true); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertTrue(false))); - assertFalse(false); assertEquals("Assertion failure", collectExceptionMsg!AssertException(assertFalse(true))); @@ -79,10 +84,11 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, string header = (msg.empty) ? null : msg ~ "; "; - fail(header ~ "expected: <" ~ to!string(expected) ~ "> but was: <"~ to!string(actual) ~ ">", + fail(header ~ "expected: <" ~ expected.to!string ~ "> but was: <"~ actual.to!string ~ ">", file, line); } +/// unittest { assertEquals("foo", "foo"); @@ -90,8 +96,8 @@ unittest collectExceptionMsg!AssertException(assertEquals("foo", "bar"))); assertEquals(42, 42); - assertEquals("expected: <42> but was: <23>", - collectExceptionMsg!AssertException(assertEquals(42, 23))); + assertEquals("expected: <42> but was: <24>", + collectExceptionMsg!AssertException(assertEquals(42, 24))); assertEquals(42.0, 42.0); @@ -117,7 +123,7 @@ void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, lazy string msg = null, foreach (index; 0 .. min(expecteds.length, actuals.length)) { assertEquals(expecteds[index], actuals[index], - header ~ "array mismatch at index " ~ to!string(index), + header ~ "array mismatch at index " ~ index.to!string, file, line); } assertEquals(expecteds.length, actuals.length, @@ -125,6 +131,7 @@ void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, lazy string msg = null, file, line); } +/// unittest { int[] expecteds = [1, 2, 3]; @@ -153,6 +160,16 @@ void assertNull(T)(T actual, lazy string msg = null, fail(msg, file, line); } +/// +unittest +{ + Object foo = new Object(); + + assertNull(null); + assertEquals("Assertion failure", + collectExceptionMsg!AssertException(assertNull(foo))); +} + /** * Asserts that the value is not null. * Throws: AssertException otherwise @@ -167,14 +184,11 @@ void assertNotNull(T)(T actual, lazy string msg = null, fail(msg, file, line); } +/// unittest { Object foo = new Object(); - assertNull(null); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertNull(foo))); - assertNotNull(foo); assertEquals("Assertion failure", collectExceptionMsg!AssertException(assertNotNull(null))); @@ -193,10 +207,21 @@ void assertSame(T, U)(T expected, U actual, lazy string msg = null, string header = (msg.empty) ? null : msg ~ "; "; - fail(header ~ "expected same: <" ~ to!string(expected) ~ "> was not: <"~ to!string(actual) ~ ">", + fail(header ~ "expected same: <" ~ expected.to!string ~ "> was not: <"~ actual.to!string ~ ">", file, line); } +/// +unittest +{ + Object foo = new Object(); + Object bar = new Object(); + + assertSame(foo, foo); + assertEquals("expected same: was not: ", + collectExceptionMsg!AssertException(assertSame(foo, bar))); +} + /** * Asserts that the values are not the same. * Throws: AssertException otherwise @@ -214,15 +239,12 @@ void assertNotSame(T, U)(T expected, U actual, lazy string msg = null, file, line); } +/// unittest { Object foo = new Object(); Object bar = new Object(); - assertSame(foo, foo); - assertEquals("expected same: was not: ", - collectExceptionMsg!AssertException(assertSame(foo, bar))); - assertNotSame(foo, bar); assertEquals("expected not same", collectExceptionMsg!AssertException(assertNotSame(foo, foo))); @@ -239,6 +261,7 @@ void fail(string msg = null, throw new AssertException(msg, file, line); } +/// unittest { assertEquals("Assertion failure", @@ -279,6 +302,7 @@ public static void assertEventually(bool delegate() probe, } } +/// unittest { assertEventually({ static count = 0; return ++count > 42; }); diff --git a/dunit/color.d b/dunit/color.d new file mode 100644 index 0000000..492b26f --- /dev/null +++ b/dunit/color.d @@ -0,0 +1,136 @@ +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +module dunit.color; + +import std.stdio; + +enum Color { red, green, yellow, onRed, onGreen, onYellow } + +version (Posix) +{ + public void writec(Color color, string text) + { + if (canUseColor()) + { + const string CSI = "\x1B["; + + final switch (color) + { + case Color.red: + stdout.write(CSI, "37;31;1m"); + break; + case Color.green: + stdout.write(CSI, "37;32;1m"); + break; + case Color.yellow: + stdout.write(CSI, "37;33;1m"); + break; + case Color.onRed: + stdout.write(CSI, "37;41;1m"); + break; + case Color.onGreen: + stdout.write(CSI, "37;42;1m"); + break; + case Color.onYellow: + stdout.write(CSI, "37;43;1m"); + break; + } + stdout.write(text); + stdout.write(CSI, "0m"); + stdout.flush(); + } + else + { + stdout.write(text); + stdout.flush(); + } + } + + private static bool canUseColor() + { + static bool useColor = false; + static bool computed = false; + + if (!computed) + { + // disable colors if the output is written to a file or pipe instead of a tty + import core.sys.posix.unistd; + + useColor = isatty(stdout.fileno()) != 0; + computed = true; + } + return useColor; + } +} + +version (Windows) +{ + public void writec(Color color, string text) + { + if (canUseColor()) + { + import core.sys.windows.windows; + + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO info; + + GetConsoleScreenBufferInfo(handle, &info); + final switch (color) + { + case Color.red: + SetConsoleTextAttribute(handle, + FOREGROUND_RED | FOREGROUND_INTENSITY); + break; + case Color.green: + SetConsoleTextAttribute(handle, + FOREGROUND_GREEN | FOREGROUND_INTENSITY); + break; + case Color.yellow: + SetConsoleTextAttribute(handle, + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); + break; + case Color.onRed: + SetConsoleTextAttribute(handle, + BACKGROUND_RED | BACKGROUND_INTENSITY); + break; + case Color.onGreen: + SetConsoleTextAttribute(handle, + BACKGROUND_GREEN | BACKGROUND_INTENSITY); + break; + case Color.onYellow: + SetConsoleTextAttribute(handle, + BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_INTENSITY); + break; + } + stdout.write(text); + stdout.flush(); + SetConsoleTextAttribute(handle, info.wAttributes); + } + else + { + stdout.write(text); + stdout.flush(); + } + } + + private static bool canUseColor() + { + static bool useColor = false; + static bool computed = false; + + if (!computed) + { + // disable colors if the results output is written to a file or pipe instead of a tty + import core.sys.windows.windows; + + CONSOLE_SCREEN_BUFFER_INFO info; + + useColor = GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info) > 0; + computed = true; + } + return useColor; + } +} diff --git a/dunit/framework.d b/dunit/framework.d index 93cf569..eb01a83 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -8,6 +8,7 @@ module dunit.framework; public import dunit.assertion; public import dunit.attributes; +import dunit.color; import core.time; import std.algorithm; @@ -103,33 +104,41 @@ public int dunit_main(string[] args) return 0; } + int result = 0; + if (verbose) - return runTests_Tree(selectedTestNamesByClass); - else - return runTests_Progress(selectedTestNamesByClass); -} + { + auto reporter = new DetailReporter(); -/** - * Runs all the unit tests, showing progress dots, and the results at the end. - */ -public static int runTests_Progress(string[][string] testNamesByClass) -{ - struct Entry + result = runTests(selectedTestNamesByClass, reporter); + } + else { - string testClass; - string testName; - Throwable throwable; + auto reporter = new ResultReporter(); + + result = runTests(selectedTestNamesByClass, reporter); + reporter.summarize; } + return result; +} - Entry[] failures = null; - Entry[] errors = null; - int count = 0; +public static int runTests(string[][string] testNamesByClass, TestListener testListener) +in +{ + assert(testListener !is null); +} +body +{ + bool failure = false; + bool error = false; foreach (className; testClassOrder) { if (className !in testNamesByClass) continue; + testListener.enterClass(className); + // create test object Object testObject = null; @@ -137,18 +146,10 @@ public static int runTests_Progress(string[][string] testNamesByClass) { testObject = testClasses[className].create(); } - catch (AssertException exception) - { - failures ~= Entry(className, "this", exception); - write(red("F")); - stdout.flush(); - continue; - } catch (Throwable throwable) { - errors ~= Entry(className, "this", throwable); - write(red("E")); - stdout.flush(); + testListener.addError("this", throwable); + error = true; continue; } @@ -159,16 +160,14 @@ public static int runTests_Progress(string[][string] testNamesByClass) } catch (AssertException exception) { - failures ~= Entry(className, "BeforeClass", exception); - write(red("F")); - stdout.flush(); + testListener.addFailure("@BeforeClass", exception); + failure = true; continue; } catch (Throwable throwable) { - errors ~= Entry(className, "BeforeClass", throwable); - write(red("E")); - stdout.flush(); + testListener.addError("@BeforeClass", throwable); + error = true; continue; } @@ -177,74 +176,71 @@ public static int runTests_Progress(string[][string] testNamesByClass) { if (canFind(testClasses[className].ignoredTests, testName)) { - write(yellow("I")); - stdout.flush(); + testListener.skipTest(testName); continue; } - ++count; - - // set up bool success = true; - try - { - testClasses[className].before(testObject); - } - catch (AssertException exception) - { - failures ~= Entry(className, "Before", exception); - write(red("F")); - stdout.flush(); - continue; - } - catch (Throwable throwable) - { - errors ~= Entry(className, "Before", throwable); - write(red("E")); - stdout.flush(); - continue; - } + testListener.enterTest(testName); - // test + // set up try { - testClasses[className].test(testObject, testName); + testClasses[className].before(testObject); } catch (AssertException exception) { - failures ~= Entry(className, testName, exception); - write(red("F")); + testListener.addFailure("@Before", exception); + failure = true; success = false; } catch (Throwable throwable) { - errors ~= Entry(className, testName, throwable); - write(red("E")); + testListener.addError("@Before", throwable); + error = true; success = false; } - // tear down (even if test failed) - try - { - testClasses[className].after(testObject); - } - catch (AssertException exception) - { - failures ~= Entry(className, "After", exception); - write(red("F")); - success = false; - } - catch (Throwable throwable) + if (success) { - errors ~= Entry(className, "After", throwable); - write(red("E")); - success = false; + // test + try + { + testClasses[className].test(testObject, testName); + } + catch (AssertException exception) + { + testListener.addFailure(testName, exception); + failure = true; + success = false; + } + catch (Throwable throwable) + { + testListener.addError(testName, throwable); + error = true; + success = false; + } + + // tear down (even if test failed) + try + { + testClasses[className].after(testObject); + } + catch (AssertException exception) + { + testListener.addFailure("@After", exception); + failure = true; + success = false; + } + catch (Throwable throwable) + { + testListener.addError("@After", throwable); + error = true; + success = false; + } } - - if (success) - write(green(".")); - stdout.flush(); + testListener.exitTest(success); } // tear down class @@ -254,257 +250,192 @@ public static int runTests_Progress(string[][string] testNamesByClass) } catch (AssertException exception) { - failures ~= Entry(className, "AfterClass", exception); + testListener.addFailure("@AfterClass", exception); + failure = true; } catch (Throwable throwable) { - errors ~= Entry(className, "AfterClass", throwable); + testListener.addError("@AfterClass", throwable); + error = true; } } - // report results - writeln(); - if (failures.empty && errors.empty) - { - writeln(); - writefln("%s (%d %s)", green("OK"), count, (count == 1) ? "Test" : "Tests"); - return 0; - } + return error ? 2 : failure ? 1 : 0; +} - // report errors - if (!errors.empty) - { - if (errors.length == 1) - writeln("There was 1 error:"); - else - writefln("There were %d errors:", errors.length); +interface TestListener +{ - foreach (i, entry; errors) - { - Throwable throwable = entry.throwable; + public void enterClass(string className); - writefln("%d) %s(%s) %s", i + 1, - entry.testName, entry.testClass, throwable.toString); - } - } + public void enterTest(string testName); - // report failures - if (!failures.empty) - { - if (failures.length == 1) - writeln("There was 1 failure:"); - else - writefln("There were %d failures:", failures.length); + public void skipTest(string testName); - foreach (i, entry; failures) - { - Throwable throwable = entry.throwable; + public void addFailure(string subject, AssertException exception); - writefln("%d) %s(%s) %s@%s(%d): %s", i + 1, - entry.testName, entry.testClass, typeid(throwable).name, - throwable.file, throwable.line, throwable.msg); - } - } + public void addError(string subject, Throwable throwable); + + public void exitTest(bool success); - writeln(); - writeln(red("NOT OK")); - writefln("Tests run: %d, Failures: %d, Errors: %d", count, failures.length, errors.length); - return (errors.length > 0) ? 2 : (failures.length > 0) ? 1 : 0; } -version (Posix) +class ResultReporter : TestListener { - private static string CSI = "\x1B["; - private static string red(string source) + private struct Issue { - return canUseColor() ? CSI ~ "37;41;1m" ~ source ~ CSI ~ "0m" : source; + string testClass; + string testName; + Throwable throwable; } - private static string green(string source) - { - return canUseColor() ? CSI ~ "37;42;1m" ~ source ~ CSI ~ "0m" : source; - } + private Issue[] failures = null; - private static string yellow(string source) - { - return canUseColor() ? CSI ~ "37;43;1m" ~ source ~ CSI ~ "0m" : source; - } + private Issue[] errors = null; - private static bool canUseColor() - { - static bool useColor = false; - static bool computed = false; + private uint count = 0; - if (!computed) - { - // disable colors if the results output is written to a file or pipe instead of a tty - import core.sys.posix.unistd; + private string className; - useColor = isatty(stdout.fileno()) != 0; - computed = true; - } - return useColor; - } -} -else -{ - private static string red(string source) + public void enterClass(string className) { - return source; + this.className = className; } - private static string green(string source) + public void enterTest(string testName) { - return source; + ++this.count; } - private static string yellow(string source) + public void skipTest(string testName) { - return source; + writec(Color.onYellow, "I"); } -} - -/** - * Runs all the unit tests, showing the test tree as the tests run. - */ -public static int runTests_Tree(string[][string] testNamesByClass) -{ - int failureCount = 0; - int errorCount = 0; - writeln("Unit tests: "); - foreach (className; testClassOrder) + public void addFailure(string subject, AssertException exception) { - if (className !in testNamesByClass) - continue; - - writeln(" ", className); + this.failures ~= Issue(this.className, subject, exception); + writec(Color.onRed, "F"); + } - // create test object - Object testObject = null; + public void addError(string subject, Throwable throwable) + { + this.errors ~= Issue(this.className, subject, throwable); + writec(Color.onRed, "E"); + } - try - { - testObject = testClasses[className].create(); - } - catch (AssertException exception) - { - writefln(" FAILURE: this(): %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - } - catch (Throwable throwable) + public void exitTest(bool success) + { + if (success) { - writeln(" ERROR: this(): ", throwable.toString); - ++errorCount; + writec(Color.onGreen, "."); } - if (testObject is null) - continue; + } - // set up class - try - { - testClasses[className].beforeClass(testObject); - } - catch (AssertException exception) - { - writefln(" FAILURE: BeforeClass: %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - continue; - } - catch (Throwable throwable) + public void summarize() + { + writeln(); + if (this.failures.empty && this.errors.empty) { - writeln(" ERROR: BeforeClass: ", throwable.toString); - ++errorCount; - continue; + writeln(); + writec(Color.onGreen, "OK"); + writefln(" (%d %s)", this.count, (this.count == 1) ? "Test" : "Tests"); + return; } - // run each test of the class - foreach (testName; testNamesByClass[className]) + // report errors + if (!this.errors.empty) { - if (canFind(testClasses[className].ignoredTests, testName)) - { - writeln(" IGNORE: ", testName, "()"); - continue; - } + writeln(); + if (this.errors.length == 1) + writeln("There was 1 error:"); + else + writefln("There were %d errors:", this.errors.length); - // set up - try - { - testClasses[className].before(testObject); - } - catch (AssertException exception) - { - writefln(" FAILURE: Before: %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - continue; - } - catch (Throwable throwable) + foreach (i, issue; this.errors) { - writeln(" ERROR: Before: ", throwable.toString); - ++errorCount; - continue; + writefln("%d) %s(%s) %s", i + 1, + issue.testName, issue.testClass, issue.throwable.toString); } + } - // test - try - { - TickDuration startTime = TickDuration.currSystemTick(); - testClasses[className].test(testObject, testName); - double elapsedMs = (TickDuration.currSystemTick() - startTime).usecs() / 1000.0; - writefln(" OK: %6.2f ms %s()", elapsedMs, testName); - } - catch (AssertException exception) - { - writefln(" FAILURE: " ~ testName ~ "(): %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - } - catch (Throwable throwable) - { - writeln(" ERROR: ", testName, "(): ", throwable.toString); - ++errorCount; - } + // report failures + if (!this.failures.empty) + { + writeln(); + if (this.failures.length == 1) + writeln("There was 1 failure:"); + else + writefln("There were %d failures:", this.failures.length); - // tear down (call anyways if test failed) - try - { - testClasses[className].after(testObject); - } - catch (AssertException exception) - { - writefln(" FAILURE: After: %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - } - catch (Throwable throwable) + foreach (i, issue; this.failures) { - writeln(" ERROR: After: ", throwable.toString); - ++errorCount; + Throwable throwable = issue.throwable; + + writefln("%d) %s(%s) %s@%s(%d): %s", i + 1, + issue.testName, issue.testClass, typeid(throwable).name, + throwable.file, throwable.line, throwable.msg); } } - // tear down class - try - { - testClasses[className].afterClass(testObject); - } - catch (AssertException exception) - { - writefln(" FAILURE: AfterClass: %s@%s(%d): %s", - typeid(exception).name, exception.file, exception.line, exception.msg); - ++failureCount; - } - catch (Throwable throwable) + writeln(); + writec(Color.onRed, "NOT OK"); + writeln(); + writefln("Tests run: %d, Failures: %d, Errors: %d", + this.count, this.failures.length, this.errors.length); + } + +} + +class DetailReporter : TestListener +{ + + private string testName; + + private TickDuration startTime; + + public void enterClass(string className) + { + writeln(className); + } + + public void enterTest(string testName) + { + this.testName = testName; + this.startTime = TickDuration.currSystemTick(); + } + + public void skipTest(string testName) + { + writec(Color.yellow, " IGNORE: "); + writeln(testName); + } + + public void addFailure(string subject, AssertException exception) + { + writec(Color.red, " FAILURE: "); + writefln("%s: %s@%s(%d): %s", subject, + typeid(exception).name, exception.file, exception.line, exception.msg); + } + + public void addError(string subject, Throwable throwable) + { + writec(Color.red, " ERROR: "); + writeln(subject, ": ", throwable.toString); + } + + public void exitTest(bool success) + { + if (success) { - writeln(" ERROR: AfterClass: ", throwable.toString); - ++errorCount; + double elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1000.0; + + writec(Color.green, " OK: "); + writefln("%6.2f ms %s", elapsed, this.testName); } } - return (errorCount > 0) ? 2 : (failureCount > 0) ? 1 : 0; + } /** diff --git a/example.d b/example.d index 5e0bce9..648d84e 100755 --- a/example.d +++ b/example.d @@ -53,43 +53,43 @@ class TestFixture public this() { - writeln("this()"); + debug writeln("@this()"); } @BeforeClass public static void setUpClass() { - writeln("BeforeClass"); + debug writeln("@BeforeClass"); } @AfterClass public static void tearDownClass() { - writeln("AfterClass"); + debug writeln("@AfterClass"); } @Before public void setUp() { - writeln("Before"); + debug writeln("@Before"); } @After public void tearDown() { - writeln("After"); + debug writeln("@After"); } @Test public void test1() { - writeln("test1()"); + debug writeln("@test1()"); } @Test public void test2() { - writeln("test2()"); + debug writeln("@test2()"); } } @@ -105,7 +105,7 @@ class TestReuse : TestFixture @Before public override void setUp() { - writeln("Before override"); + debug writeln("@Before override"); } } From eb21e1cfb4dcde78a9fa65a1bdefdcc0ddac7871 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 2 Jun 2013 21:51:38 +0200 Subject: [PATCH 18/73] highlight difference between expected and actual representation --- README.md | 4 ++-- dunit/assertion.d | 21 +++++++++++-------- dunit/diff.d | 53 +++++++++++++++++++++++++++++++++++++++++++++++ example.d | 6 +++--- 4 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 dunit/diff.d diff --git a/README.md b/README.md index 4aa17bb..06be6de 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ more helpful than violated contracts. For example, - assertEquals(42, answer); + assertEquals("bar", "baz"); will report something like - expected: <42> but was: <24> + expected: <"ba[r]"> but was: <"ba[z]"> names of all failed test methods (as helpful as the names of the test methods are expressive) diff --git a/dunit/assertion.d b/dunit/assertion.d index 9712cc8..e903971 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -6,6 +6,8 @@ module dunit.assertion; +import dunit.diff; + import core.thread; import core.time; import std.algorithm; @@ -83,8 +85,9 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, return; string header = (msg.empty) ? null : msg ~ "; "; + auto desc = diff(expected.to!string, actual.to!string); - fail(header ~ "expected: <" ~ expected.to!string ~ "> but was: <"~ actual.to!string ~ ">", + fail(header ~ "expected: <" ~ desc[0] ~ "> but was: <" ~ desc[1] ~ ">", file, line); } @@ -92,11 +95,11 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, unittest { assertEquals("foo", "foo"); - assertEquals("expected: but was: ", - collectExceptionMsg!AssertException(assertEquals("foo", "bar"))); + assertEquals("expected: but was: ", + collectExceptionMsg!AssertException(assertEquals("bar", "baz"))); assertEquals(42, 42); - assertEquals("expected: <42> but was: <24>", + assertEquals("expected: <[42]> but was: <[24]>", collectExceptionMsg!AssertException(assertEquals(42, 24))); assertEquals(42.0, 42.0); @@ -106,7 +109,7 @@ unittest assertEquals(foo, foo); assertEquals(bar, bar); - assertEquals("expected: but was: ", + assertEquals("expected: <[object.Object]> but was: <[null]>", collectExceptionMsg!AssertException(assertEquals(foo, bar))); } @@ -138,11 +141,11 @@ unittest double[] actuals = [1, 2, 3]; assertArrayEquals(expecteds, actuals); - assertEquals("array mismatch at index 1; expected: <2> but was: <2.3>", + assertEquals("array mismatch at index 1; expected: <2[]> but was: <2[.3]>", collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2.3]))); - assertEquals("array length mismatch; expected: <3> but was: <2>", + assertEquals("array length mismatch; expected: <[3]> but was: <[2]>", collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2]))); - assertEquals("array mismatch at index 2; expected: but was: ", + assertEquals("array mismatch at index 2; expected: <[r]> but was: <[z]>", collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); } @@ -284,7 +287,7 @@ unittest * Throws: AssertException when the probe fails to become true before timeout */ public static void assertEventually(bool delegate() probe, - Duration timeout = dur!"msecs"(500), Duration delay = dur!"msecs"(10), + Duration timeout = msecs(500), Duration delay = msecs(10), lazy string msg = null, string file = __FILE__, size_t line = __LINE__) diff --git a/dunit/diff.d b/dunit/diff.d new file mode 100644 index 0000000..2a629c7 --- /dev/null +++ b/dunit/diff.d @@ -0,0 +1,53 @@ +// Copyright Mario Kröplin 2013. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +module dunit.diff; + +import std.algorithm; +import std.range; +import std.typecons; + +/** + * Returns a pair of strings that highlight the difference between lhs and rhs. + */ +Tuple!(string, string) diff(string)(string lhs, string rhs) +{ + const MAX_LENGTH = 20; + + if (lhs == rhs) + return tuple(lhs, rhs); + + auto rest = mismatch(lhs, rhs); + auto retroDiff = mismatch(retro(rest[0]), retro(rest[1])); + auto diff = tuple(retro(retroDiff[0]), retro(retroDiff[1])); + string prefix = lhs[0 .. $ - rest[0].length]; + string suffix = lhs[prefix.length + diff[0].length .. $]; + + if (prefix.length > MAX_LENGTH) + prefix = "..." ~ prefix[$ - MAX_LENGTH .. $]; + if (suffix.length > MAX_LENGTH) + suffix = suffix[0 .. MAX_LENGTH] ~ "..."; + + return tuple( + prefix ~ '[' ~ diff[0] ~ ']' ~ suffix, + prefix ~ '[' ~ diff[1] ~ ']' ~ suffix); +} + +/// +unittest +{ + assert(diff("abc", "abc") == tuple("abc", "abc")); + // highlight difference + assert(diff("abc", "Abc") == tuple("[a]bc", "[A]bc")); + assert(diff("abc", "aBc") == tuple("a[b]c", "a[B]c")); + assert(diff("abc", "abC") == tuple("ab[c]", "ab[C]")); + assert(diff("abc", "") == tuple("[abc]", "[]")); + assert(diff("abc", "abbc") == tuple("ab[]c", "ab[b]c")); + // abbreviate long prefix or suffix + assert(diff("_12345678901234567890a", "_12345678901234567890A") + == tuple("...12345678901234567890[a]", "...12345678901234567890[A]")); + assert(diff("a12345678901234567890_", "A12345678901234567890_") + == tuple("[a]12345678901234567890...", "[A]12345678901234567890...")); +} diff --git a/example.d b/example.d index 648d84e..8d7fd2f 100755 --- a/example.d +++ b/example.d @@ -24,8 +24,8 @@ class Test @Test public void assertEqualsFailure() { - int expected = 42; - int actual = 4 * 6; + string expected = "bar"; + string actual = "baz"; assertEquals(expected, actual); } @@ -176,7 +176,7 @@ class TestingAsynchronousCode private void threadFunction() { - Thread.sleep(dur!"msecs"(100)); + Thread.sleep(msecs(100)); done = true; } From 79055ddf7331a19ff1c0a591d8e4a3c083fd2aa4 Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 12 Jun 2013 23:58:54 +0200 Subject: [PATCH 19/73] cleaned up README --- README.md | 77 +++++++++++++++++++++++++++-------------------- dunit/framework.d | 26 ++++++++-------- 2 files changed, 58 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 06be6de..74d1ba9 100644 --- a/README.md +++ b/README.md @@ -6,40 +6,64 @@ for the [D Programming Language](http://dlang.org). Being based on [JUnit](http://junit.org) it allows to organize tests according to the [xUnit Test Patterns](http://xunitpatterns.com). -It's known to work with versions D 2.062 and D 2.063. +Looking for a replacement of +[DUnit](http://www.dsource.org/projects/dmocks/wiki/DUnit) for D1, +I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. +First, I had to fix some issues, but by now the original implementation +has been largely revised. + +The xUnit Testing Framework is known to work with versions D 2.062 and D 2.063. Testing Functions vs. Interactions ---------------------------------- -D's built-in support for unit tests is best suited for testing functions, +D's built-in support for unittests is best suited for testing functions, when the test cases can be expressed as one-liners. +(Have a look at the documented unittests for the `dunit.assertion` functions.) + +But you're on your own, when you have to write a lot more code per test case, +for example for testing interactions of objects. -For testing interactions of objects, however, more support is required, -for example, for setting up a test fixture. -This is the responsibility of a testing framework. +So, here is what the xUnit Testing Framework has to offer: +- tests are organized in classes +- tests are always named +- tests can reuse a shared fixture +- you see the progress as the tests are run +- you see all failed tests at once +- you get more information about failures Failures vs. Errors ------------------- -With no additional effort, specialized assertion functions report failures -more helpful than violated contracts. +Specialized assertion functions provide more information about failures than +the built-in `assert` expression. For example, assertEquals("bar", "baz"); -will report something like +will not only report the faulty value but will also highlight the difference: + + expected: but was: - expected: <"ba[r]"> but was: <"ba[z]"> +Together with the expressive name of the test (that's your responsibility) +this should be enough information for failures. On the other hand, for +violated contracts and other exceptions from deep down the unit under test +you may wish for the stack trace. -names of all failed test methods (as helpful as the names of the test methods are expressive) +That's why the xUnit Testing Framework distinguishes failures from errors, +and why `dunit.assertion` doesn't use `AssertError` but introduces its own +`AssertException`. User Defined Attributes ----------------------- -`@Test`, `@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore` +Thanks to D's User Defined Attributes, test names no longer have to start with +"test". -instead of naming convention testLikeThis +Put `mixin UnitTest;` in your test class and attach `@Test`, +`@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore` +(borrowed from JUnit 4) to the member functions to state their purpose. Examples -------- @@ -50,29 +74,16 @@ Run the included example to see the xUnit Testing Framework in action: (When you get one error and two failures, everything works fine.) -unittest functions testing the assertions - - dmd -debug example.d dunit/assertion.d dunit/attributes.d dunit/framework.d - ./example --verbose - -selective test execution - - ./example.d --list - ./example.d --filter testEqualsFailure - -display usage - - ./example.d --help - -TODO ----- +Have a look at the debug output of the example in "verbose" style: -more helpful string difference + rdmd -debug example.d --verbose -forked from [jmcabo/dunit](https://github.com/jmcabo/dunit); fixed issues; restructured +Or just focus on the issues: -not [D(1)Unit](http://www.dsource.org/projects/dmocks/wiki/DUnit) - allows to call (passed) test methods during setup + ./example.d --filter Test.assert --filter error -won't fix [Issue 4653 - More unit test functions should be added](http://d.puremagic.com/issues/show_bug.cgi?id=4653) +Next Steps +---------- -TODO: [Hamcrest](http://code.google.com/p/hamcrest/) Matchers and `assertThat` +Integrate [Hamcrest](http://code.google.com/p/hamcrest/) Matchers +and `assertThat`. diff --git a/dunit/framework.d b/dunit/framework.d index eb01a83..a895a9b 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -336,13 +336,6 @@ class ResultReporter : TestListener public void summarize() { writeln(); - if (this.failures.empty && this.errors.empty) - { - writeln(); - writec(Color.onGreen, "OK"); - writefln(" (%d %s)", this.count, (this.count == 1) ? "Test" : "Tests"); - return; - } // report errors if (!this.errors.empty) @@ -379,11 +372,20 @@ class ResultReporter : TestListener } } - writeln(); - writec(Color.onRed, "NOT OK"); - writeln(); - writefln("Tests run: %d, Failures: %d, Errors: %d", - this.count, this.failures.length, this.errors.length); + if (this.failures.empty && this.errors.empty) + { + writeln(); + writec(Color.onGreen, "OK"); + writefln(" (%d %s)", this.count, (this.count == 1) ? "Test" : "Tests"); + } + else + { + writeln(); + writec(Color.onRed, "NOT OK"); + writeln(); + writefln("Tests run: %d, Failures: %d, Errors: %d", + this.count, this.failures.length, this.errors.length); + } } } From 3433f8424b7001d515a21dc94883452f90e80480 Mon Sep 17 00:00:00 2001 From: linkrope Date: Thu, 13 Jun 2013 21:44:39 +0200 Subject: [PATCH 20/73] prefer switch (...) with (...) --- dunit/color.d | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/dunit/color.d b/dunit/color.d index 492b26f..625fa8a 100644 --- a/dunit/color.d +++ b/dunit/color.d @@ -17,24 +17,24 @@ version (Posix) { const string CSI = "\x1B["; - final switch (color) + final switch (color) with (Color) { - case Color.red: + case red: stdout.write(CSI, "37;31;1m"); break; - case Color.green: + case green: stdout.write(CSI, "37;32;1m"); break; - case Color.yellow: + case yellow: stdout.write(CSI, "37;33;1m"); break; - case Color.onRed: + case onRed: stdout.write(CSI, "37;41;1m"); break; - case Color.onGreen: + case onGreen: stdout.write(CSI, "37;42;1m"); break; - case Color.onYellow: + case onYellow: stdout.write(CSI, "37;43;1m"); break; } @@ -78,29 +78,29 @@ version (Windows) CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(handle, &info); - final switch (color) + final switch (color) with (Color) { - case Color.red: + case red: SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_INTENSITY); break; - case Color.green: + case green: SetConsoleTextAttribute(handle, FOREGROUND_GREEN | FOREGROUND_INTENSITY); break; - case Color.yellow: + case yellow: SetConsoleTextAttribute(handle, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY); break; - case Color.onRed: + case onRed: SetConsoleTextAttribute(handle, BACKGROUND_RED | BACKGROUND_INTENSITY); break; - case Color.onGreen: + case onGreen: SetConsoleTextAttribute(handle, BACKGROUND_GREEN | BACKGROUND_INTENSITY); break; - case Color.onYellow: + case onYellow: SetConsoleTextAttribute(handle, BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_INTENSITY); break; From fb6cd7ca5b5aab89d9571360256b9b7d788279f8 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 10 Aug 2013 20:50:59 +0200 Subject: [PATCH 21/73] added dub support --- README.md | 5 +++++ dunit/color.d | 8 ++++---- dunit/diff.d | 6 +++--- package.json | 21 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 package.json diff --git a/README.md b/README.md index 74d1ba9..b8e6a77 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ Or just focus on the issues: ./example.d --filter Test.assert --filter error +Alternatively, build and run the example using +[dub](https://github.com/rejectedsoftware/dub): + + dub --build=plain --config=example -- -verbose + Next Steps ---------- diff --git a/dunit/color.d b/dunit/color.d index 625fa8a..9ba9151 100644 --- a/dunit/color.d +++ b/dunit/color.d @@ -12,7 +12,7 @@ enum Color { red, green, yellow, onRed, onGreen, onYellow } version (Posix) { public void writec(Color color, string text) - { + { if (canUseColor()) { const string CSI = "\x1B["; @@ -48,17 +48,17 @@ version (Posix) stdout.flush(); } } - + private static bool canUseColor() { static bool useColor = false; static bool computed = false; - + if (!computed) { // disable colors if the output is written to a file or pipe instead of a tty import core.sys.posix.unistd; - + useColor = isatty(stdout.fileno()) != 0; computed = true; } diff --git a/dunit/diff.d b/dunit/diff.d index 2a629c7..85b0a13 100644 --- a/dunit/diff.d +++ b/dunit/diff.d @@ -15,7 +15,7 @@ import std.typecons; Tuple!(string, string) diff(string)(string lhs, string rhs) { const MAX_LENGTH = 20; - + if (lhs == rhs) return tuple(lhs, rhs); @@ -28,7 +28,7 @@ Tuple!(string, string) diff(string)(string lhs, string rhs) if (prefix.length > MAX_LENGTH) prefix = "..." ~ prefix[$ - MAX_LENGTH .. $]; if (suffix.length > MAX_LENGTH) - suffix = suffix[0 .. MAX_LENGTH] ~ "..."; + suffix = suffix[0 .. MAX_LENGTH] ~ "..."; return tuple( prefix ~ '[' ~ diff[0] ~ ']' ~ suffix, @@ -44,7 +44,7 @@ unittest assert(diff("abc", "aBc") == tuple("a[b]c", "a[B]c")); assert(diff("abc", "abC") == tuple("ab[c]", "ab[C]")); assert(diff("abc", "") == tuple("[abc]", "[]")); - assert(diff("abc", "abbc") == tuple("ab[]c", "ab[b]c")); + assert(diff("abc", "abbc") == tuple("ab[]c", "ab[b]c")); // abbreviate long prefix or suffix assert(diff("_12345678901234567890a", "_12345678901234567890A") == tuple("...12345678901234567890[a]", "...12345678901234567890[A]")); diff --git a/package.json b/package.json new file mode 100644 index 0000000..7df5f74 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "dunit", + "description": "xUnit Testing Framework for D", + "copyright": "Copyright © 2013, Mario Kröplin", + "authors": ["Juan Manuel Cabo", "Mario Kröplin"], + "dependencies": {}, + "configurations": + [ + { + "name": "library", + "targetType": "library", + "sourcePaths": ["dunit"] + }, + { + "name": "example", + "targetType": "executable", + "sourceFiles": ["example.d"], + "sourcePaths": ["dunit"] + } + ] +} From a3662b2a44c610b3c82d2a3df89855da8a281f58 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 18 Aug 2013 15:05:34 +0200 Subject: [PATCH 22/73] added assertOp --- README.md | 13 ++++++++++++- dunit/assertion.d | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b8e6a77..20035f8 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,17 @@ will not only report the faulty value but will also highlight the difference: expected: but was: +The more general + + assertOp!">="(a, b); + +(borrowed from +[Issue 4653](http://d.puremagic.com/issues/show_bug.cgi?id=4653)) +will at least report the concrete values in case of a failure: + + condition (2 >= 3) not satisfied + + Together with the expressive name of the test (that's your responsibility) this should be enough information for failures. On the other hand, for violated contracts and other exceptions from deep down the unit under test @@ -85,7 +96,7 @@ Or just focus on the issues: Alternatively, build and run the example using [dub](https://github.com/rejectedsoftware/dub): - dub --build=plain --config=example -- -verbose + dub --build=plain --config=example -- --verbose Next Steps ---------- diff --git a/dunit/assertion.d b/dunit/assertion.d index e903971..c336afb 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -13,6 +13,7 @@ import core.time; import std.algorithm; import std.array; import std.conv; +import std.string; version (unittest) import std.exception; @@ -271,6 +272,34 @@ unittest collectExceptionMsg!AssertException(fail())); } +/** + * Asserts that the condition (lhs op rhs) is satisfied. + * Throws: AssertException otherwise + * See_Also: http://d.puremagic.com/issues/show_bug.cgi?id=4653 + */ +template assertOp(string op) +{ + void assertOp(T, U)(T lhs, U rhs, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) + { + mixin("if (lhs " ~ op ~ " rhs) return;"); + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(format("%scondition (%s %s %s) not satisfied", header, lhs, op, rhs), + file, line); + } +} + +/// +unittest +{ + assertOp!"<"(2, 3); + assertEquals("condition (2 >= 3) not satisfied", + collectExceptionMsg!AssertException(assertOp!">="(2, 3))); +} + /** * Checks a probe until the timeout expires. The assert error is produced * if the probe fails to return 'true' before the timeout. From 2b54f6a78030da2051e53a08cecdaf578d137b84 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 14 Sep 2013 14:31:44 +0200 Subject: [PATCH 23/73] added assertEmpty and assertNotEmpty --- dunit/assertion.d | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/dunit/assertion.d b/dunit/assertion.d index c336afb..159eee7 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -150,6 +150,50 @@ unittest collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); } +/** + * Asserts that the value is empty. + * Throws: AssertException otherwise + */ +void assertEmpty(T)(T actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (actual.empty) + return; + + fail(msg, file, line); +} + +/// +unittest +{ + assertEmpty([]); + assertEquals("Assertion failure", + collectExceptionMsg!AssertException(assertEmpty([1, 2, 3]))); +} + +/** + * Asserts that the value is not empty. + * Throws: AssertException otherwise + */ +void assertNotEmpty(T)(T actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + if (!actual.empty) + return; + + fail(msg, file, line); +} + +/// +unittest +{ + assertNotEmpty([1, 2, 3]); + assertEquals("Assertion failure", + collectExceptionMsg!AssertException(assertNotEmpty([]))); +} + /** * Asserts that the value is null. * Throws: AssertException otherwise @@ -284,9 +328,9 @@ template assertOp(string op) size_t line = __LINE__) { mixin("if (lhs " ~ op ~ " rhs) return;"); - + string header = (msg.empty) ? null : msg ~ "; "; - + fail(format("%scondition (%s %s %s) not satisfied", header, lhs, op, rhs), file, line); } From e0d1e8bb329b1ae60d5ef2a890ab789a6456687a Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 15 Sep 2013 22:09:15 +0200 Subject: [PATCH 24/73] added XML test reporting --- README.md | 25 ++- dunit/assertion.d | 2 +- dunit/framework.d | 456 +++++++++++++++++++++++++++++----------------- example.d | 15 +- 4 files changed, 317 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 20035f8..3fdeb5f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,17 @@ Put `mixin UnitTest;` in your test class and attach `@Test`, `@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore` (borrowed from JUnit 4) to the member functions to state their purpose. +Test Results +------------ + +Test results are reported while the tests are run. A "progress bar" is written +with a "`.`" for each passed test, an "`F`" for each failure, an "`E`" for each +error, and an "`I`" (ignore) for each skipped test. + +In addition, an XML test report is available that uses the JUnitReport format. +The continuous integration tool [Jenkins](http://jenkins-ci.org), for example, +understands the JUnitReport format and can be used to browse the test results. + Examples -------- @@ -83,7 +94,7 @@ Run the included example to see the xUnit Testing Framework in action: ./example.d -(When you get one error and two failures, everything works fine.) +(When you get three failures, one error, and one skip, everything works fine.) Have a look at the debug output of the example in "verbose" style: @@ -98,8 +109,12 @@ Alternatively, build and run the example using dub --build=plain --config=example -- --verbose -Next Steps ----------- +Related Projects +---------------- -Integrate [Hamcrest](http://code.google.com/p/hamcrest/) Matchers -and `assertThat`. +- [DMocks-revived](https://github.com/QAston/DMocks-revived): + a mock-object framework that allows to mock interfaces or classes +- [specd](https://github.com/jostly/specd): + a unit testing framework inspired by [specs2](http://etorreborre.github.io/specs2/) and [ScalaTest](http://www.scalatest.org) +- [unit-threaded](https://github.com/atilaneves/unit-threaded): + a multi-threaded unit testing framework diff --git a/dunit/assertion.d b/dunit/assertion.d index 159eee7..e65d0d4 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -331,7 +331,7 @@ template assertOp(string op) string header = (msg.empty) ? null : msg ~ "; "; - fail(format("%scondition (%s %s %s) not satisfied", header, lhs, op, rhs), + fail("%scondition (%s %s %s) not satisfied".format(header, lhs, op, rhs), file, line); } } diff --git a/dunit/framework.d b/dunit/framework.d index a895a9b..396b292 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -15,10 +15,13 @@ import std.algorithm; import std.array; import std.conv; import std.getopt; +import std.file; import std.path; import std.regex; import std.stdio; +import std.string; public import std.typetuple; +import std.xml; struct TestClass { @@ -49,12 +52,14 @@ public int dunit_main(string[] args) string[] filters = null; bool help = false; bool list = false; + string report = null; bool verbose = false; getopt(args, "filter|f", &filters, "help|h", &help, "list|l", &list, + "report", &report, "verbose|v", &verbose); if (help) @@ -67,6 +72,7 @@ public int dunit_main(string[] args) writeln(" Multiple selections are processed in sequence"); writeln(" -h, --help Display usage information, then exit"); writeln(" -l, --list Display the test functions, then exit"); + writeln(" --report FILE Write JUnit-style XML test report"); writeln(" -v, --verbose Display more information as the tests are run"); return 0; } @@ -104,199 +110,189 @@ public int dunit_main(string[] args) return 0; } - int result = 0; + TestListener[] testListeners = null; if (verbose) { - auto reporter = new DetailReporter(); - - result = runTests(selectedTestNamesByClass, reporter); + testListeners ~= new DetailReporter(); } else { - auto reporter = new ResultReporter(); + testListeners ~= new IssueReporter(); + } - result = runTests(selectedTestNamesByClass, reporter); - reporter.summarize; + if (!report.empty) + { + testListeners ~= new XmlReporter(report); } - return result; + + auto reporter = new ResultReporter(); + + testListeners ~= reporter; + runTests(selectedTestNamesByClass, testListeners); + return (reporter.errors > 0) ? 2 : (reporter.failures > 0) ? 1 : 0; } -public static int runTests(string[][string] testNamesByClass, TestListener testListener) +public static void runTests(string[][string] testNamesByClass, TestListener[] testListeners) in { - assert(testListener !is null); + assert(all!"a !is null"(testListeners)); } body { - bool failure = false; - bool error = false; - - foreach (className; testClassOrder) + bool tryRun(string phase, void delegate() action) { - if (className !in testNamesByClass) - continue; - - testListener.enterClass(className); - - // create test object - Object testObject = null; - - try - { - testObject = testClasses[className].create(); - } - catch (Throwable throwable) - { - testListener.addError("this", throwable); - error = true; - continue; - } - - // set up class try { - testClasses[className].beforeClass(testObject); + action(); + return true; } catch (AssertException exception) { - testListener.addFailure("@BeforeClass", exception); - failure = true; - continue; + foreach (testListener; testListeners) + testListener.addFailure(phase, exception); + return false; } catch (Throwable throwable) { - testListener.addError("@BeforeClass", throwable); - error = true; - continue; + foreach (testListener; testListeners) + testListener.addError(phase, throwable); + return false; } + } - // run each test function of the class + foreach (className; testClassOrder) + { + if (className !in testNamesByClass) + continue; + + foreach (testListener; testListeners) + testListener.enterClass(className); + + Object testObject = null; + bool classSetUp = true; // not yet failed + + // run each @Test of the class foreach (testName; testNamesByClass[className]) { - if (canFind(testClasses[className].ignoredTests, testName)) - { - testListener.skipTest(testName); - continue; - } - bool success = true; + bool ignore = canFind(testClasses[className].ignoredTests, testName); - testListener.enterTest(testName); + foreach (testListener; testListeners) + testListener.enterTest(testName); + scope (exit) + foreach (testListener; testListeners) + testListener.exitTest(success); - // set up - try + // create test object on demand + if (!ignore && testObject is null) { - testClasses[className].before(testObject); - } - catch (AssertException exception) - { - testListener.addFailure("@Before", exception); - failure = true; - success = false; + if (classSetUp) + { + classSetUp = tryRun("this", + { testObject = testClasses[className].create(); }); + } + if (classSetUp) + { + classSetUp = tryRun("@BeforeClass", + { testClasses[className].beforeClass(testObject); }); + } } - catch (Throwable throwable) + + if (ignore || !classSetUp) { - testListener.addError("@Before", throwable); - error = true; + foreach (testListener; testListeners) + testListener.skip(); success = false; + continue; } + success = tryRun("@Before", + { testClasses[className].before(testObject); }); + if (success) { - // test - try - { - testClasses[className].test(testObject, testName); - } - catch (AssertException exception) - { - testListener.addFailure(testName, exception); - failure = true; - success = false; - } - catch (Throwable throwable) - { - testListener.addError(testName, throwable); - error = true; - success = false; - } - - // tear down (even if test failed) - try - { - testClasses[className].after(testObject); - } - catch (AssertException exception) - { - testListener.addFailure("@After", exception); - failure = true; - success = false; - } - catch (Throwable throwable) - { - testListener.addError("@After", throwable); - error = true; - success = false; - } + success = tryRun("@Test", + { testClasses[className].test(testObject, testName); }); + // run @After even if @Test failed + success = tryRun("@After", + { testClasses[className].after(testObject); }) + && success; } - testListener.exitTest(success); } - // tear down class - try + if (testObject !is null && classSetUp) { - testClasses[className].afterClass(testObject); - } - catch (AssertException exception) - { - testListener.addFailure("@AfterClass", exception); - failure = true; - } - catch (Throwable throwable) - { - testListener.addError("@AfterClass", throwable); - error = true; + tryRun("@AfterClass", + { testClasses[className].afterClass(testObject); }); } } - return error ? 2 : failure ? 1 : 0; + foreach (testListener; testListeners) + testListener.exit(); } interface TestListener { - public void enterClass(string className); - public void enterTest(string testName); + public void skip(); + public void addFailure(string phase, AssertException exception); + public void addError(string phase, Throwable throwable); + public void exitTest(bool success); + public void exit(); - public void skipTest(string testName); - - public void addFailure(string subject, AssertException exception); + public static string prettyOrigin(string className, string testName, string phase) + { + string origin = prettyOrigin(testName, phase); - public void addError(string subject, Throwable throwable); + if (origin.startsWith('@')) + return className ~ origin; + else + return className ~ '.' ~ origin; + } - public void exitTest(bool success); + public static string prettyOrigin(string testName, string phase) + { + switch (phase) + { + case "@Test": + return testName; + case "this": + case "@BeforeClass": + case "@AfterClass": + return phase; + default: + return testName ~ phase; + } + } + public static string description(Throwable throwable) + { + with (throwable) + { + if (file.empty) + return typeid(throwable).name; + else + return "%s@%s(%d)".format(typeid(throwable).name, file, line); + } + } } -class ResultReporter : TestListener +class IssueReporter : TestListener { - private struct Issue { string testClass; string testName; + string phase; Throwable throwable; } private Issue[] failures = null; - private Issue[] errors = null; - - private uint count = 0; - private string className; + private string testName; public void enterClass(string className) { @@ -305,35 +301,33 @@ class ResultReporter : TestListener public void enterTest(string testName) { - ++this.count; + this.testName = testName; } - public void skipTest(string testName) + public void skip() { writec(Color.onYellow, "I"); } - public void addFailure(string subject, AssertException exception) + public void addFailure(string phase, AssertException exception) { - this.failures ~= Issue(this.className, subject, exception); + this.failures ~= Issue(this.className, this.testName, phase, exception); writec(Color.onRed, "F"); } - public void addError(string subject, Throwable throwable) + public void addError(string phase, Throwable throwable) { - this.errors ~= Issue(this.className, subject, throwable); + this.errors ~= Issue(this.className, this.testName, phase, throwable); writec(Color.onRed, "E"); } public void exitTest(bool success) { if (success) - { writec(Color.onGreen, "."); - } } - public void summarize() + public void exit() { writeln(); @@ -348,8 +342,10 @@ class ResultReporter : TestListener foreach (i, issue; this.errors) { - writefln("%d) %s(%s) %s", i + 1, - issue.testName, issue.testClass, issue.throwable.toString); + writefln("%d) %s", i + 1, + prettyOrigin(issue.testClass, issue.testName, issue.phase)); + writeln(issue.throwable.toString); + writeln("----------------"); } } @@ -366,35 +362,17 @@ class ResultReporter : TestListener { Throwable throwable = issue.throwable; - writefln("%d) %s(%s) %s@%s(%d): %s", i + 1, - issue.testName, issue.testClass, typeid(throwable).name, - throwable.file, throwable.line, throwable.msg); + writefln("%d) %s", i + 1, + prettyOrigin(issue.testClass, issue.testName, issue.phase)); + writefln("%s: %s", description(throwable), throwable.msg); } } - - if (this.failures.empty && this.errors.empty) - { - writeln(); - writec(Color.onGreen, "OK"); - writefln(" (%d %s)", this.count, (this.count == 1) ? "Test" : "Tests"); - } - else - { - writeln(); - writec(Color.onRed, "NOT OK"); - writeln(); - writefln("Tests run: %d, Failures: %d, Errors: %d", - this.count, this.failures.length, this.errors.length); - } } - } class DetailReporter : TestListener { - private string testName; - private TickDuration startTime; public void enterClass(string className) @@ -408,36 +386,184 @@ class DetailReporter : TestListener this.startTime = TickDuration.currSystemTick(); } - public void skipTest(string testName) + public void skip() { writec(Color.yellow, " IGNORE: "); - writeln(testName); + writeln(this.testName); } - public void addFailure(string subject, AssertException exception) + public void addFailure(string phase, AssertException exception) { writec(Color.red, " FAILURE: "); - writefln("%s: %s@%s(%d): %s", subject, - typeid(exception).name, exception.file, exception.line, exception.msg); + writeln(prettyOrigin(this.testName, phase)); + writefln(" %s: %s", description(exception), exception.msg); } - public void addError(string subject, Throwable throwable) + public void addError(string phase, Throwable throwable) { writec(Color.red, " ERROR: "); - writeln(subject, ": ", throwable.toString); + writeln(prettyOrigin(this.testName, phase)); + writeln(" ", throwable.toString); + writeln("----------------"); } public void exitTest(bool success) { if (success) { - double elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1000.0; + double elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0; writec(Color.green, " OK: "); writefln("%6.2f ms %s", elapsed, this.testName); } } + public void exit() + { + // do nothing + } + } + +class ResultReporter : TestListener +{ + private uint tests = 0; + private uint failures = 0; + private uint errors = 0; + private uint skips = 0; + + public void enterClass(string className) + { + // do nothing + } + + public void enterTest(string testName) + { + ++this.tests; + } + + public void skip() + { + ++this.skips; + } + + public void addFailure(string phase, AssertException exception) + { + ++this.failures; + } + + public void addError(string phase, Throwable throwable) + { + ++this.errors; + } + + public void exitTest(bool success) + { + // do nothing + } + + public void exit() + { + writeln(); + writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d", + this.tests, this.failures, this.errors, this.skips); + + if (this.failures + this.errors == 0) + { + writec(Color.onGreen, "OK"); + writeln(); + } + else + { + writec(Color.onRed, "NOT OK"); + writeln(); + } + } +} + +class XmlReporter : TestListener +{ + private string fileName; + private Document document; + private Element testSuite; + private Element testCase; + private string className; + private TickDuration startTime; + + public this(string fileName) + { + this.fileName = fileName; + this.document = new Document(new Tag("testsuites")); + this.testSuite = new Element("testsuite"); + this.testSuite.tag.attr["name"] = "dunit"; + this.document ~= this.testSuite; + } + + public void enterClass(string className) + { + this.className = className; + } + + public void enterTest(string testName) + { + this.testCase = new Element("testcase"); + this.testCase.tag.attr["classname"] = this.className; + this.testCase.tag.attr["name"] = testName; + this.testSuite ~= this.testCase; + this.startTime = TickDuration.currSystemTick(); + } + + public void skip() + { + // avoid wrong interpretation of more than one child + if (this.testCase.elements.empty) + { + this.testCase ~= new Element("skipped"); + } + } + + public void addFailure(string phase, AssertException exception) + { + // avoid wrong interpretation of more than one child + if (this.testCase.elements.empty) + { + auto failure = new Element("failure"); + string message = "%s %s: %s".format(phase, + description(exception), exception.msg); + + // FIXME encoding will be fixed in D 2.064 + failure.tag.attr["message"] = encode(encode(message)); + this.testCase ~= failure; + } + } + + public void addError(string phase, Throwable throwable) + { + // avoid wrong interpretation of more than one child + if (this.testCase.elements.empty) + { + auto error = new Element("error", throwable.info.toString); + string message = "%s %s: %s".format(phase, + description(throwable), throwable.msg); + + // FIXME encoding will be fixed in D 2.064 + error.tag.attr["message"] = encode(encode(message)); + this.testCase ~= error; + } + } + + public void exitTest(bool success) + { + double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; + + this.testCase.tag.attr["time"] = "%.3f".format(elapsed); + } + + public void exit() + { + string report = join(this.document.pretty(4), "\n") ~ "\n"; + + std.file.write(this.fileName, report); + } } /** @@ -445,7 +571,6 @@ class DetailReporter : TestListener */ mixin template UnitTest() { - public static this() { TestClass testClass; @@ -550,5 +675,4 @@ mixin template UnitTest() enum _hasAttribute = staticIndexOf!(attribute, __traits(getAttributes, __traits(getMember, T, name))) != -1; } - } diff --git a/example.d b/example.d index 8d7fd2f..04eabca 100755 --- a/example.d +++ b/example.d @@ -18,7 +18,6 @@ import std.stdio; */ class Test { - mixin UnitTest; @Test @@ -38,6 +37,12 @@ class Test assertArrayEquals(expected, actual); } + + @Test + public void assertOpFailure() + { + assertOp!"<"(6 * 7, 42); + } } /** @@ -48,7 +53,6 @@ class Test */ class TestFixture { - mixin UnitTest; public this() @@ -91,7 +95,6 @@ class TestFixture { debug writeln("@test2()"); } - } /** @@ -99,7 +102,6 @@ class TestFixture */ class TestReuse : TestFixture { - mixin UnitTest; @Before @@ -107,7 +109,6 @@ class TestReuse : TestFixture { debug writeln("@Before override"); } - } /** @@ -115,7 +116,6 @@ class TestReuse : TestFixture */ class TestingThisAndThat { - mixin UnitTest; // test function can have default arguments @@ -146,7 +146,6 @@ class TestingThisAndThat { assert(false); } - } /** @@ -154,7 +153,6 @@ class TestingThisAndThat */ class TestingAsynchronousCode { - mixin UnitTest; private Thread thread; @@ -189,7 +187,6 @@ class TestingAsynchronousCode assertEventually({ return done; }); } - } // either use the 'Main' mixin or call 'dunit_main(args)' From b2618fefd43e1f2e0031ab850ebf300a176af7a9 Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 24 Sep 2013 23:29:06 +0200 Subject: [PATCH 25/73] added aliases --- README.md | 7 ++++--- dunit/assertion.d | 5 +++++ example.d | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3fdeb5f..8b25586 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,13 @@ Test Results ------------ Test results are reported while the tests are run. A "progress bar" is written -with a "`.`" for each passed test, an "`F`" for each failure, an "`E`" for each -error, and an "`I`" (ignore) for each skipped test. +with a `.` for each passed test, an `F` for each failure, an `E` for each error, +and an `I` (ignore) for each skipped test. In addition, an XML test report is available that uses the JUnitReport format. The continuous integration tool [Jenkins](http://jenkins-ci.org), for example, -understands the JUnitReport format and can be used to browse the test results. +understands this JUnitReport format. Thus, Jenkins can be used to browse +test reports, track failures and errors, and even provide trends over time. Examples -------- diff --git a/dunit/assertion.d b/dunit/assertion.d index e65d0d4..c0f5c6f 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -316,6 +316,11 @@ unittest collectExceptionMsg!AssertException(fail())); } +alias assertOp!">" assertGreaterThan; +alias assertOp!">=" assertGreaterThanOrEqual; +alias assertOp!"<" assertLessThan; +alias assertOp!"<=" assertLessThanOrEqual; + /** * Asserts that the condition (lhs op rhs) is satisfied. * Throws: AssertException otherwise diff --git a/example.d b/example.d index 04eabca..c45b8f0 100755 --- a/example.d +++ b/example.d @@ -41,7 +41,7 @@ class Test @Test public void assertOpFailure() { - assertOp!"<"(6 * 7, 42); + assertLessThan(6 * 7, 42); } } From 944a18988f3d3f95af832da21748eb68ea3fd38c Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 28 Sep 2013 13:03:04 +0200 Subject: [PATCH 26/73] tidied up --- dunit/framework.d | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dunit/framework.d b/dunit/framework.d index 396b292..01a1258 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -576,9 +576,9 @@ mixin template UnitTest() TestClass testClass; testClass.tests = _memberFunctions!(typeof(this), Test, - __traits(allMembers, typeof(this))).result.dup; + __traits(allMembers, typeof(this))).dup; testClass.ignoredTests = _memberFunctions!(typeof(this), Ignore, - __traits(allMembers, typeof(this))).result.dup; + __traits(allMembers, typeof(this))).dup; static Object create() { @@ -588,31 +588,31 @@ mixin template UnitTest() static void beforeClass(Object o) { mixin(_sequence(_memberFunctions!(typeof(this), BeforeClass, - __traits(allMembers, typeof(this))).result)); + __traits(allMembers, typeof(this))))); } static void before(Object o) { mixin(_sequence(_memberFunctions!(typeof(this), Before, - __traits(allMembers, typeof(this))).result)); + __traits(allMembers, typeof(this))))); } static void test(Object o, string name) { mixin(_choice(_memberFunctions!(typeof(this), Test, - __traits(allMembers, typeof(this))).result)); + __traits(allMembers, typeof(this))))); } static void after(Object o) { mixin(_sequence(_memberFunctions!(typeof(this), After, - __traits(allMembers, typeof(this))).result)); + __traits(allMembers, typeof(this))))); } static void afterClass(Object o) { mixin(_sequence(_memberFunctions!(typeof(this), AfterClass, - __traits(allMembers, typeof(this))).result)); + __traits(allMembers, typeof(this))))); } testClass.create = &create; @@ -654,18 +654,18 @@ mixin template UnitTest() { static if (names.length == 0) { - immutable(string[]) result = []; + immutable(string[]) _memberFunctions = []; } else { static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) && _hasAttribute!(T, names[0], U)) { - immutable(string[]) result = [names[0]] ~ _memberFunctions!(T, U, names[1 .. $]).result; + immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, U, names[1 .. $]); } else { - immutable(string[]) result = _memberFunctions!(T, U, names[1 .. $]).result; + immutable(string[]) _memberFunctions = _memberFunctions!(T, U, names[1 .. $]); } } } From 45a036ea1692a7f4e3ba012c16268896981a3cc8 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 28 Sep 2013 16:58:23 +0200 Subject: [PATCH 27/73] added support for @Ignore reason --- README.md | 4 +- dunit/attributes.d | 6 +- dunit/framework.d | 151 +++++++++++++++++++++++++++++---------------- example.d | 2 +- 4 files changed, 106 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 8b25586..6d49e9d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Thanks to D's User Defined Attributes, test names no longer have to start with "test". Put `mixin UnitTest;` in your test class and attach `@Test`, -`@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore` +`@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore("...")` (borrowed from JUnit 4) to the member functions to state their purpose. Test Results @@ -117,5 +117,7 @@ Related Projects a mock-object framework that allows to mock interfaces or classes - [specd](https://github.com/jostly/specd): a unit testing framework inspired by [specs2](http://etorreborre.github.io/specs2/) and [ScalaTest](http://www.scalatest.org) +- [DUnit](https://github.com/kalekold/dunit): + a toolkit of test assertions and a template mixin to enable mocking - [unit-threaded](https://github.com/atilaneves/unit-threaded): a multi-threaded unit testing framework diff --git a/dunit/attributes.d b/dunit/attributes.d index b0902b2..88ea3ca 100644 --- a/dunit/attributes.d +++ b/dunit/attributes.d @@ -4,5 +4,9 @@ enum After; enum AfterClass; enum Before; enum BeforeClass; -enum Ignore; enum Test; + +struct Ignore +{ + string reason = null; +} diff --git a/dunit/framework.d b/dunit/framework.d index 01a1258..49f3a86 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -14,19 +14,14 @@ import core.time; import std.algorithm; import std.array; import std.conv; -import std.getopt; -import std.file; -import std.path; -import std.regex; import std.stdio; import std.string; public import std.typetuple; -import std.xml; struct TestClass { string[] tests; - string[] ignoredTests; + string[string] ignoredTests; Object function() create; void function(Object o) beforeClass; @@ -49,6 +44,10 @@ mixin template Main() public int dunit_main(string[] args) { + import std.getopt; + import std.path; + import std.regex; + string[] filters = null; bool help = false; bool list = false; @@ -176,7 +175,7 @@ body foreach (testName; testNamesByClass[className]) { bool success = true; - bool ignore = canFind(testClasses[className].ignoredTests, testName); + bool ignore = cast(bool)(testName in testClasses[className].ignoredTests); foreach (testListener; testListeners) testListener.enterTest(testName); @@ -201,8 +200,10 @@ body if (ignore || !classSetUp) { + string reason = testClasses[className].ignoredTests[testName]; + foreach (testListener; testListeners) - testListener.skip(); + testListener.skip(reason); success = false; continue; } @@ -236,7 +237,7 @@ interface TestListener { public void enterClass(string className); public void enterTest(string testName); - public void skip(); + public void skip(string reason); public void addFailure(string phase, AssertException exception); public void addError(string phase, Throwable throwable); public void exitTest(bool success); @@ -304,7 +305,7 @@ class IssueReporter : TestListener this.testName = testName; } - public void skip() + public void skip(string reason) { writec(Color.onYellow, "I"); } @@ -386,10 +387,12 @@ class DetailReporter : TestListener this.startTime = TickDuration.currSystemTick(); } - public void skip() + public void skip(string reason) { writec(Color.yellow, " IGNORE: "); writeln(this.testName); + if (!reason.empty) + writefln(` "%s"`, reason); } public void addFailure(string phase, AssertException exception) @@ -441,7 +444,7 @@ class ResultReporter : TestListener ++this.tests; } - public void skip() + public void skip(string reason) { ++this.skips; } @@ -482,6 +485,8 @@ class ResultReporter : TestListener class XmlReporter : TestListener { + import std.xml; + private string fileName; private Document document; private Element testSuite; @@ -512,12 +517,16 @@ class XmlReporter : TestListener this.startTime = TickDuration.currSystemTick(); } - public void skip() + public void skip(string reason) { // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) { - this.testCase ~= new Element("skipped"); + auto element = new Element("skipped"); + + // FIXME encoding will be fixed in D 2.064 + element.tag.attr["message"] = encode(encode(reason)); + this.testCase ~= element; } } @@ -526,13 +535,13 @@ class XmlReporter : TestListener // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) { - auto failure = new Element("failure"); + auto element = new Element("failure"); string message = "%s %s: %s".format(phase, description(exception), exception.msg); // FIXME encoding will be fixed in D 2.064 - failure.tag.attr["message"] = encode(encode(message)); - this.testCase ~= failure; + element.tag.attr["message"] = encode(encode(message)); + this.testCase ~= element; } } @@ -541,13 +550,13 @@ class XmlReporter : TestListener // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) { - auto error = new Element("error", throwable.info.toString); + auto element = new Element("error", throwable.info.toString); string message = "%s %s: %s".format(phase, description(throwable), throwable.msg); // FIXME encoding will be fixed in D 2.064 - error.tag.attr["message"] = encode(encode(message)); - this.testCase ~= error; + element.tag.attr["message"] = encode(encode(message)); + this.testCase ~= element; } } @@ -560,9 +569,11 @@ class XmlReporter : TestListener public void exit() { + import std.file; + string report = join(this.document.pretty(4), "\n") ~ "\n"; - std.file.write(this.fileName, report); + write(this.fileName, report); } } @@ -571,14 +582,23 @@ class XmlReporter : TestListener */ mixin template UnitTest() { - public static this() + private static this() { + import std.range; + + alias TypeTuple!(__traits(allMembers, typeof(this))) allMembers; + TestClass testClass; + string[] ignoredTests = _annotations!(typeof(this), Ignore, allMembers).dup; - testClass.tests = _memberFunctions!(typeof(this), Test, - __traits(allMembers, typeof(this))).dup; - testClass.ignoredTests = _memberFunctions!(typeof(this), Ignore, - __traits(allMembers, typeof(this))).dup; + testClass.tests = _memberFunctions!(typeof(this), Test, allMembers).dup; + foreach (chunk; chunks(ignoredTests, 2)) + { + string testName = chunk[0]; + string reason = chunk[1]; + + testClass.ignoredTests[testName] = reason; + } static Object create() { @@ -587,32 +607,27 @@ mixin template UnitTest() static void beforeClass(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), BeforeClass, - __traits(allMembers, typeof(this))))); + mixin(_sequence(_memberFunctions!(typeof(this), BeforeClass, allMembers))); } static void before(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), Before, - __traits(allMembers, typeof(this))))); + mixin(_sequence(_memberFunctions!(typeof(this), Before, allMembers))); } static void test(Object o, string name) { - mixin(_choice(_memberFunctions!(typeof(this), Test, - __traits(allMembers, typeof(this))))); + mixin(_choice(_memberFunctions!(typeof(this), Test, allMembers))); } static void after(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), After, - __traits(allMembers, typeof(this))))); + mixin(_sequence(_memberFunctions!(typeof(this), After, allMembers))); } static void afterClass(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), AfterClass, - __traits(allMembers, typeof(this))))); + mixin(_sequence(_memberFunctions!(typeof(this), AfterClass, allMembers))); } testClass.create = &create; @@ -632,9 +647,7 @@ mixin template UnitTest() block ~= "switch (name)\n{\n"; foreach (memberFunction; memberFunctions) - { block ~= `case "` ~ memberFunction ~ `": testObject.` ~ memberFunction ~ "(); break;\n"; - } block ~= "default: break;\n}\n"; return block; } @@ -644,35 +657,65 @@ mixin template UnitTest() string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; foreach (memberFunction; memberFunctions) - { block ~= "testObject." ~ memberFunction ~ "();\n"; - } return block; } - private template _memberFunctions(alias T, alias U, names...) + private template _memberFunctions(alias T, attribute, names...) { static if (names.length == 0) - { immutable(string[]) _memberFunctions = []; - } else - { static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) - && _hasAttribute!(T, names[0], U)) - { - immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, U, names[1 .. $]); - } + && _hasAttribute!(T, names[0], attribute)) + immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, attribute, names[1 .. $]); else - { - immutable(string[]) _memberFunctions = _memberFunctions!(T, U, names[1 .. $]); - } + immutable(string[]) _memberFunctions = _memberFunctions!(T, attribute, names[1 .. $]); + } + + private template _hasAttribute(alias T, string name, attribute) + { + alias TypeTuple!(__traits(getMember, T, name)) member; + alias TypeTuple!(__traits(getAttributes, member)) attributes; + + enum _hasAttribute = staticIndexOf!(attribute, attributes) != -1; + } + + private template _annotations(alias T, attribute, names...) + { + static if (names.length == 0) + immutable(string[]) _annotations = []; + else + { + alias TypeTuple!(__traits(getMember, T, names[0])) member; + alias TypeTuple!(__traits(getAttributes, member)) attributes; + + static if (_canFindValue!(attribute, attributes)) + immutable(string[]) _annotations = [names[0], _findValue!(attribute, attributes).reason] + ~ _annotations!(T, attribute, names[1 .. $]); + else + immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); } } - template _hasAttribute(alias T, string name, attribute) + private template _canFindValue(attribute, T...) + { + static if (T.length == 0) + enum bool _canFindValue = false; + else + static if (is(typeof(T[0]) : attribute)) + enum bool _canFindValue = true; + else + enum bool _canFindValue = _canFindValue!(attribute, T[1 .. $]); + } + + private template _findValue(attribute, T...) { - enum _hasAttribute = staticIndexOf!(attribute, - __traits(getAttributes, __traits(getMember, T, name))) != -1; + static assert(T.length > 0); + + static if (is(typeof(T[0]) : attribute)) + enum _findValue = T[0]; + else + enum _findValue = _findValue!(attribute, T[1 .. $]); } } diff --git a/example.d b/example.d index c45b8f0..03b4ac2 100755 --- a/example.d +++ b/example.d @@ -134,7 +134,7 @@ class TestingThisAndThat // disabled test function @Test - @Ignore + @Ignore("not ready yet") public void failure() { testResult(false); From 8b07dc2f7df2561ba734919984cd774e8e717728 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 28 Sep 2013 20:52:33 +0200 Subject: [PATCH 28/73] tidied up --- README.md | 5 ++--- dunit/diff.d | 4 ++-- dunit/framework.d | 32 +++++++++++++------------------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6d49e9d..a0dc897 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ For example, will not only report the faulty value but will also highlight the difference: - expected: but was: + expected: > but was: > The more general - assertOp!">="(a, b); + assertOp!">="(a, b); // alias assertGreaterThanOrEqual (borrowed from [Issue 4653](http://d.puremagic.com/issues/show_bug.cgi?id=4653)) @@ -56,7 +56,6 @@ will at least report the concrete values in case of a failure: condition (2 >= 3) not satisfied - Together with the expressive name of the test (that's your responsibility) this should be enough information for failures. On the other hand, for violated contracts and other exceptions from deep down the unit under test diff --git a/dunit/diff.d b/dunit/diff.d index 85b0a13..ba843f0 100644 --- a/dunit/diff.d +++ b/dunit/diff.d @@ -31,8 +31,8 @@ Tuple!(string, string) diff(string)(string lhs, string rhs) suffix = suffix[0 .. MAX_LENGTH] ~ "..."; return tuple( - prefix ~ '[' ~ diff[0] ~ ']' ~ suffix, - prefix ~ '[' ~ diff[1] ~ ']' ~ suffix); + prefix ~ '<' ~ diff[0] ~ '>' ~ suffix, + prefix ~ '<' ~ diff[1] ~ '>' ~ suffix); } /// diff --git a/dunit/framework.d b/dunit/framework.d index 49f3a86..28bdf0c 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -200,7 +200,7 @@ body if (ignore || !classSetUp) { - string reason = testClasses[className].ignoredTests[testName]; + string reason = testClasses[className].ignoredTests.get(testName, null); foreach (testListener; testListeners) testListener.skip(reason); @@ -307,7 +307,7 @@ class IssueReporter : TestListener public void skip(string reason) { - writec(Color.onYellow, "I"); + writec(Color.onYellow, "S"); } public void addFailure(string phase, AssertException exception) @@ -389,7 +389,7 @@ class DetailReporter : TestListener public void skip(string reason) { - writec(Color.yellow, " IGNORE: "); + writec(Color.yellow, " SKIP: "); writeln(this.testName); if (!reason.empty) writefln(` "%s"`, reason); @@ -689,33 +689,27 @@ mixin template UnitTest() { alias TypeTuple!(__traits(getMember, T, names[0])) member; alias TypeTuple!(__traits(getAttributes, member)) attributes; + enum index = _indexOfValue!(attribute, attributes); - static if (_canFindValue!(attribute, attributes)) - immutable(string[]) _annotations = [names[0], _findValue!(attribute, attributes).reason] + static if (index != -1) + immutable(string[]) _annotations = [names[0], attributes[index].reason] ~ _annotations!(T, attribute, names[1 .. $]); else immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); } } - private template _canFindValue(attribute, T...) + private template _indexOfValue(attribute, T...) { static if (T.length == 0) - enum bool _canFindValue = false; + enum _indexOfValue = -1; else static if (is(typeof(T[0]) : attribute)) - enum bool _canFindValue = true; + enum _indexOfValue = 0; else - enum bool _canFindValue = _canFindValue!(attribute, T[1 .. $]); - } - - private template _findValue(attribute, T...) - { - static assert(T.length > 0); - - static if (is(typeof(T[0]) : attribute)) - enum _findValue = T[0]; - else - enum _findValue = _findValue!(attribute, T[1 .. $]); + { + enum index = _indexOfValue!(attribute, T[1 .. $]); + enum _indexOfValue = (index == -1) ? -1 : index + 1; + } } } From b1c0bb4c58be3d05fb3d838b770c72adafa19b7c Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 29 Sep 2013 16:14:00 +0200 Subject: [PATCH 29/73] added assertArrayEquals for associative arrays --- README.md | 4 +-- dunit/assertion.d | 68 ++++++++++++++++++++++++++++++++++++----------- dunit/diff.d | 14 +++++----- example.d | 11 +++++++- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a0dc897..822efb2 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Test Results Test results are reported while the tests are run. A "progress bar" is written with a `.` for each passed test, an `F` for each failure, an `E` for each error, -and an `I` (ignore) for each skipped test. +and an `S` for each skipped test. In addition, an XML test report is available that uses the JUnitReport format. The continuous integration tool [Jenkins](http://jenkins-ci.org), for example, @@ -94,7 +94,7 @@ Run the included example to see the xUnit Testing Framework in action: ./example.d -(When you get three failures, one error, and one skip, everything works fine.) +(When you get four failures, one error, and one skip, everything works fine.) Have a look at the debug output of the example in "verbose" style: diff --git a/dunit/assertion.d b/dunit/assertion.d index c0f5c6f..42576d5 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -96,11 +96,11 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, unittest { assertEquals("foo", "foo"); - assertEquals("expected: but was: ", + assertEquals("expected: > but was: >", collectExceptionMsg!AssertException(assertEquals("bar", "baz"))); assertEquals(42, 42); - assertEquals("expected: <[42]> but was: <[24]>", + assertEquals("expected: <<42>> but was: <<24>>", collectExceptionMsg!AssertException(assertEquals(42, 24))); assertEquals(42.0, 42.0); @@ -110,7 +110,7 @@ unittest assertEquals(foo, foo); assertEquals(bar, bar); - assertEquals("expected: <[object.Object]> but was: <[null]>", + assertEquals("expected: <> but was: <>", collectExceptionMsg!AssertException(assertEquals(foo, bar))); } @@ -118,19 +118,19 @@ unittest * Asserts that the arrays are equal. * Throws: AssertException otherwise */ -void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, lazy string msg = null, +void assertArrayEquals(T, U)(T[] expected, U[] actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { string header = (msg.empty) ? null : msg ~ "; "; - foreach (index; 0 .. min(expecteds.length, actuals.length)) + foreach (index; 0 .. min(expected.length, actual.length)) { - assertEquals(expecteds[index], actuals[index], + assertEquals(expected[index], actual[index], header ~ "array mismatch at index " ~ index.to!string, file, line); } - assertEquals(expecteds.length, actuals.length, + assertEquals(expected.length, actual.length, header ~ "array length mismatch", file, line); } @@ -138,18 +138,54 @@ void assertArrayEquals(T, U)(T[] expecteds, U[] actuals, lazy string msg = null, /// unittest { - int[] expecteds = [1, 2, 3]; - double[] actuals = [1, 2, 3]; - - assertArrayEquals(expecteds, actuals); - assertEquals("array mismatch at index 1; expected: <2[]> but was: <2[.3]>", - collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2.3]))); - assertEquals("array length mismatch; expected: <[3]> but was: <[2]>", - collectExceptionMsg!AssertException(assertArrayEquals(expecteds, [1, 2]))); - assertEquals("array mismatch at index 2; expected: <[r]> but was: <[z]>", + double[] expected = [1, 2, 3]; + + assertArrayEquals(expected, [1, 2, 3]); + assertEquals("array mismatch at index 1; expected: <2<>> but was: <2<.3>>", + collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2.3]))); + assertEquals("array length mismatch; expected: <<3>> but was: <<2>>", + collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2]))); + assertEquals("array mismatch at index 2; expected: <> but was: <>", collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); } +/** + * Asserts that the associative arrays are equal. + * Throws: AssertException otherwise + */ +void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + string header = (msg.empty) ? null : msg ~ "; "; + + foreach (key; expected.byKey) + if (key in actual) + { + assertEquals(expected[key], actual[key], + // format string key with double quotes + format(header ~ `array mismatch at key %(%s%)`, [key]), + file, line); + } + + auto difference = setSymmetricDifference(expected.keys.sort, actual.keys.sort); + + assertEmpty(difference, + "key mismatch; difference: %(%s, %)".format(difference)); +} + +/// +unittest +{ + double[string] expected = ["foo": 1, "bar": 2]; + + assertArrayEquals(expected, ["foo": 1, "bar": 2]); + assertEquals(`array mismatch at key "foo"; expected: <<1>> but was: <<2>>`, + collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 2]))); + assertEquals(`key mismatch; difference: "bar"`, + collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 1]))); +} + /** * Asserts that the value is empty. * Throws: AssertException otherwise diff --git a/dunit/diff.d b/dunit/diff.d index ba843f0..3e7e2c2 100644 --- a/dunit/diff.d +++ b/dunit/diff.d @@ -40,14 +40,14 @@ unittest { assert(diff("abc", "abc") == tuple("abc", "abc")); // highlight difference - assert(diff("abc", "Abc") == tuple("[a]bc", "[A]bc")); - assert(diff("abc", "aBc") == tuple("a[b]c", "a[B]c")); - assert(diff("abc", "abC") == tuple("ab[c]", "ab[C]")); - assert(diff("abc", "") == tuple("[abc]", "[]")); - assert(diff("abc", "abbc") == tuple("ab[]c", "ab[b]c")); + assert(diff("abc", "Abc") == tuple("bc", "bc")); + assert(diff("abc", "aBc") == tuple("ac", "ac")); + assert(diff("abc", "abC") == tuple("ab", "ab")); + assert(diff("abc", "") == tuple("", "<>")); + assert(diff("abc", "abbc") == tuple("ab<>c", "abc")); // abbreviate long prefix or suffix assert(diff("_12345678901234567890a", "_12345678901234567890A") - == tuple("...12345678901234567890[a]", "...12345678901234567890[A]")); + == tuple("...12345678901234567890", "...12345678901234567890")); assert(diff("a12345678901234567890_", "A12345678901234567890_") - == tuple("[a]12345678901234567890...", "[A]12345678901234567890...")); + == tuple("12345678901234567890...", "12345678901234567890...")); } diff --git a/example.d b/example.d index 03b4ac2..eb34d73 100755 --- a/example.d +++ b/example.d @@ -1,4 +1,4 @@ -#!/usr/bin/env rdmd +#!/usr/bin/env rdmd -unittest // Copyright Juan Manuel Cabo 2012. // Copyright Mario Kröplin 2013. @@ -38,6 +38,15 @@ class Test assertArrayEquals(expected, actual); } + @Test + public void assertAssocArrayEqualsFailure() + { + string[int] expected = [1: "foo", 2: "bar"]; + string[int] actual = [1: "foo", 2: "baz"]; + + assertArrayEquals(expected, actual); + } + @Test public void assertOpFailure() { From be56f8a4232051ca863939367501012c95ddf1b7 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 29 Sep 2013 18:12:00 +0200 Subject: [PATCH 30/73] improved layout for long or multi-line differences --- dunit/assertion.d | 3 +-- dunit/diff.d | 24 ++++++++++++++++++++++++ dunit/framework.d | 9 +++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/dunit/assertion.d b/dunit/assertion.d index 42576d5..745047b 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -86,9 +86,8 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, return; string header = (msg.empty) ? null : msg ~ "; "; - auto desc = diff(expected.to!string, actual.to!string); - fail(header ~ "expected: <" ~ desc[0] ~ "> but was: <" ~ desc[1] ~ ">", + fail(header ~ description(expected.to!string, actual.to!string), file, line); } diff --git a/dunit/diff.d b/dunit/diff.d index 3e7e2c2..9398b55 100644 --- a/dunit/diff.d +++ b/dunit/diff.d @@ -9,6 +9,30 @@ import std.algorithm; import std.range; import std.typecons; +/** + * Returns a description of the difference between the strings. + */ +string description(string expected, string actual) +{ + const MAX_LENGTH = 20; + auto result = diff(expected, actual); + bool oneLiner = max(result[0].length, result[1].length) <= MAX_LENGTH + && !result[0].canFind("\n", "\r") + && !result[1].canFind("\n", "\r"); + + if (oneLiner) + return "expected: <" ~ result[0] ~ "> but was: <" ~ result[1] ~ ">"; + else + return "expected:\n" ~ result[0] ~ "\nbut was:\n" ~ result[1]; +} + +/// +unittest +{ + assert(description("ab", "Ab") == "expected: <b> but was: <b>"); + assert(description("a\nb", "A\nb") == "expected:\n\nb\nbut was:\n\nb"); +} + /** * Returns a pair of strings that highlight the difference between lhs and rhs. */ diff --git a/dunit/framework.d b/dunit/framework.d index 28bdf0c..aee3ee2 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -392,14 +392,14 @@ class DetailReporter : TestListener writec(Color.yellow, " SKIP: "); writeln(this.testName); if (!reason.empty) - writefln(` "%s"`, reason); + writeln(indent(`"%s"`.format(reason))); } public void addFailure(string phase, AssertException exception) { writec(Color.red, " FAILURE: "); writeln(prettyOrigin(this.testName, phase)); - writefln(" %s: %s", description(exception), exception.msg); + writeln(indent("%s: %s".format(description(exception), exception.msg))); } public void addError(string phase, Throwable throwable) @@ -425,6 +425,11 @@ class DetailReporter : TestListener { // do nothing } + + private string indent(string s, string indent = " ") + { + return s.splitLines(KeepTerminator.yes).map!(line => indent ~ line).join; + } } class ResultReporter : TestListener From 7fe6a38cf9411850a1f05133320e33ffa72ed840 Mon Sep 17 00:00:00 2001 From: linkrope Date: Mon, 21 Oct 2013 22:47:39 +0200 Subject: [PATCH 31/73] fixed template for more restrictive D 2.064 --- dunit/attributes.d | 2 +- dunit/framework.d | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/dunit/attributes.d b/dunit/attributes.d index 88ea3ca..37d5e72 100644 --- a/dunit/attributes.d +++ b/dunit/attributes.d @@ -8,5 +8,5 @@ enum Test; struct Ignore { - string reason = null; + string reason; } diff --git a/dunit/framework.d b/dunit/framework.d index aee3ee2..7b149d5 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -54,12 +54,20 @@ public int dunit_main(string[] args) string report = null; bool verbose = false; - getopt(args, - "filter|f", &filters, - "help|h", &help, - "list|l", &list, - "report", &report, - "verbose|v", &verbose); + try + { + getopt(args, + "filter|f", &filters, + "help|h", &help, + "list|l", &list, + "report", &report, + "verbose|v", &verbose); + } + catch (Exception exception) + { + stderr.writeln("error: ", exception.msg); + return 1; + } if (help) { @@ -129,7 +137,7 @@ public int dunit_main(string[] args) testListeners ~= reporter; runTests(selectedTestNamesByClass, testListeners); - return (reporter.errors > 0) ? 2 : (reporter.failures > 0) ? 1 : 0; + return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } public static void runTests(string[][string] testNamesByClass, TestListener[] testListeners) @@ -691,17 +699,20 @@ mixin template UnitTest() static if (names.length == 0) immutable(string[]) _annotations = []; else - { - alias TypeTuple!(__traits(getMember, T, names[0])) member; - alias TypeTuple!(__traits(getAttributes, member)) attributes; - enum index = _indexOfValue!(attribute, attributes); - - static if (index != -1) - immutable(string[]) _annotations = [names[0], attributes[index].reason] - ~ _annotations!(T, attribute, names[1 .. $]); + static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) + { + alias TypeTuple!(__traits(getMember, T, names[0])) member; + alias TypeTuple!(__traits(getAttributes, member)) attributes; + enum index = _indexOfValue!(attribute, attributes); + + static if (index != -1) + immutable(string[]) _annotations = [names[0], attributes[index].reason] + ~ _annotations!(T, attribute, names[1 .. $]); + else + immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); + } else immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); - } } private template _indexOfValue(attribute, T...) From 227a0e0413c83e1be7fb8ec426b0ae10f125accb Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 26 Jan 2014 17:20:03 +0100 Subject: [PATCH 32/73] updated to D 2.064.2 --- README.md | 2 +- dunit/assertion.d | 38 ++++++++++++++++++++++----- dunit/framework.d | 66 +++++++++++++++++++++++++++++------------------ dunit/package.d | 5 ++++ example.d | 2 +- 5 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 dunit/package.d diff --git a/README.md b/README.md index 822efb2..f2727ab 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. First, I had to fix some issues, but by now the original implementation has been largely revised. -The xUnit Testing Framework is known to work with versions D 2.062 and D 2.063. +The xUnit Testing Framework is known to work with version D 2.064.2. Testing Functions vs. Interactions ---------------------------------- diff --git a/dunit/assertion.d b/dunit/assertion.d index 745047b..9a2db8a 100644 --- a/dunit/assertion.d +++ b/dunit/assertion.d @@ -14,6 +14,7 @@ import std.algorithm; import std.array; import std.conv; import std.string; +import std.traits; version (unittest) import std.exception; @@ -75,12 +76,13 @@ unittest } /** - * Asserts that the values are equal. + * Asserts that the string values are equal. * Throws: AssertException otherwise */ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) + if (isSomeString!T) { if (expected == actual) return; @@ -97,9 +99,31 @@ unittest assertEquals("foo", "foo"); assertEquals("expected: > but was: >", collectExceptionMsg!AssertException(assertEquals("bar", "baz"))); +} +/** + * Asserts that the string values are equal. + * Throws: AssertException otherwise + */ +void assertEquals(T, U)(T expected, U actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) + if (!isSomeString!T) +{ + if (expected == actual) + return; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ "expected: <" ~ expected.to!string ~ "> but was: <"~ actual.to!string ~ ">", + file, line); +} + +/// +unittest +{ assertEquals(42, 42); - assertEquals("expected: <<42>> but was: <<24>>", + assertEquals("expected: <42> but was: <24>", collectExceptionMsg!AssertException(assertEquals(42, 24))); assertEquals(42.0, 42.0); @@ -109,7 +133,7 @@ unittest assertEquals(foo, foo); assertEquals(bar, bar); - assertEquals("expected: <> but was: <>", + assertEquals("expected: but was: ", collectExceptionMsg!AssertException(assertEquals(foo, bar))); } @@ -140,11 +164,11 @@ unittest double[] expected = [1, 2, 3]; assertArrayEquals(expected, [1, 2, 3]); - assertEquals("array mismatch at index 1; expected: <2<>> but was: <2<.3>>", + assertEquals("array mismatch at index 1; expected: <2> but was: <2.3>", collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2.3]))); - assertEquals("array length mismatch; expected: <<3>> but was: <<2>>", + assertEquals("array length mismatch; expected: <3> but was: <2>", collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2]))); - assertEquals("array mismatch at index 2; expected: <> but was: <>", + assertEquals("array mismatch at index 2; expected: but was: ", collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); } @@ -179,7 +203,7 @@ unittest double[string] expected = ["foo": 1, "bar": 2]; assertArrayEquals(expected, ["foo": 1, "bar": 2]); - assertEquals(`array mismatch at key "foo"; expected: <<1>> but was: <<2>>`, + assertEquals(`array mismatch at key "foo"; expected: <1> but was: <2>`, collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 2]))); assertEquals(`key mismatch; difference: "bar"`, collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 1]))); diff --git a/dunit/framework.d b/dunit/framework.d index 7b149d5..aed831c 100644 --- a/dunit/framework.d +++ b/dunit/framework.d @@ -6,8 +6,8 @@ module dunit.framework; -public import dunit.assertion; -public import dunit.attributes; +import dunit.assertion; +import dunit.attributes; import dunit.color; import core.time; @@ -34,6 +34,12 @@ struct TestClass string[] testClassOrder; TestClass[string] testClasses; +struct TestSelection +{ + string className; + string[] testNames; +} + mixin template Main() { int main (string[] args) @@ -84,30 +90,46 @@ public int dunit_main(string[] args) return 0; } - string[][string] selectedTestNamesByClass = null; + TestSelection[] testSelections = null; if (filters is null) - filters = [null]; - - foreach (filter; filters) { foreach (className; testClassOrder) { - foreach (testName; testClasses[className].tests) + string[] testNames = testClasses[className].tests; + + testSelections ~= TestSelection(className, testNames); + } + } + else + { + foreach (filter; filters) + { + foreach (className; testClassOrder) { - string fullyQualifiedName = className ~ '.' ~ testName; + foreach (testName; testClasses[className].tests) + { + string fullyQualifiedName = className ~ '.' ~ testName; - if (match(fullyQualifiedName, filter)) - selectedTestNamesByClass[className] ~= testName; + if (match(fullyQualifiedName, filter)) + { + auto result = testSelections.find!"a.className == b"(className); + + if (result.empty) + testSelections ~= TestSelection(className, [testName]); + else + result.front.testNames ~= testName; + } + } } } } if (list) { - foreach (className; testClassOrder) + foreach (testSelection; testSelections) with (testSelection) { - foreach (testName; selectedTestNamesByClass.get(className, null)) + foreach (testName; testNames) { string fullyQualifiedName = className ~ '.' ~ testName; @@ -136,11 +158,11 @@ public int dunit_main(string[] args) auto reporter = new ResultReporter(); testListeners ~= reporter; - runTests(selectedTestNamesByClass, testListeners); + runTests(testSelections, testListeners); return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } -public static void runTests(string[][string] testNamesByClass, TestListener[] testListeners) +public static void runTests(TestSelection[] testSelections, TestListener[] testListeners) in { assert(all!"a !is null"(testListeners)); @@ -168,11 +190,8 @@ body } } - foreach (className; testClassOrder) + foreach (testSelection; testSelections) with (testSelection) { - if (className !in testNamesByClass) - continue; - foreach (testListener; testListeners) testListener.enterClass(className); @@ -180,7 +199,7 @@ body bool classSetUp = true; // not yet failed // run each @Test of the class - foreach (testName; testNamesByClass[className]) + foreach (testName; testNames) { bool success = true; bool ignore = cast(bool)(testName in testClasses[className].ignoredTests); @@ -537,8 +556,7 @@ class XmlReporter : TestListener { auto element = new Element("skipped"); - // FIXME encoding will be fixed in D 2.064 - element.tag.attr["message"] = encode(encode(reason)); + element.tag.attr["message"] = reason; this.testCase ~= element; } } @@ -552,8 +570,7 @@ class XmlReporter : TestListener string message = "%s %s: %s".format(phase, description(exception), exception.msg); - // FIXME encoding will be fixed in D 2.064 - element.tag.attr["message"] = encode(encode(message)); + element.tag.attr["message"] = message; this.testCase ~= element; } } @@ -567,8 +584,7 @@ class XmlReporter : TestListener string message = "%s %s: %s".format(phase, description(throwable), throwable.msg); - // FIXME encoding will be fixed in D 2.064 - element.tag.attr["message"] = encode(encode(message)); + element.tag.attr["message"] = message; this.testCase ~= element; } } diff --git a/dunit/package.d b/dunit/package.d new file mode 100644 index 0000000..602de62 --- /dev/null +++ b/dunit/package.d @@ -0,0 +1,5 @@ +module dunit; + +public import dunit.assertion; +public import dunit.attributes; +public import dunit.framework; diff --git a/example.d b/example.d index eb34d73..96253c0 100755 --- a/example.d +++ b/example.d @@ -8,7 +8,7 @@ module example; -import dunit.framework; +import dunit; import core.thread; import core.time; import std.stdio; From 45f3a43a5dc57a3dd0ace87b5f428cf9102e8f20 Mon Sep 17 00:00:00 2001 From: Mike Bierlee Date: Sat, 3 May 2014 01:22:11 +0200 Subject: [PATCH 33/73] Change DUB configuration to work with latest version of DUB --- package.json => dub.json | 40 +++++++++++++--------------- {dunit => source/dunit}/assertion.d | 0 {dunit => source/dunit}/attributes.d | 0 {dunit => source/dunit}/color.d | 0 {dunit => source/dunit}/diff.d | 0 {dunit => source/dunit}/framework.d | 0 {dunit => source/dunit}/package.d | 0 7 files changed, 19 insertions(+), 21 deletions(-) rename package.json => dub.json (71%) rename {dunit => source/dunit}/assertion.d (100%) rename {dunit => source/dunit}/attributes.d (100%) rename {dunit => source/dunit}/color.d (100%) rename {dunit => source/dunit}/diff.d (100%) rename {dunit => source/dunit}/framework.d (100%) rename {dunit => source/dunit}/package.d (100%) diff --git a/package.json b/dub.json similarity index 71% rename from package.json rename to dub.json index 7df5f74..2b5ee6a 100644 --- a/package.json +++ b/dub.json @@ -1,21 +1,19 @@ -{ - "name": "dunit", - "description": "xUnit Testing Framework for D", - "copyright": "Copyright © 2013, Mario Kröplin", - "authors": ["Juan Manuel Cabo", "Mario Kröplin"], - "dependencies": {}, - "configurations": - [ - { - "name": "library", - "targetType": "library", - "sourcePaths": ["dunit"] - }, - { - "name": "example", - "targetType": "executable", - "sourceFiles": ["example.d"], - "sourcePaths": ["dunit"] - } - ] -} +{ + "name": "dunit", + "description": "xUnit Testing Framework for D", + "copyright": "Copyright © 2013, Mario Kröplin", + "authors": ["Juan Manuel Cabo", "Mario Kröplin"], + "dependencies": {}, + "configurations": + [ + { + "name": "library", + "targetType": "library" + }, + { + "name": "example", + "targetType": "executable", + "sourceFiles": ["example.d"] + } + ] +} diff --git a/dunit/assertion.d b/source/dunit/assertion.d similarity index 100% rename from dunit/assertion.d rename to source/dunit/assertion.d diff --git a/dunit/attributes.d b/source/dunit/attributes.d similarity index 100% rename from dunit/attributes.d rename to source/dunit/attributes.d diff --git a/dunit/color.d b/source/dunit/color.d similarity index 100% rename from dunit/color.d rename to source/dunit/color.d diff --git a/dunit/diff.d b/source/dunit/diff.d similarity index 100% rename from dunit/diff.d rename to source/dunit/diff.d diff --git a/dunit/framework.d b/source/dunit/framework.d similarity index 100% rename from dunit/framework.d rename to source/dunit/framework.d diff --git a/dunit/package.d b/source/dunit/package.d similarity index 100% rename from dunit/package.d rename to source/dunit/package.d From 098158d920f081d32f0cc9828004ba0fc53241ed Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 26 Jul 2014 23:30:47 +0200 Subject: [PATCH 34/73] tidied up --- README.md | 10 ++++++---- dub.json | 1 + example.d | 2 +- {source => src}/dunit/assertion.d | 0 {source => src}/dunit/attributes.d | 0 {source => src}/dunit/color.d | 0 {source => src}/dunit/diff.d | 0 {source => src}/dunit/framework.d | 0 {source => src}/dunit/package.d | 0 9 files changed, 8 insertions(+), 5 deletions(-) rename {source => src}/dunit/assertion.d (100%) rename {source => src}/dunit/attributes.d (100%) rename {source => src}/dunit/color.d (100%) rename {source => src}/dunit/diff.d (100%) rename {source => src}/dunit/framework.d (100%) rename {source => src}/dunit/package.d (100%) diff --git a/README.md b/README.md index f2727ab..c5925a0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ Testing Functions vs. Interactions D's built-in support for unittests is best suited for testing functions, when the test cases can be expressed as one-liners. -(Have a look at the documented unittests for the `dunit.assertion` functions.) +(Have a look at the documented unittests for the +[`dunit.assertion`](src/dunit/assertion.d) functions.) But you're on your own, when you have to write a lot more code per test case, for example for testing interactions of objects. @@ -90,7 +91,7 @@ test reports, track failures and errors, and even provide trends over time. Examples -------- -Run the included example to see the xUnit Testing Framework in action: +Run the included [example](example.d) to see the xUnit Testing Framework in action: ./example.d @@ -98,7 +99,7 @@ Run the included example to see the xUnit Testing Framework in action: Have a look at the debug output of the example in "verbose" style: - rdmd -debug example.d --verbose + rdmd -debug -Isrc example.d --verbose Or just focus on the issues: @@ -115,7 +116,8 @@ Related Projects - [DMocks-revived](https://github.com/QAston/DMocks-revived): a mock-object framework that allows to mock interfaces or classes - [specd](https://github.com/jostly/specd): - a unit testing framework inspired by [specs2](http://etorreborre.github.io/specs2/) and [ScalaTest](http://www.scalatest.org) + a unit testing framework inspired by [specs2](http://etorreborre.github.io/specs2/) + and [ScalaTest](http://www.scalatest.org) - [DUnit](https://github.com/kalekold/dunit): a toolkit of test assertions and a template mixin to enable mocking - [unit-threaded](https://github.com/atilaneves/unit-threaded): diff --git a/dub.json b/dub.json index 2b5ee6a..c145aee 100644 --- a/dub.json +++ b/dub.json @@ -3,6 +3,7 @@ "description": "xUnit Testing Framework for D", "copyright": "Copyright © 2013, Mario Kröplin", "authors": ["Juan Manuel Cabo", "Mario Kröplin"], + "license" : "BSL-1.0", "dependencies": {}, "configurations": [ diff --git a/example.d b/example.d index 96253c0..ff21cd1 100755 --- a/example.d +++ b/example.d @@ -1,4 +1,4 @@ -#!/usr/bin/env rdmd -unittest +#!/usr/bin/env rdmd -unittest -Isrc // Copyright Juan Manuel Cabo 2012. // Copyright Mario Kröplin 2013. diff --git a/source/dunit/assertion.d b/src/dunit/assertion.d similarity index 100% rename from source/dunit/assertion.d rename to src/dunit/assertion.d diff --git a/source/dunit/attributes.d b/src/dunit/attributes.d similarity index 100% rename from source/dunit/attributes.d rename to src/dunit/attributes.d diff --git a/source/dunit/color.d b/src/dunit/color.d similarity index 100% rename from source/dunit/color.d rename to src/dunit/color.d diff --git a/source/dunit/diff.d b/src/dunit/diff.d similarity index 100% rename from source/dunit/diff.d rename to src/dunit/diff.d diff --git a/source/dunit/framework.d b/src/dunit/framework.d similarity index 100% rename from source/dunit/framework.d rename to src/dunit/framework.d diff --git a/source/dunit/package.d b/src/dunit/package.d similarity index 100% rename from source/dunit/package.d rename to src/dunit/package.d From 2ca210690cae719cc3930b4fd122c9903fa00be7 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 27 Jul 2014 00:40:46 +0200 Subject: [PATCH 35/73] added assertRangeEquals --- README.md | 6 ++-- example.d | 17 ++++----- src/dunit/assertion.d | 81 +++++++++++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index c5925a0..68c98b7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. First, I had to fix some issues, but by now the original implementation has been largely revised. -The xUnit Testing Framework is known to work with version D 2.064.2. +The xUnit Testing Framework is known to work with version D 2.064.2 and D 2.065.0. Testing Functions vs. Interactions ---------------------------------- @@ -63,8 +63,8 @@ violated contracts and other exceptions from deep down the unit under test you may wish for the stack trace. That's why the xUnit Testing Framework distinguishes failures from errors, -and why `dunit.assertion` doesn't use `AssertError` but introduces its own -`AssertException`. +and why [`dunit.assertion`](src/dunit/assertion.d) doesn't use `AssertError` +but introduces its own `AssertException`. User Defined Attributes ----------------------- diff --git a/example.d b/example.d index ff21cd1..fed7eac 100755 --- a/example.d +++ b/example.d @@ -1,7 +1,7 @@ #!/usr/bin/env rdmd -unittest -Isrc // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2013. +// Copyright Mario Kröplin 2014. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -11,6 +11,7 @@ module example; import dunit; import core.thread; import core.time; +import std.range; import std.stdio; /** @@ -30,21 +31,21 @@ class Test } @Test - public void assertArrayEqualsFailure() + public void assertAssocArrayEqualsFailure() { - int[] expected = [0, 1, 1, 2, 3]; - int[] actual = [0, 1, 2, 3]; + string[int] expected = [1: "foo", 2: "bar"]; + string[int] actual = [1: "foo", 2: "baz"]; assertArrayEquals(expected, actual); } @Test - public void assertAssocArrayEqualsFailure() + public void assertRangeEqualsFailure() { - string[int] expected = [1: "foo", 2: "bar"]; - string[int] actual = [1: "foo", 2: "baz"]; + int[] expected = [0, 1, 1, 2]; + auto actual = iota(0, 3); - assertArrayEquals(expected, actual); + assertRangeEquals(expected, actual); } @Test diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 9a2db8a..690eb19 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2013. +// Copyright Mario Kröplin 2014. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -13,6 +13,7 @@ import core.time; import std.algorithm; import std.array; import std.conv; +import std.range; import std.string; import std.traits; @@ -115,7 +116,7 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, string header = (msg.empty) ? null : msg ~ "; "; - fail(header ~ "expected: <" ~ expected.to!string ~ "> but was: <"~ actual.to!string ~ ">", + fail(header ~ format("expected: <%s> but was: <%s>", expected, actual), file, line); } @@ -145,33 +146,11 @@ void assertArrayEquals(T, U)(T[] expected, U[] actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { - string header = (msg.empty) ? null : msg ~ "; "; - - foreach (index; 0 .. min(expected.length, actual.length)) - { - assertEquals(expected[index], actual[index], - header ~ "array mismatch at index " ~ index.to!string, - file, line); - } - assertEquals(expected.length, actual.length, - header ~ "array length mismatch", + assertRangeEquals(expected, actual, + msg, file, line); } -/// -unittest -{ - double[] expected = [1, 2, 3]; - - assertArrayEquals(expected, [1, 2, 3]); - assertEquals("array mismatch at index 1; expected: <2> but was: <2.3>", - collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2.3]))); - assertEquals("array length mismatch; expected: <3> but was: <2>", - collectExceptionMsg!AssertException(assertArrayEquals(expected, [1, 2]))); - assertEquals("array mismatch at index 2; expected: but was: ", - collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); -} - /** * Asserts that the associative arrays are equal. * Throws: AssertException otherwise @@ -187,7 +166,7 @@ void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = nu { assertEquals(expected[key], actual[key], // format string key with double quotes - format(header ~ `array mismatch at key %(%s%)`, [key]), + format(header ~ "mismatch at key %(%s%)", [key]), file, line); } @@ -203,12 +182,56 @@ unittest double[string] expected = ["foo": 1, "bar": 2]; assertArrayEquals(expected, ["foo": 1, "bar": 2]); - assertEquals(`array mismatch at key "foo"; expected: <1> but was: <2>`, + assertEquals(`mismatch at key "foo"; expected: <1> but was: <2>`, collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 2]))); assertEquals(`key mismatch; difference: "bar"`, collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 1]))); } +/** + * Asserts that the ranges are equal. + * Throws: AssertException otherwise + */ +void assertRangeEquals(R1, R2)(R1 expected, R2 actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) + if (isInputRange!R1 && isInputRange!R2 && is(typeof(expected.front == actual.front))) +{ + string header = (msg.empty) ? null : msg ~ "; "; + size_t index = 0; + + for (; !expected.empty && ! actual.empty; ++index, expected.popFront, actual.popFront) + { + assertEquals(expected.front, actual.front, + header ~ format("mismatch at index %s", index), + file, line); + } + assertEmpty(expected, + header ~ format("length mismatch at index %s; ", index) ~ + format("expected: <%s> but was: empty", expected.front), + file, line); + assertEmpty(actual, + header ~ format("length mismatch at index %s; ", index) ~ + format("expected: empty but was: <%s>", actual.front), + file, line); +} + +/// +unittest +{ + double[] expected = [0, 1]; + + assertRangeEquals(expected, [0, 1]); + assertEquals("mismatch at index 1; expected: <1> but was: <1.2>", + collectExceptionMsg!AssertException(assertRangeEquals(expected, [0, 1.2, 3]))); + assertEquals("length mismatch at index 1; expected: <1> but was: empty", + collectExceptionMsg!AssertException(assertRangeEquals(expected, [0]))); + assertEquals("length mismatch at index 2; expected: empty but was: <2>", + collectExceptionMsg!AssertException(assertRangeEquals(expected, [0, 1, 2]))); + assertEquals("mismatch at index 2; expected: but was: ", + collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); +} + /** * Asserts that the value is empty. * Throws: AssertException otherwise @@ -314,7 +337,7 @@ void assertSame(T, U)(T expected, U actual, lazy string msg = null, string header = (msg.empty) ? null : msg ~ "; "; - fail(header ~ "expected same: <" ~ expected.to!string ~ "> was not: <"~ actual.to!string ~ ">", + fail(header ~ format("expected same: <%s> was not: <%s>", expected, actual), file, line); } From 306acd4017d7530b0720e6bd8a06e7998c8a0a92 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 27 Jul 2014 01:04:50 +0200 Subject: [PATCH 36/73] fixed style --- src/dunit/assertion.d | 8 ++++---- src/dunit/framework.d | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 690eb19..a75bf96 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -398,10 +398,10 @@ unittest collectExceptionMsg!AssertException(fail())); } -alias assertOp!">" assertGreaterThan; -alias assertOp!">=" assertGreaterThanOrEqual; -alias assertOp!"<" assertLessThan; -alias assertOp!"<=" assertLessThanOrEqual; +alias assertGreaterThan = assertOp!">"; +alias assertGreaterThanOrEqual = assertOp!">="; +alias assertLessThan = assertOp!"<"; +alias assertLessThanOrEqual = assertOp!"<="; /** * Asserts that the condition (lhs op rhs) is satisfied. diff --git a/src/dunit/framework.d b/src/dunit/framework.d index aed831c..c0439e1 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2013. +// Copyright Mario Kröplin 2014. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -615,7 +615,7 @@ mixin template UnitTest() { import std.range; - alias TypeTuple!(__traits(allMembers, typeof(this))) allMembers; + alias allMembers = TypeTuple!(__traits(allMembers, typeof(this))); TestClass testClass; string[] ignoredTests = _annotations!(typeof(this), Ignore, allMembers).dup; @@ -704,8 +704,8 @@ mixin template UnitTest() private template _hasAttribute(alias T, string name, attribute) { - alias TypeTuple!(__traits(getMember, T, name)) member; - alias TypeTuple!(__traits(getAttributes, member)) attributes; + alias member = TypeTuple!(__traits(getMember, T, name)); + alias attributes = TypeTuple!(__traits(getAttributes, member)); enum _hasAttribute = staticIndexOf!(attribute, attributes) != -1; } @@ -717,8 +717,8 @@ mixin template UnitTest() else static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) { - alias TypeTuple!(__traits(getMember, T, names[0])) member; - alias TypeTuple!(__traits(getAttributes, member)) attributes; + alias member = TypeTuple!(__traits(getMember, T, names[0])); + alias attributes = TypeTuple!(__traits(getAttributes, member)); enum index = _indexOfValue!(attribute, attributes); static if (index != -1) From d711ec64783ed3224f5869414aaae94ba8cfdac5 Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 30 Jul 2014 22:25:32 +0200 Subject: [PATCH 37/73] workaround for uniform constructor syntax to be introduced in D 2.066 mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()") will now even compile for scalar types --- src/dunit/framework.d | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index c0439e1..8250248 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -692,10 +692,13 @@ mixin template UnitTest() private template _memberFunctions(alias T, attribute, names...) { + import std.traits; + static if (names.length == 0) immutable(string[]) _memberFunctions = []; else - static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) + static if (isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) + && __traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) && _hasAttribute!(T, names[0], attribute)) immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, attribute, names[1 .. $]); else @@ -712,10 +715,13 @@ mixin template UnitTest() private template _annotations(alias T, attribute, names...) { + import std.traits; + static if (names.length == 0) immutable(string[]) _annotations = []; else - static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) + static if (isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) + && __traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) { alias member = TypeTuple!(__traits(getMember, T, names[0])); alias attributes = TypeTuple!(__traits(getAttributes, member)); From 2fbcb53e74d70c0c8e23a9898f78625a5db9d993 Mon Sep 17 00:00:00 2001 From: linkrope Date: Thu, 31 Jul 2014 22:31:10 +0200 Subject: [PATCH 38/73] disambiguate package name --- dub.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dub.json b/dub.json index c145aee..86304f0 100644 --- a/dub.json +++ b/dub.json @@ -1,5 +1,5 @@ { - "name": "dunit", + "name": "d-unit", "description": "xUnit Testing Framework for D", "copyright": "Copyright © 2013, Mario Kröplin", "authors": ["Juan Manuel Cabo", "Mario Kröplin"], From 135d1d005712eb9c7dfead454dc432bbf13d4050 Mon Sep 17 00:00:00 2001 From: linkrope Date: Mon, 25 Aug 2014 17:19:33 +0200 Subject: [PATCH 39/73] fixed order of conditions to avoid problems with private protection --- src/dunit/framework.d | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 8250248..1ca00aa 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -697,8 +697,8 @@ mixin template UnitTest() static if (names.length == 0) immutable(string[]) _memberFunctions = []; else - static if (isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) - && __traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) + static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) + && isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) && _hasAttribute!(T, names[0], attribute)) immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, attribute, names[1 .. $]); else @@ -720,8 +720,8 @@ mixin template UnitTest() static if (names.length == 0) immutable(string[]) _annotations = []; else - static if (isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) - && __traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()"))) + static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) + && isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0]))) { alias member = TypeTuple!(__traits(getMember, T, names[0])); alias attributes = TypeTuple!(__traits(getAttributes, member)); From 438ed5938abf8ba4b411736bd40da3394436144f Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 7 Oct 2014 23:27:19 +0200 Subject: [PATCH 40/73] reworked UDA templates --- README.md | 2 +- src/dunit/framework.d | 173 +++++++++++++++++++++++------------------- 2 files changed, 98 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 68c98b7..1c2928c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. First, I had to fix some issues, but by now the original implementation has been largely revised. -The xUnit Testing Framework is known to work with version D 2.064.2 and D 2.065.0. +The xUnit Testing Framework is known to work with version D 2.065.0 and D 2.066.0. Testing Functions vs. Interactions ---------------------------------- diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 1ca00aa..efbe380 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -21,7 +21,7 @@ public import std.typetuple; struct TestClass { string[] tests; - string[string] ignoredTests; + Ignore[string] ignores; Object function() create; void function(Object o) beforeClass; @@ -48,6 +48,17 @@ mixin template Main() } } +const string USAGE = `Usage: %s [options] +Run the functions with @Test attribute of all classes that mix in UnitTest. + +Options: + -f, --filter REGEX Select test functions matching the regular expression + Multiple selections are processed in sequence + -h, --help Display usage information, then exit + -l, --list Display the test functions, then exit + --report FILE Write JUnit-style XML test report + -v, --verbose Display more information as the tests are run`; + public int dunit_main(string[] args) { import std.getopt; @@ -77,16 +88,7 @@ public int dunit_main(string[] args) if (help) { - writefln("Usage: %s [options]", args.empty ? "testrunner" : baseName(args[0])); - writeln("Run the functions with @Test attribute of all classes that mix in UnitTest."); - writeln(); - writeln("Options:"); - writeln(" -f, --filter REGEX Select test functions matching the regular expression"); - writeln(" Multiple selections are processed in sequence"); - writeln(" -h, --help Display usage information, then exit"); - writeln(" -l, --list Display the test functions, then exit"); - writeln(" --report FILE Write JUnit-style XML test report"); - writeln(" -v, --verbose Display more information as the tests are run"); + writefln(USAGE, args.empty ? "testrunner" : baseName(args[0])); return 0; } @@ -202,7 +204,7 @@ body foreach (testName; testNames) { bool success = true; - bool ignore = cast(bool)(testName in testClasses[className].ignoredTests); + bool ignore = cast(bool)(testName in testClasses[className].ignores); foreach (testListener; testListeners) testListener.enterTest(testName); @@ -227,7 +229,7 @@ body if (ignore || !classSetUp) { - string reason = testClasses[className].ignoredTests.get(testName, null); + string reason = testClasses[className].ignores.get(testName, Ignore.init).reason; foreach (testListener; testListeners) testListener.skip(reason); @@ -613,21 +615,10 @@ mixin template UnitTest() { private static this() { - import std.range; - - alias allMembers = TypeTuple!(__traits(allMembers, typeof(this))); - TestClass testClass; - string[] ignoredTests = _annotations!(typeof(this), Ignore, allMembers).dup; - testClass.tests = _memberFunctions!(typeof(this), Test, allMembers).dup; - foreach (chunk; chunks(ignoredTests, 2)) - { - string testName = chunk[0]; - string reason = chunk[1]; - - testClass.ignoredTests[testName] = reason; - } + testClass.tests = _members!(typeof(this), Test); + testClass.ignores = _attributes!(typeof(this), Ignore); static Object create() { @@ -636,27 +627,27 @@ mixin template UnitTest() static void beforeClass(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), BeforeClass, allMembers))); + mixin(_sequence(_members!(typeof(this), BeforeClass))); } static void before(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), Before, allMembers))); + mixin(_sequence(_members!(typeof(this), Before))); } static void test(Object o, string name) { - mixin(_choice(_memberFunctions!(typeof(this), Test, allMembers))); + mixin(_choice(_members!(typeof(this), Test))); } static void after(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), After, allMembers))); + mixin(_sequence(_members!(typeof(this), After))); } static void afterClass(Object o) { - mixin(_sequence(_memberFunctions!(typeof(this), AfterClass, allMembers))); + mixin(_sequence(_members!(typeof(this), AfterClass))); } testClass.create = &create; @@ -670,7 +661,7 @@ mixin template UnitTest() testClasses[this.classinfo.name] = testClass; } - private static string _choice(const string[] memberFunctions) + private static string _choice(in string[] memberFunctions) { string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; @@ -681,7 +672,7 @@ mixin template UnitTest() return block; } - private static string _sequence(const string[] memberFunctions) + private static string _sequence(in string[] memberFunctions) { string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; @@ -690,64 +681,94 @@ mixin template UnitTest() return block; } - private template _memberFunctions(alias T, attribute, names...) + template _members(T, Attribute) { - import std.traits; + static string[] helper() + { + string[] members; + + foreach (name; __traits(allMembers, T)) + { + static if (__traits(compiles, __traits(getMember, T, name))) + { + import std.typecons; - static if (names.length == 0) - immutable(string[]) _memberFunctions = []; - else - static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) - && isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0])) - && _hasAttribute!(T, names[0], attribute)) - immutable(string[]) _memberFunctions = [names[0]] ~ _memberFunctions!(T, attribute, names[1 .. $]); - else - immutable(string[]) _memberFunctions = _memberFunctions!(T, attribute, names[1 .. $]); + alias member = TypeTuple!(__traits(getMember, T, name)); + + static if (__traits(compiles, _hasAttribute!(member, Attribute))) + { + static if (_hasAttribute!(member, Attribute)) + members ~= name; + } + } + } + return members; + } + + enum _members = helper; } - private template _hasAttribute(alias T, string name, attribute) + template _attributes(T, Attribute) { - alias member = TypeTuple!(__traits(getMember, T, name)); - alias attributes = TypeTuple!(__traits(getAttributes, member)); + static Attribute[string] helper() + { + Attribute[string] attributes; + + foreach (name; __traits(allMembers, T)) + { + static if (__traits(compiles, __traits(getMember, T, name))) + { + import std.typecons; + + alias member = TypeTuple!(__traits(getMember, T, name)); + + static if (__traits(compiles, _hasAttribute!(member, Attribute))) + { + static if (_hasAttribute!(member, Attribute)) + attributes[name] = _findAttribute!(member, Attribute); + } + } + } + return attributes; + } - enum _hasAttribute = staticIndexOf!(attribute, attributes) != -1; + enum _attributes = helper; } - private template _annotations(alias T, attribute, names...) + template _findAttribute(alias member, Attribute) { - import std.traits; - - static if (names.length == 0) - immutable(string[]) _annotations = []; - else - static if (__traits(compiles, mixin("(new " ~ T.stringof ~ "())." ~ names[0] ~ "()")) - && isSomeFunction!(mixin(T.stringof ~ '.' ~ names[0]))) + static auto helper() + { + static if (__traits(compiles, __traits(getAttributes, member))) { - alias member = TypeTuple!(__traits(getMember, T, names[0])); - alias attributes = TypeTuple!(__traits(getAttributes, member)); - enum index = _indexOfValue!(attribute, attributes); - - static if (index != -1) - immutable(string[]) _annotations = [names[0], attributes[index].reason] - ~ _annotations!(T, attribute, names[1 .. $]); - else - immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); + foreach (attribute; __traits(getAttributes, member)) + { + static if (is(attribute == Attribute)) + return Attribute.init; + static if (is(typeof(attribute) == Attribute)) + return attribute; + } } - else - immutable(string[]) _annotations = _annotations!(T, attribute, names[1 .. $]); + assert(0); + } + + enum _findAttribute = helper; } - private template _indexOfValue(attribute, T...) + template _hasAttribute(alias member, Attribute) { - static if (T.length == 0) - enum _indexOfValue = -1; - else - static if (is(typeof(T[0]) : attribute)) - enum _indexOfValue = 0; - else + static bool helper() + { + foreach (attribute; __traits(getAttributes, member)) { - enum index = _indexOfValue!(attribute, T[1 .. $]); - enum _indexOfValue = (index == -1) ? -1 : index + 1; + static if (is(attribute == Attribute)) + return true; + static if (is(typeof(attribute) == Attribute)) + return true; } + return false; + } + + enum bool _hasAttribute = helper; } } From 5fde91f45acba32312cab3de21979f5694c7b1f1 Mon Sep 17 00:00:00 2001 From: linkrope Date: Thu, 19 Feb 2015 23:47:33 +0100 Subject: [PATCH 41/73] improved test robustness --- .travis.yml | 1 + src/dunit/assertion.d | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..62d0c9e --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: d diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index a75bf96..cb335b1 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -447,7 +447,7 @@ unittest * Throws: AssertException when the probe fails to become true before timeout */ public static void assertEventually(bool delegate() probe, - Duration timeout = msecs(500), Duration delay = msecs(10), + Duration timeout = 500.msecs, Duration delay = 10.msecs, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) @@ -468,7 +468,7 @@ public static void assertEventually(bool delegate() probe, /// unittest { - assertEventually({ static count = 0; return ++count > 42; }); + assertEventually({ static count = 0; return ++count > 23; }); assertEquals("timed out", collectExceptionMsg!AssertException(assertEventually({ return false; }))); From c5dde81c553133c57c1200297c0e4238c2829744 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 20 Feb 2015 00:00:04 +0100 Subject: [PATCH 42/73] removed obsolete statement --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1c2928c..d0345ce 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. First, I had to fix some issues, but by now the original implementation has been largely revised. -The xUnit Testing Framework is known to work with version D 2.065.0 and D 2.066.0. - Testing Functions vs. Interactions ---------------------------------- From 89971b708c707e13e82bafaceca3bfb160ddd7e5 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 20 Feb 2015 00:16:12 +0100 Subject: [PATCH 43/73] added Travis CI status --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d0345ce..5432787 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ xUnit Testing Framework for D ============================= +[![Build Status](https://travis-ci.org/linkrope/dunit.svg?branch=master)](https://travis-ci.org/linkrope/dunit) + This is a simple implementation of the xUnit Testing Framework for the [D Programming Language](http://dlang.org). Being based on [JUnit](http://junit.org) it allows to organize tests From ad0d83e7968c7c1112fe8023160bcc393e569018 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 11 Mar 2015 21:34:27 +0100 Subject: [PATCH 44/73] Replace .sort property with std.algorithm.sort --- src/dunit/assertion.d | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index cb335b1..5d36e1b 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -170,7 +170,8 @@ void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = nu file, line); } - auto difference = setSymmetricDifference(expected.keys.sort, actual.keys.sort); + import std.algorithm: sort; + auto difference = setSymmetricDifference(expected.keys.sort(), actual.keys.sort()); assertEmpty(difference, "key mismatch; difference: %(%s, %)".format(difference)); From f6b78e0e350f144dec1673f0fc14df268ee9db9e Mon Sep 17 00:00:00 2001 From: = Date: Wed, 11 Mar 2015 21:37:56 +0100 Subject: [PATCH 45/73] Replace .sort property with std.algorithm.sort --- src/dunit/assertion.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 5d36e1b..ee61276 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -170,7 +170,7 @@ void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = nu file, line); } - import std.algorithm: sort; + import std.algorithm: sort; auto difference = setSymmetricDifference(expected.keys.sort(), actual.keys.sort()); assertEmpty(difference, From e7ff7266b94e6bf36a214218a3c6f1ca729f4155 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 22 May 2015 15:58:19 +0200 Subject: [PATCH 46/73] changed to take arrays by const --- src/dunit/assertion.d | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index ee61276..0a0bb5d 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -142,7 +142,7 @@ unittest * Asserts that the arrays are equal. * Throws: AssertException otherwise */ -void assertArrayEquals(T, U)(T[] expected, U[] actual, lazy string msg = null, +void assertArrayEquals(T, U)(in T[] expected, in U[] actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -155,7 +155,7 @@ void assertArrayEquals(T, U)(T[] expected, U[] actual, lazy string msg = null, * Asserts that the associative arrays are equal. * Throws: AssertException otherwise */ -void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = null, +void assertArrayEquals(T, U, V)(in T[V] expected, in U[V] actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { @@ -170,7 +170,6 @@ void assertArrayEquals(T, U, V)(T[V] expected, U[V] actual, lazy string msg = nu file, line); } - import std.algorithm: sort; auto difference = setSymmetricDifference(expected.keys.sort(), actual.keys.sort()); assertEmpty(difference, From de60fb624df82f66fc37d2130ce2ba8a73dc59bb Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 31 May 2015 16:55:06 +0200 Subject: [PATCH 47/73] changed to use GetoptResult --- src/dunit/framework.d | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index efbe380..d29093d 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2014. +// Copyright Mario Kröplin 2015. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -48,47 +48,37 @@ mixin template Main() } } -const string USAGE = `Usage: %s [options] -Run the functions with @Test attribute of all classes that mix in UnitTest. - -Options: - -f, --filter REGEX Select test functions matching the regular expression - Multiple selections are processed in sequence - -h, --help Display usage information, then exit - -l, --list Display the test functions, then exit - --report FILE Write JUnit-style XML test report - -v, --verbose Display more information as the tests are run`; - public int dunit_main(string[] args) { import std.getopt; import std.path; import std.regex; + GetoptResult result; string[] filters = null; - bool help = false; bool list = false; string report = null; bool verbose = false; try { - getopt(args, - "filter|f", &filters, - "help|h", &help, - "list|l", &list, - "report", &report, - "verbose|v", &verbose); + result = getopt(args, + "filter|f", "Select test functions matching the regular expression", &filters, + "list|l", "Display the test functions, then exit", &list, + "report", "Write JUnit-style XML test report", &report, + "verbose|v", "Display more information as the tests are run", &verbose); } - catch (Exception exception) + catch (GetOptException exception) { stderr.writeln("error: ", exception.msg); return 1; } - if (help) + if (result.helpWanted) { - writefln(USAGE, args.empty ? "testrunner" : baseName(args[0])); + writefln("Usage: %s [options]", args.empty ? "testrunner" : baseName(args[0])); + writeln("Run the functions with @Test attribute of all classes that mix in UnitTest."); + defaultGetoptPrinter("Options:", result.options); return 0; } @@ -115,12 +105,12 @@ public int dunit_main(string[] args) if (match(fullyQualifiedName, filter)) { - auto result = testSelections.find!"a.className == b"(className); + auto foundTestSelections = testSelections.find!"a.className == b"(className); - if (result.empty) + if (foundTestSelections.empty) testSelections ~= TestSelection(className, [testName]); else - result.front.testNames ~= testName; + foundTestSelections.front.testNames ~= testName; } } } From 4f4a74be394b6137b4b983ccd91115c5b156f01b Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 3 Jul 2015 19:16:05 +0200 Subject: [PATCH 48/73] changed to run unittest functions explicitly --- src/dunit/framework.d | 75 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index d29093d..6d8a7a8 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -10,6 +10,7 @@ import dunit.assertion; import dunit.attributes; import dunit.color; +import core.runtime; import core.time; import std.algorithm; import std.array; @@ -40,6 +41,11 @@ struct TestSelection string[] testNames; } +shared static this() +{ + Runtime.moduleUnitTester = () => true; +} + mixin template Main() { int main (string[] args) @@ -68,7 +74,7 @@ public int dunit_main(string[] args) "report", "Write JUnit-style XML test report", &report, "verbose|v", "Display more information as the tests are run", &verbose); } - catch (GetOptException exception) + catch (Exception exception) { stderr.writeln("error: ", exception.msg); return 1; @@ -131,8 +137,6 @@ public int dunit_main(string[] args) return 0; } - TestListener[] testListeners = null; - if (verbose) { testListeners ~= new DetailReporter(); @@ -150,10 +154,59 @@ public int dunit_main(string[] args) auto reporter = new ResultReporter(); testListeners ~= reporter; + + if (filters is null) + { + Runtime.moduleUnitTester = &moduleUnitTester; + + runModuleUnitTests; + } + runTests(testSelections, testListeners); return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } +private bool moduleUnitTester() +{ + foreach (moduleInfo; ModuleInfo) + { + if (moduleInfo) + { + auto unitTest = moduleInfo.unitTest; + + if (unitTest) + { + bool success; + + foreach (testListener; testListeners) + testListener.enterClass(moduleInfo.name); + foreach (testListener; testListeners) + testListener.enterTest("unittest"); + try + { + unitTest(); + success = true; + } + catch (AssertException exception) + { + foreach (testListener; testListeners) + testListener.addFailure(null, exception); + success = false; + } + catch (Throwable throwable) + { + foreach (testListener; testListeners) + testListener.addError(null, throwable); + success = false; + } + foreach (testListener; testListeners) + testListener.exitTest(success); + } + } + } + return true; +} + public static void runTests(TestSelection[] testSelections, TestListener[] testListeners) in { @@ -252,6 +305,8 @@ body testListener.exit(); } +private __gshared TestListener[] testListeners = null; + interface TestListener { public void enterClass(string className); @@ -676,7 +731,7 @@ mixin template UnitTest() static string[] helper() { string[] members; - + foreach (name; __traits(allMembers, T)) { static if (__traits(compiles, __traits(getMember, T, name))) @@ -684,7 +739,7 @@ mixin template UnitTest() import std.typecons; alias member = TypeTuple!(__traits(getMember, T, name)); - + static if (__traits(compiles, _hasAttribute!(member, Attribute))) { static if (_hasAttribute!(member, Attribute)) @@ -694,7 +749,7 @@ mixin template UnitTest() } return members; } - + enum _members = helper; } @@ -703,15 +758,15 @@ mixin template UnitTest() static Attribute[string] helper() { Attribute[string] attributes; - + foreach (name; __traits(allMembers, T)) { static if (__traits(compiles, __traits(getMember, T, name))) { import std.typecons; - + alias member = TypeTuple!(__traits(getMember, T, name)); - + static if (__traits(compiles, _hasAttribute!(member, Attribute))) { static if (_hasAttribute!(member, Attribute)) @@ -758,7 +813,7 @@ mixin template UnitTest() } return false; } - + enum bool _hasAttribute = helper; } } From 7b388f6cc5130607a0f5a949360bf2495ba004e7 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 4 Jul 2015 23:29:37 +0200 Subject: [PATCH 49/73] changed to run unittest functions like @Test functions --- src/dunit/framework.d | 200 +++++++++++++++++++----------------------- 1 file changed, 89 insertions(+), 111 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 6d8a7a8..722074b 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -21,29 +21,24 @@ public import std.typetuple; struct TestClass { + string name; string[] tests; Ignore[string] ignores; Object function() create; void function(Object o) beforeClass; void function(Object o) before; - void function(Object o, string testName) test; + void delegate(Object o, string test) test; void function(Object o) after; void function(Object o) afterClass; } -string[] testClassOrder; -TestClass[string] testClasses; +TestClass[] testClasses; struct TestSelection { - string className; - string[] testNames; -} - -shared static this() -{ - Runtime.moduleUnitTester = () => true; + TestClass testClass; + string[] tests; } mixin template Main() @@ -88,35 +83,33 @@ public int dunit_main(string[] args) return 0; } + testClasses = unitTestFunctions ~ testClasses; + TestSelection[] testSelections = null; if (filters is null) { - foreach (className; testClassOrder) - { - string[] testNames = testClasses[className].tests; - - testSelections ~= TestSelection(className, testNames); - } + foreach (testClass; testClasses) + testSelections ~= TestSelection(testClass, testClass.tests); } else { foreach (filter; filters) { - foreach (className; testClassOrder) + foreach (testClass; testClasses) { - foreach (testName; testClasses[className].tests) + foreach (test; testClass.tests) { - string fullyQualifiedName = className ~ '.' ~ testName; + string fullyQualifiedName = testClass.name ~ '.' ~ test; if (match(fullyQualifiedName, filter)) { - auto foundTestSelections = testSelections.find!"a.className == b"(className); + auto foundTestSelections = testSelections.find!"a.testClass.name == b"(testClass.name); if (foundTestSelections.empty) - testSelections ~= TestSelection(className, [testName]); + testSelections ~= TestSelection(testClass, [test]); else - foundTestSelections.front.testNames ~= testName; + foundTestSelections.front.tests ~= test; } } } @@ -127,9 +120,9 @@ public int dunit_main(string[] args) { foreach (testSelection; testSelections) with (testSelection) { - foreach (testName; testNames) + foreach (test; tests) { - string fullyQualifiedName = className ~ '.' ~ testName; + string fullyQualifiedName = testClass.name ~ '.' ~ test; writeln(fullyQualifiedName); } @@ -154,59 +147,10 @@ public int dunit_main(string[] args) auto reporter = new ResultReporter(); testListeners ~= reporter; - - if (filters is null) - { - Runtime.moduleUnitTester = &moduleUnitTester; - - runModuleUnitTests; - } - runTests(testSelections, testListeners); return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } -private bool moduleUnitTester() -{ - foreach (moduleInfo; ModuleInfo) - { - if (moduleInfo) - { - auto unitTest = moduleInfo.unitTest; - - if (unitTest) - { - bool success; - - foreach (testListener; testListeners) - testListener.enterClass(moduleInfo.name); - foreach (testListener; testListeners) - testListener.enterTest("unittest"); - try - { - unitTest(); - success = true; - } - catch (AssertException exception) - { - foreach (testListener; testListeners) - testListener.addFailure(null, exception); - success = false; - } - catch (Throwable throwable) - { - foreach (testListener; testListeners) - testListener.addError(null, throwable); - success = false; - } - foreach (testListener; testListeners) - testListener.exitTest(success); - } - } - } - return true; -} - public static void runTests(TestSelection[] testSelections, TestListener[] testListeners) in { @@ -238,19 +182,19 @@ body foreach (testSelection; testSelections) with (testSelection) { foreach (testListener; testListeners) - testListener.enterClass(className); + testListener.enterClass(testClass.name); Object testObject = null; bool classSetUp = true; // not yet failed // run each @Test of the class - foreach (testName; testNames) + foreach (test; tests) { bool success = true; - bool ignore = cast(bool)(testName in testClasses[className].ignores); + bool ignore = cast(bool)(test in testClass.ignores); foreach (testListener; testListeners) - testListener.enterTest(testName); + testListener.enterTest(test); scope (exit) foreach (testListener; testListeners) testListener.exitTest(success); @@ -261,18 +205,18 @@ body if (classSetUp) { classSetUp = tryRun("this", - { testObject = testClasses[className].create(); }); + { testObject = testClass.create(); }); } if (classSetUp) { classSetUp = tryRun("@BeforeClass", - { testClasses[className].beforeClass(testObject); }); + { testClass.beforeClass(testObject); }); } } if (ignore || !classSetUp) { - string reason = testClasses[className].ignores.get(testName, Ignore.init).reason; + string reason = testClass.ignores.get(test, Ignore.init).reason; foreach (testListener; testListeners) testListener.skip(reason); @@ -281,15 +225,15 @@ body } success = tryRun("@Before", - { testClasses[className].before(testObject); }); + { testClass.before(testObject); }); if (success) { success = tryRun("@Test", - { testClasses[className].test(testObject, testName); }); + { testClass.test(testObject, test); }); // run @After even if @Test failed success = tryRun("@After", - { testClasses[className].after(testObject); }) + { testClass.after(testObject); }) && success; } } @@ -297,7 +241,7 @@ body if (testObject !is null && classSetUp) { tryRun("@AfterClass", - { testClasses[className].afterClass(testObject); }); + { testClass.afterClass(testObject); }); } } @@ -310,16 +254,16 @@ private __gshared TestListener[] testListeners = null; interface TestListener { public void enterClass(string className); - public void enterTest(string testName); + public void enterTest(string test); public void skip(string reason); public void addFailure(string phase, AssertException exception); public void addError(string phase, Throwable throwable); public void exitTest(bool success); public void exit(); - public static string prettyOrigin(string className, string testName, string phase) + public static string prettyOrigin(string className, string test, string phase) { - string origin = prettyOrigin(testName, phase); + string origin = prettyOrigin(test, phase); if (origin.startsWith('@')) return className ~ origin; @@ -327,18 +271,18 @@ interface TestListener return className ~ '.' ~ origin; } - public static string prettyOrigin(string testName, string phase) + public static string prettyOrigin(string test, string phase) { switch (phase) { case "@Test": - return testName; + return test; case "this": case "@BeforeClass": case "@AfterClass": return phase; default: - return testName ~ phase; + return test ~ phase; } } @@ -359,7 +303,7 @@ class IssueReporter : TestListener private struct Issue { string testClass; - string testName; + string test; string phase; Throwable throwable; } @@ -367,16 +311,16 @@ class IssueReporter : TestListener private Issue[] failures = null; private Issue[] errors = null; private string className; - private string testName; + private string test; public void enterClass(string className) { this.className = className; } - public void enterTest(string testName) + public void enterTest(string test) { - this.testName = testName; + this.test = test; } public void skip(string reason) @@ -386,13 +330,13 @@ class IssueReporter : TestListener public void addFailure(string phase, AssertException exception) { - this.failures ~= Issue(this.className, this.testName, phase, exception); + this.failures ~= Issue(this.className, this.test, phase, exception); writec(Color.onRed, "F"); } public void addError(string phase, Throwable throwable) { - this.errors ~= Issue(this.className, this.testName, phase, throwable); + this.errors ~= Issue(this.className, this.test, phase, throwable); writec(Color.onRed, "E"); } @@ -418,7 +362,7 @@ class IssueReporter : TestListener foreach (i, issue; this.errors) { writefln("%d) %s", i + 1, - prettyOrigin(issue.testClass, issue.testName, issue.phase)); + prettyOrigin(issue.testClass, issue.test, issue.phase)); writeln(issue.throwable.toString); writeln("----------------"); } @@ -438,7 +382,7 @@ class IssueReporter : TestListener Throwable throwable = issue.throwable; writefln("%d) %s", i + 1, - prettyOrigin(issue.testClass, issue.testName, issue.phase)); + prettyOrigin(issue.testClass, issue.test, issue.phase)); writefln("%s: %s", description(throwable), throwable.msg); } } @@ -447,7 +391,7 @@ class IssueReporter : TestListener class DetailReporter : TestListener { - private string testName; + private string test; private TickDuration startTime; public void enterClass(string className) @@ -455,16 +399,16 @@ class DetailReporter : TestListener writeln(className); } - public void enterTest(string testName) + public void enterTest(string test) { - this.testName = testName; + this.test = test; this.startTime = TickDuration.currSystemTick(); } public void skip(string reason) { writec(Color.yellow, " SKIP: "); - writeln(this.testName); + writeln(this.test); if (!reason.empty) writeln(indent(`"%s"`.format(reason))); } @@ -472,14 +416,14 @@ class DetailReporter : TestListener public void addFailure(string phase, AssertException exception) { writec(Color.red, " FAILURE: "); - writeln(prettyOrigin(this.testName, phase)); + writeln(prettyOrigin(this.test, phase)); writeln(indent("%s: %s".format(description(exception), exception.msg))); } public void addError(string phase, Throwable throwable) { writec(Color.red, " ERROR: "); - writeln(prettyOrigin(this.testName, phase)); + writeln(prettyOrigin(this.test, phase)); writeln(" ", throwable.toString); writeln("----------------"); } @@ -491,7 +435,7 @@ class DetailReporter : TestListener double elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0; writec(Color.green, " OK: "); - writefln("%6.2f ms %s", elapsed, this.testName); + writefln("%6.2f ms %s", elapsed, this.test); } } @@ -518,7 +462,7 @@ class ResultReporter : TestListener // do nothing } - public void enterTest(string testName) + public void enterTest(string test) { ++this.tests; } @@ -587,11 +531,11 @@ class XmlReporter : TestListener this.className = className; } - public void enterTest(string testName) + public void enterTest(string test) { this.testCase = new Element("testcase"); this.testCase.tag.attr["classname"] = this.className; - this.testCase.tag.attr["name"] = testName; + this.testCase.tag.attr["name"] = test; this.testSuite ~= this.testCase; this.startTime = TickDuration.currSystemTick(); } @@ -653,6 +597,40 @@ class XmlReporter : TestListener } } +shared static this() +{ + Runtime.moduleUnitTester = () => true; +} + +private TestClass[] unitTestFunctions() +{ + TestClass[] testClasses = null; + TestClass testClass; + + testClass.tests = ["unittest"]; + testClass.create = () => null; + testClass.beforeClass = (o) {}; + testClass.before = (o) {}; + testClass.after = (o) {}; + testClass.afterClass = (o) {}; + + foreach (moduleInfo; ModuleInfo) + { + if (moduleInfo) + { + auto unitTest = moduleInfo.unitTest; + + if (unitTest) + { + testClass.name = moduleInfo.name; + testClass.test = (o, test) { unitTest(); }; + testClasses ~= testClass; + } + } + } + return testClasses; +} + /** * Registers a class as a unit test. */ @@ -662,6 +640,7 @@ mixin template UnitTest() { TestClass testClass; + testClass.name = this.classinfo.name; testClass.tests = _members!(typeof(this), Test); testClass.ignores = _attributes!(typeof(this), Ignore); @@ -680,7 +659,7 @@ mixin template UnitTest() mixin(_sequence(_members!(typeof(this), Before))); } - static void test(Object o, string name) + void test(Object o, string name) { mixin(_choice(_members!(typeof(this), Test))); } @@ -702,8 +681,7 @@ mixin template UnitTest() testClass.after = &after; testClass.afterClass = &afterClass; - testClassOrder ~= this.classinfo.name; - testClasses[this.classinfo.name] = testClass; + testClasses ~= testClass; } private static string _choice(in string[] memberFunctions) From 43da39a330d0a4d956f4baab6a3d5881cdfc2a93 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 4 Dec 2015 13:28:10 +0100 Subject: [PATCH 50/73] support for approximately equal floating-point values --- src/dunit/assertion.d | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 0a0bb5d..bebf311 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -103,13 +103,41 @@ unittest } /** - * Asserts that the string values are equal. + * Asserts that the floating-point values are approximately equal. * Throws: AssertException otherwise */ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) - if (!isSomeString!T) + if (isFloatingPoint!T || isFloatingPoint!U) +{ + import std.math : approxEqual; + + if (approxEqual(expected, actual)) + return; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ format("expected: <%s> but was: <%s>", expected, actual), + file, line); +} + +/// +unittest +{ + assertEquals(1, 1.01); + assertEquals("expected: <1> but was: <1.1>", + collectExceptionMsg!AssertException(assertEquals(1, 1.1))); +} + +/** + * Asserts that the values are equal. + * Throws: AssertException otherwise + */ +void assertEquals(T, U)(T expected, U actual, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) + if (!isSomeString!T && !isFloatingPoint!T && !isFloatingPoint!U) { if (expected == actual) return; @@ -127,8 +155,6 @@ unittest assertEquals("expected: <42> but was: <24>", collectExceptionMsg!AssertException(assertEquals(42, 24))); - assertEquals(42.0, 42.0); - Object foo = new Object(); Object bar = null; From 313095be9ac1a5e8843a4ae17b05e2ebcfc1d9eb Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 17 Jan 2016 18:08:15 +0100 Subject: [PATCH 51/73] added progressive XML output --- example.d | 2 +- src/dunit/framework.d | 93 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/example.d b/example.d index fed7eac..22821c6 100755 --- a/example.d +++ b/example.d @@ -184,7 +184,7 @@ class TestingAsynchronousCode private void threadFunction() { - Thread.sleep(msecs(100)); + Thread.sleep(100.msecs); done = true; } diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 722074b..f4c157e 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -60,14 +60,17 @@ public int dunit_main(string[] args) bool list = false; string report = null; bool verbose = false; + bool xml = false; try { result = getopt(args, - "filter|f", "Select test functions matching the regular expression", &filters, "list|l", "Display the test functions, then exit", &list, + "filter|f", "Select test functions matching the regular expression", &filters, + "verbose|v", "Display more information as the tests are run", &verbose, + "xml", "Display progressive XML output", &xml, "report", "Write JUnit-style XML test report", &report, - "verbose|v", "Display more information as the tests are run", &verbose); + ); } catch (Exception exception) { @@ -130,24 +133,27 @@ public int dunit_main(string[] args) return 0; } - if (verbose) + if (xml) { - testListeners ~= new DetailReporter(); + testListeners ~= new XmlReporter(); } else { - testListeners ~= new IssueReporter(); + if (verbose) + testListeners ~= new DetailReporter(); + else + testListeners ~= new IssueReporter(); } if (!report.empty) - { - testListeners ~= new XmlReporter(report); - } + testListeners ~= new ReportReporter(report); auto reporter = new ResultReporter(); testListeners ~= reporter; runTests(testSelections, testListeners); + if (!xml) + reporter.write(); return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } @@ -488,6 +494,11 @@ class ResultReporter : TestListener } public void exit() + { + // do nothing + } + + public void write() { writeln(); writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d", @@ -510,6 +521,72 @@ class XmlReporter : TestListener { import std.xml; + private Document testCase; + private string className; + private TickDuration startTime; + + public void enterClass(string className) + { + this.className = className; + } + + public void enterTest(string test) + { + this.testCase = new Document(new Tag("testcase")); + this.testCase.tag.attr["classname"] = this.className; + this.testCase.tag.attr["name"] = test; + this.startTime = TickDuration.currSystemTick(); + } + + public void skip(string reason) + { + auto element = new Element("skipped"); + + element.tag.attr["message"] = reason; + this.testCase ~= element; + } + + public void addFailure(string phase, AssertException exception) + { + auto element = new Element("failure"); + string message = "%s %s: %s".format(phase, + description(exception), exception.msg); + + element.tag.attr["message"] = message; + this.testCase ~= element; + } + + public void addError(string phase, Throwable throwable) + { + auto element = new Element("error", throwable.info.toString); + string message = "%s %s: %s".format(phase, + description(throwable), throwable.msg); + + element.tag.attr["message"] = message; + this.testCase ~= element; + } + + public void exitTest(bool success) + { + double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; + + this.testCase.tag.attr["time"] = "%.3f".format(elapsed); + + string report = join(this.testCase.pretty(4), "\n"); + + writeln(report); + } + + public void exit() + { + // do nothing + } +} + +class ReportReporter : TestListener +{ + import std.xml; + private string fileName; private Document document; private Element testSuite; From e8c2ada34758ae22b2ab30ae77b6e4884cdb8a0e Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 19 Aug 2016 22:21:44 +0200 Subject: [PATCH 52/73] switch to JUnit 5 --- README.md | 18 ++- example.d | 42 +++++-- src/dunit/assertion.d | 258 +++++++++++++++++++++++++++++++-------- src/dunit/attributes.d | 16 ++- src/dunit/framework.d | 106 +++++++--------- src/dunit/ng/assertion.d | 89 ++++++++++++++ src/dunit/ng/package.d | 5 + 7 files changed, 403 insertions(+), 131 deletions(-) create mode 100644 src/dunit/ng/assertion.d create mode 100644 src/dunit/ng/package.d diff --git a/README.md b/README.md index 5432787..62cbf49 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ Thanks to D's User Defined Attributes, test names no longer have to start with "test". Put `mixin UnitTest;` in your test class and attach `@Test`, -`@Before`, `@After`, `@BeforeClass`, `@AfterClass`, and `@Ignore("...")` -(borrowed from JUnit 4) to the member functions to state their purpose. +`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, and `@Disabled("...")` +(borrowed from JUnit 5) to the member functions to state their purpose. Test Results ------------ @@ -110,6 +110,20 @@ Alternatively, build and run the example using dub --build=plain --config=example -- --verbose +"Next Generation" +----------------- + +JUnit's `assertEquals(expected, actual)` got changed into +TestNG's `assertEquals(actual, expected)`, which feels more natural. +Moreover, the reversed order of arguments is more convenient for +D's Uniform Function Call Syntax: `answer.assertEquals(42)`. +The only effect, however, is on the failure messages, +which will be confusing if the order is mixed up. + +So, if you prefer TestNG's order of arguments, +import `dunit.ng` or `dunit.ng.assertion` +instead of the conventional `dunit` and `dunit.assertion`. + Related Projects ---------------- diff --git a/example.d b/example.d index 22821c6..9d13d8b 100755 --- a/example.d +++ b/example.d @@ -1,7 +1,11 @@ -#!/usr/bin/env rdmd -unittest -Isrc +#!/usr/bin/env dub +/+ dub.sdl: +name "example" +dependency "d-unit" version=">=0.7.2" ++/ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2014. +// Copyright Mario Kröplin 2016. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -49,9 +53,12 @@ class Test } @Test - public void assertOpFailure() + public void assertAllFailure() { - assertLessThan(6 * 7, 42); + assertAll( + assertLessThan(6 * 7, 42), + assertGreaterThan(6 * 7, 42), + ); } } @@ -70,25 +77,25 @@ class TestFixture debug writeln("@this()"); } - @BeforeClass + @BeforeAll public static void setUpClass() { debug writeln("@BeforeClass"); } - @AfterClass + @AfterAll public static void tearDownClass() { debug writeln("@AfterClass"); } - @Before + @BeforeEach public void setUp() { debug writeln("@Before"); } - @After + @AfterEach public void tearDown() { debug writeln("@After"); @@ -114,7 +121,7 @@ class TestReuse : TestFixture { mixin UnitTest; - @Before + @BeforeEach public override void setUp() { debug writeln("@Before override"); @@ -144,7 +151,7 @@ class TestingThisAndThat // disabled test function @Test - @Ignore("not ready yet") + @Disabled("not ready yet") public void failure() { testResult(false); @@ -156,6 +163,17 @@ class TestingThisAndThat { assert(false); } + + // expected exception can be further verified + @Test + public void testException() + { + import std.exception : enforce; + + auto exception = expectThrows(enforce(false)); + + assertEquals("Enforcement failed", exception.msg); + } } /** @@ -169,14 +187,14 @@ class TestingAsynchronousCode private bool done; - @Before + @BeforeEach public void setUp() { done = false; thread = new Thread(&threadFunction); } - @After + @AfterEach public void tearDown() { thread.join(); diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index bebf311..4cb72b2 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -1,13 +1,11 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2014. +// Copyright Mario Kröplin 2016. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) module dunit.assertion; -import dunit.diff; - import core.thread; import core.time; import std.algorithm; @@ -17,18 +15,56 @@ import std.range; import std.string; import std.traits; -version (unittest) import std.exception; - /** * Thrown on an assertion failure. */ class AssertException : Exception { - this(string msg = null, + @safe pure nothrow this(string msg, string file = __FILE__, - size_t line = __LINE__) + size_t line = __LINE__, + Throwable next = null) { - super(msg.empty ? "Assertion failure" : msg, file, line); + super(msg.empty ? "Assertion failure" : msg, file, line, next); + } +} + +/** + * Thrown on an assertion failure. + */ +class AssertAllException : AssertException +{ + AssertException[] exceptions; + + @safe pure nothrow this(AssertException[] exceptions, + string file = __FILE__, + size_t line = __LINE__, + Throwable next = null) + { + string msg = heading(exceptions.length); + + exceptions.each!(exception => msg ~= '\n' ~ exception.description); + this.exceptions = exceptions; + super(msg, file, line, next); + } + + @safe pure nothrow static string heading(size_t count) + { + if (count == 1) + return "1 assertion failure:"; + else + return text(count, " assertion failures:"); + } +} + +@safe pure nothrow string description(Throwable throwable) +{ + with (throwable) + { + if (file.empty) + return text(typeid(throwable).name, ": ", msg); + else + return text(typeid(throwable).name, "@", file, "(", line, "): ", msg); } } @@ -50,8 +86,10 @@ void assertTrue(bool condition, lazy string msg = null, unittest { assertTrue(true); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertTrue(false))); + + auto exception = expectThrows!AssertException(assertTrue(false)); + + assertEquals("Assertion failure", exception.msg); } /** @@ -72,8 +110,10 @@ void assertFalse(bool condition, lazy string msg = null, unittest { assertFalse(false); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertFalse(true))); + + auto exception = expectThrows!AssertException(assertFalse(true)); + + assertEquals("Assertion failure", exception.msg); } /** @@ -85,6 +125,8 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, size_t line = __LINE__) if (isSomeString!T) { + import dunit.diff : description; + if (expected == actual) return; @@ -98,8 +140,10 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, unittest { assertEquals("foo", "foo"); - assertEquals("expected: > but was: >", - collectExceptionMsg!AssertException(assertEquals("bar", "baz"))); + + auto exception = expectThrows!AssertException(assertEquals("bar", "baz")); + + assertEquals("expected: > but was: >", exception.msg); } /** @@ -126,8 +170,10 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, unittest { assertEquals(1, 1.01); - assertEquals("expected: <1> but was: <1.1>", - collectExceptionMsg!AssertException(assertEquals(1, 1.1))); + + auto exception = expectThrows!AssertException(assertEquals(1, 1.1)); + + assertEquals("expected: <1> but was: <1.1>", exception.msg); } /** @@ -152,16 +198,24 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, unittest { assertEquals(42, 42); - assertEquals("expected: <42> but was: <24>", - collectExceptionMsg!AssertException(assertEquals(42, 24))); + auto exception = expectThrows!AssertException(assertEquals(42, 24)); + + assertEquals("expected: <42> but was: <24>", exception.msg); +} + +/// +unittest +{ Object foo = new Object(); Object bar = null; assertEquals(foo, foo); assertEquals(bar, bar); - assertEquals("expected: but was: ", - collectExceptionMsg!AssertException(assertEquals(foo, bar))); + + auto exception = expectThrows!AssertException(assertEquals(foo, bar)); + + assertEquals("expected: but was: ", exception.msg); } /** @@ -208,10 +262,13 @@ unittest double[string] expected = ["foo": 1, "bar": 2]; assertArrayEquals(expected, ["foo": 1, "bar": 2]); - assertEquals(`mismatch at key "foo"; expected: <1> but was: <2>`, - collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 2]))); - assertEquals(`key mismatch; difference: "bar"`, - collectExceptionMsg!AssertException(assertArrayEquals(expected, ["foo": 1]))); + + AssertException exception; + + exception = expectThrows!AssertException(assertArrayEquals(expected, ["foo": 2])); + assertEquals(`mismatch at key "foo"; expected: <1> but was: <2>`, exception.msg); + exception = expectThrows!AssertException(assertArrayEquals(expected, ["foo": 1])); + assertEquals(`key mismatch; difference: "bar"`, exception.msg); } /** @@ -248,14 +305,17 @@ unittest double[] expected = [0, 1]; assertRangeEquals(expected, [0, 1]); - assertEquals("mismatch at index 1; expected: <1> but was: <1.2>", - collectExceptionMsg!AssertException(assertRangeEquals(expected, [0, 1.2, 3]))); - assertEquals("length mismatch at index 1; expected: <1> but was: empty", - collectExceptionMsg!AssertException(assertRangeEquals(expected, [0]))); - assertEquals("length mismatch at index 2; expected: empty but was: <2>", - collectExceptionMsg!AssertException(assertRangeEquals(expected, [0, 1, 2]))); - assertEquals("mismatch at index 2; expected: but was: ", - collectExceptionMsg!AssertException(assertArrayEquals("bar", "baz"))); + + AssertException exception; + + exception = expectThrows!AssertException(assertRangeEquals(expected, [0, 1.2, 3])); + assertEquals("mismatch at index 1; expected: <1> but was: <1.2>", exception.msg); + exception = expectThrows!AssertException(assertRangeEquals(expected, [0])); + assertEquals("length mismatch at index 1; expected: <1> but was: empty", exception.msg); + exception = expectThrows!AssertException(assertRangeEquals(expected, [0, 1, 2])); + assertEquals("length mismatch at index 2; expected: empty but was: <2>", exception.msg); + exception = expectThrows!AssertException(assertArrayEquals("bar", "baz")); + assertEquals("mismatch at index 2; expected: but was: ", exception.msg); } /** @@ -276,8 +336,10 @@ void assertEmpty(T)(T actual, lazy string msg = null, unittest { assertEmpty([]); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertEmpty([1, 2, 3]))); + + auto exception = expectThrows!AssertException(assertEmpty([1, 2, 3])); + + assertEquals("Assertion failure", exception.msg); } /** @@ -298,8 +360,10 @@ void assertNotEmpty(T)(T actual, lazy string msg = null, unittest { assertNotEmpty([1, 2, 3]); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertNotEmpty([]))); + + auto exception = expectThrows!AssertException(assertNotEmpty([])); + + assertEquals("Assertion failure", exception.msg); } /** @@ -322,8 +386,10 @@ unittest Object foo = new Object(); assertNull(null); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertNull(foo))); + + auto exception = expectThrows!AssertException(assertNull(foo)); + + assertEquals("Assertion failure", exception.msg); } /** @@ -346,8 +412,10 @@ unittest Object foo = new Object(); assertNotNull(foo); - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(assertNotNull(null))); + + auto exception = expectThrows!AssertException(assertNotNull(null)); + + assertEquals("Assertion failure", exception.msg); } /** @@ -374,8 +442,10 @@ unittest Object bar = new Object(); assertSame(foo, foo); - assertEquals("expected same: was not: ", - collectExceptionMsg!AssertException(assertSame(foo, bar))); + + auto exception = expectThrows!AssertException(assertSame(foo, bar)); + + assertEquals("expected same: was not: ", exception.msg); } /** @@ -402,8 +472,88 @@ unittest Object bar = new Object(); assertNotSame(foo, bar); - assertEquals("expected not same", - collectExceptionMsg!AssertException(assertNotSame(foo, foo))); + + auto exception = expectThrows!AssertException(assertNotSame(foo, foo)); + + assertEquals("expected not same", exception.msg); +} + +/** + * Asserts that all assertions pass. + * Throws: AssertAllException otherwise + */ +void assertAll(void delegate()[] assertions ...) +{ + AssertException[] exceptions = null; + + foreach (assertion; assertions) + try + assertion(); + catch (AssertException exception) + exceptions ~= exception; + if (!exceptions.empty) + { + // [Issue 16345] IFTI fails with lazy variadic function in some cases + const file = null; + const line = 0; + + throw new AssertAllException(exceptions, file, line); + } +} + +/// +unittest +{ + assertAll( + assertTrue(true), + assertFalse(false), + ); + + auto exception = expectThrows!AssertException(assertAll( + assertTrue(false), + assertFalse(true), + )); + + assertTrue(exception.msg.canFind("2 assertion failures"), exception.msg); +} + +/** + * Asserts that the expression throws the specified throwable. + * Throws: AssertException otherwise + * Returns: the caught throwable + */ +T expectThrows(T : Throwable = Exception, E)(lazy E expression, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + try + expression(); + catch (T throwable) + return throwable; + + string header = (msg.empty) ? null : msg ~ "; "; + + fail(header ~ format("expected <%s> was not thrown", T.stringof), + file, line); + assert(0); +} + +/// +unittest +{ + import std.exception : enforce; + + auto exception = expectThrows(enforce(false)); + + assertEquals("Enforcement failed", exception.msg); +} + +/// +unittest +{ + auto exception = expectThrows!AssertException(expectThrows(42)); + + assertEquals("expected was not thrown", exception.msg); } /** @@ -420,8 +570,9 @@ void fail(string msg = null, /// unittest { - assertEquals("Assertion failure", - collectExceptionMsg!AssertException(fail())); + auto exception = expectThrows!AssertException(fail()); + + assertEquals("Assertion failure", exception.msg); } alias assertGreaterThan = assertOp!">"; @@ -453,8 +604,10 @@ template assertOp(string op) unittest { assertOp!"<"(2, 3); - assertEquals("condition (2 >= 3) not satisfied", - collectExceptionMsg!AssertException(assertOp!">="(2, 3))); + + auto exception = expectThrows!AssertException(assertOp!">="(2, 3)); + + assertEquals("condition (2 >= 3) not satisfied", exception.msg); } /** @@ -478,11 +631,11 @@ public static void assertEventually(bool delegate() probe, string file = __FILE__, size_t line = __LINE__) { - TickDuration startTime = TickDuration.currSystemTick(); + const startTime = TickDuration.currSystemTick(); while (!probe()) { - Duration elapsedTime = cast(Duration)(TickDuration.currSystemTick() - startTime); + const elapsedTime = cast(Duration)(TickDuration.currSystemTick() - startTime); if (elapsedTime >= timeout) fail(msg.empty ? "timed out" : msg, file, line); @@ -496,6 +649,7 @@ unittest { assertEventually({ static count = 0; return ++count > 23; }); - assertEquals("timed out", - collectExceptionMsg!AssertException(assertEventually({ return false; }))); + auto exception = expectThrows!AssertException(assertEventually({ return false; })); + + assertEquals("timed out", exception.msg); } diff --git a/src/dunit/attributes.d b/src/dunit/attributes.d index 37d5e72..4077833 100644 --- a/src/dunit/attributes.d +++ b/src/dunit/attributes.d @@ -1,12 +1,18 @@ module dunit.attributes; -enum After; -enum AfterClass; -enum Before; -enum BeforeClass; +enum AfterEach; +enum AfterAll; +enum BeforeEach; +enum BeforeAll; enum Test; -struct Ignore +struct Disabled { string reason; } + +deprecated("use AfterEach instead") alias After = AfterEach; +deprecated("use AfterAll instead") alias AfterClass = AfterAll; +deprecated("use BeforeEach instead") alias Before = BeforeEach; +deprecated("use BeforeAll instead") alias BeforeClass = BeforeAll; +deprecated("use Disabled instead") alias Ignore = Disabled; diff --git a/src/dunit/framework.d b/src/dunit/framework.d index f4c157e..86d9e35 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2015. +// Copyright Mario Kröplin 2016. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -23,14 +23,14 @@ struct TestClass { string name; string[] tests; - Ignore[string] ignores; + Disabled[string] disabled; Object function() create; - void function(Object o) beforeClass; - void function(Object o) before; + void function(Object o) beforeAll; + void function(Object o) beforeEach; void delegate(Object o, string test) test; - void function(Object o) after; - void function(Object o) afterClass; + void function(Object o) afterEach; + void function(Object o) afterAll; } TestClass[] testClasses; @@ -190,6 +190,7 @@ body foreach (testListener; testListeners) testListener.enterClass(testClass.name); + // TODO testObject per test; static BeforeAll and AfterAll Object testObject = null; bool classSetUp = true; // not yet failed @@ -197,7 +198,7 @@ body foreach (test; tests) { bool success = true; - bool ignore = cast(bool)(test in testClass.ignores); + bool ignore = cast(bool)(test in testClass.disabled); foreach (testListener; testListeners) testListener.enterTest(test); @@ -215,14 +216,14 @@ body } if (classSetUp) { - classSetUp = tryRun("@BeforeClass", - { testClass.beforeClass(testObject); }); + classSetUp = tryRun("@BeforeAll", + { testClass.beforeAll(testObject); }); } } if (ignore || !classSetUp) { - string reason = testClass.ignores.get(test, Ignore.init).reason; + string reason = testClass.disabled.get(test, Disabled.init).reason; foreach (testListener; testListeners) testListener.skip(reason); @@ -230,24 +231,24 @@ body continue; } - success = tryRun("@Before", - { testClass.before(testObject); }); + success = tryRun("@BeforeEach", + { testClass.beforeEach(testObject); }); if (success) { success = tryRun("@Test", { testClass.test(testObject, test); }); - // run @After even if @Test failed - success = tryRun("@After", - { testClass.after(testObject); }) + // run @AfterEach even if @Test failed + success = tryRun("@AfterEach", + { testClass.afterEach(testObject); }) && success; } } if (testObject !is null && classSetUp) { - tryRun("@AfterClass", - { testClass.afterClass(testObject); }); + tryRun("@AfterAll", + { testClass.afterAll(testObject); }); } } @@ -284,24 +285,13 @@ interface TestListener case "@Test": return test; case "this": - case "@BeforeClass": - case "@AfterClass": + case "@BeforeAll": + case "@AfterAll": return phase; default: return test ~ phase; } } - - public static string description(Throwable throwable) - { - with (throwable) - { - if (file.empty) - return typeid(throwable).name; - else - return "%s@%s(%d)".format(typeid(throwable).name, file, line); - } - } } class IssueReporter : TestListener @@ -389,7 +379,7 @@ class IssueReporter : TestListener writefln("%d) %s", i + 1, prettyOrigin(issue.testClass, issue.test, issue.phase)); - writefln("%s: %s", description(throwable), throwable.msg); + writefln(throwable.description); } } } @@ -416,14 +406,14 @@ class DetailReporter : TestListener writec(Color.yellow, " SKIP: "); writeln(this.test); if (!reason.empty) - writeln(indent(`"%s"`.format(reason))); + writeln(indent(format(`"%s"`, reason))); } public void addFailure(string phase, AssertException exception) { writec(Color.red, " FAILURE: "); writeln(prettyOrigin(this.test, phase)); - writeln(indent("%s: %s".format(description(exception), exception.msg))); + writeln(indent(exception.description)); } public void addError(string phase, Throwable throwable) @@ -549,8 +539,7 @@ class XmlReporter : TestListener public void addFailure(string phase, AssertException exception) { auto element = new Element("failure"); - string message = "%s %s: %s".format(phase, - description(exception), exception.msg); + string message = format("%s %s", phase, exception.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -559,8 +548,7 @@ class XmlReporter : TestListener public void addError(string phase, Throwable throwable) { auto element = new Element("error", throwable.info.toString); - string message = "%s %s: %s".format(phase, - description(throwable), throwable.msg); + string message = format("%s %s", phase, throwable.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -570,7 +558,7 @@ class XmlReporter : TestListener { double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; - this.testCase.tag.attr["time"] = "%.3f".format(elapsed); + this.testCase.tag.attr["time"] = format("%.3f", elapsed); string report = join(this.testCase.pretty(4), "\n"); @@ -635,8 +623,7 @@ class ReportReporter : TestListener if (this.testCase.elements.empty) { auto element = new Element("failure"); - string message = "%s %s: %s".format(phase, - description(exception), exception.msg); + string message = format("%s %s", phase, exception.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -649,8 +636,7 @@ class ReportReporter : TestListener if (this.testCase.elements.empty) { auto element = new Element("error", throwable.info.toString); - string message = "%s %s: %s".format(phase, - description(throwable), throwable.msg); + string message = format("%s %s", phase, throwable.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -661,7 +647,7 @@ class ReportReporter : TestListener { double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; - this.testCase.tag.attr["time"] = "%.3f".format(elapsed); + this.testCase.tag.attr["time"] = format("%.3f", elapsed); } public void exit() @@ -686,10 +672,10 @@ private TestClass[] unitTestFunctions() testClass.tests = ["unittest"]; testClass.create = () => null; - testClass.beforeClass = (o) {}; - testClass.before = (o) {}; - testClass.after = (o) {}; - testClass.afterClass = (o) {}; + testClass.beforeAll = (o) {}; + testClass.beforeEach = (o) {}; + testClass.afterEach = (o) {}; + testClass.afterAll = (o) {}; foreach (moduleInfo; ModuleInfo) { @@ -719,21 +705,21 @@ mixin template UnitTest() testClass.name = this.classinfo.name; testClass.tests = _members!(typeof(this), Test); - testClass.ignores = _attributes!(typeof(this), Ignore); + testClass.disabled = _attributes!(typeof(this), Disabled); static Object create() { mixin("return new " ~ typeof(this).stringof ~ "();"); } - static void beforeClass(Object o) + static void beforeAll(Object o) { - mixin(_sequence(_members!(typeof(this), BeforeClass))); + mixin(_sequence(_members!(typeof(this), BeforeAll))); } - static void before(Object o) + static void beforeEach(Object o) { - mixin(_sequence(_members!(typeof(this), Before))); + mixin(_sequence(_members!(typeof(this), BeforeEach))); } void test(Object o, string name) @@ -741,22 +727,22 @@ mixin template UnitTest() mixin(_choice(_members!(typeof(this), Test))); } - static void after(Object o) + static void afterEach(Object o) { - mixin(_sequence(_members!(typeof(this), After))); + mixin(_sequence(_members!(typeof(this), AfterEach))); } - static void afterClass(Object o) + static void afterAll(Object o) { - mixin(_sequence(_members!(typeof(this), AfterClass))); + mixin(_sequence(_members!(typeof(this), AfterAll))); } testClass.create = &create; - testClass.beforeClass = &beforeClass; - testClass.before = &before; + testClass.beforeAll = &beforeAll; + testClass.beforeEach = &beforeEach; testClass.test = &test; - testClass.after = &after; - testClass.afterClass = &afterClass; + testClass.afterEach = &afterEach; + testClass.afterAll = &afterAll; testClasses ~= testClass; } diff --git a/src/dunit/ng/assertion.d b/src/dunit/ng/assertion.d new file mode 100644 index 0000000..609a7f0 --- /dev/null +++ b/src/dunit/ng/assertion.d @@ -0,0 +1,89 @@ +// Copyright Mario Kröplin 2016. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +module dunit.ng.assertion; + +public import dunit.assertion : assertTrue, assertFalse, assertEmpty, assertNotEmpty, assertNull, assertNotNull, + assertAll, expectThrows, fail, + assertGreaterThan, assertGreaterThanOrEqual, assertLessThan, assertLessThanOrEqual, assertOp, + assertEventually; + +/** + * Asserts that the values are equal. + * Throws: AssertException otherwise + */ +void assertEquals(T, U)(T actual, U expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertEquals; + + assertEquals(expected, actual, msg, file, line); +} + +/** + * Asserts that the arrays are equal. + * Throws: AssertException otherwise + */ +void assertArrayEquals(T, U)(in T[] actual, in U[] expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertArrayEquals; + + assertArrayEquals(expected, actual, msg, file, line); +} + +/** + * Asserts that the associative arrays are equal. + * Throws: AssertException otherwise + */ +void assertArrayEquals(T, U, V)(in T[V] actual, in U[V] expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertArrayEquals; + + assertArrayEquals(expected, actual, msg, file, line); +} + +/** + * Asserts that the ranges are equal. + * Throws: AssertException otherwise + */ +void assertRangeEquals(R1, R2)(R1 actual, R2 expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertRangeEquals; + + assertRangeEquals(expected, actual, msg, file, line); +} + +/** + * Asserts that the values are the same. + * Throws: AssertException otherwise + */ +void assertSame(T, U)(T actual, U expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertSame; + + assertSame(expected, actual, msg, file, line); +} + +/** + * Asserts that the values are not the same. + * Throws: AssertException otherwise + */ +void assertNotSame(T, U)(T actual, U expected, lazy string msg = null, + string file = __FILE__, + size_t line = __LINE__) +{ + import dunit.assertion : assertNotSame; + + assertNotSame(expected, actual, msg, file, line); +} diff --git a/src/dunit/ng/package.d b/src/dunit/ng/package.d new file mode 100644 index 0000000..20e5765 --- /dev/null +++ b/src/dunit/ng/package.d @@ -0,0 +1,5 @@ +module dunit.ng; + +public import dunit.ng.assertion; +public import dunit.attributes; +public import dunit.framework; From 797c6ca8a61dbe15d5f5cc0ec1be0087bc91a08b Mon Sep 17 00:00:00 2001 From: linkrope Date: Mon, 22 Aug 2016 00:32:13 +0200 Subject: [PATCH 53/73] use one test object per test case --- example.d | 30 ++++++++--------- src/dunit/framework.d | 78 ++++++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/example.d b/example.d index 9d13d8b..94a06fd 100755 --- a/example.d +++ b/example.d @@ -78,27 +78,27 @@ class TestFixture } @BeforeAll - public static void setUpClass() + public static void setUpAll() { - debug writeln("@BeforeClass"); + debug writeln("@BeforeAll"); } @AfterAll - public static void tearDownClass() + public static void tearDownAll() { - debug writeln("@AfterClass"); + debug writeln("@AfterAll"); } @BeforeEach public void setUp() { - debug writeln("@Before"); + debug writeln("@BeforeEach"); } @AfterEach public void tearDown() { - debug writeln("@After"); + debug writeln("@AfterEach"); } @Test @@ -124,7 +124,7 @@ class TestReuse : TestFixture @BeforeEach public override void setUp() { - debug writeln("@Before override"); + debug writeln("@BeforeEach override"); } } @@ -164,16 +164,16 @@ class TestingThisAndThat assert(false); } - // expected exception can be further verified - @Test - public void testException() - { - import std.exception : enforce; + // expected exception can be further verified + @Test + public void testException() + { + import std.exception : enforce; - auto exception = expectThrows(enforce(false)); + auto exception = expectThrows(enforce(false)); - assertEquals("Enforcement failed", exception.msg); - } + assertEquals("Enforcement failed", exception.msg); + } } /** diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 86d9e35..dde24c6 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -26,11 +26,11 @@ struct TestClass Disabled[string] disabled; Object function() create; - void function(Object o) beforeAll; + void function() beforeAll; void function(Object o) beforeEach; void delegate(Object o, string test) test; void function(Object o) afterEach; - void function(Object o) afterAll; + void function() afterAll; } TestClass[] testClasses; @@ -190,15 +190,13 @@ body foreach (testListener; testListeners) testListener.enterClass(testClass.name); - // TODO testObject per test; static BeforeAll and AfterAll - Object testObject = null; - bool classSetUp = true; // not yet failed + bool initialized = false; + bool setUp = false; // run each @Test of the class foreach (test; tests) { - bool success = true; - bool ignore = cast(bool)(test in testClass.disabled); + bool success = false; foreach (testListener; testListeners) testListener.enterTest(test); @@ -206,34 +204,36 @@ body foreach (testListener; testListeners) testListener.exitTest(success); - // create test object on demand - if (!ignore && testObject is null) - { - if (classSetUp) - { - classSetUp = tryRun("this", - { testObject = testClass.create(); }); - } - if (classSetUp) - { - classSetUp = tryRun("@BeforeAll", - { testClass.beforeAll(testObject); }); - } - } - - if (ignore || !classSetUp) + if (test in testClass.disabled || (initialized && !setUp)) { string reason = testClass.disabled.get(test, Disabled.init).reason; foreach (testListener; testListeners) testListener.skip(reason); - success = false; continue; } - success = tryRun("@BeforeEach", - { testClass.beforeEach(testObject); }); + // use lazy initialization to run @BeforeAll + // (failure or error can only be reported for a given test) + if (!initialized) + { + setUp = tryRun("@BeforeAll", + { testClass.beforeAll(); }); + initialized = true; + } + Object testObject = null; + + if (setUp) + { + success = tryRun("this", + { testObject = testClass.create(); }); + } + if (success) + { + success = tryRun("@BeforeEach", + { testClass.beforeEach(testObject); }); + } if (success) { success = tryRun("@Test", @@ -244,11 +244,10 @@ body && success; } } - - if (testObject !is null && classSetUp) + if (setUp) { tryRun("@AfterAll", - { testClass.afterAll(testObject); }); + { testClass.afterAll(); }); } } @@ -672,10 +671,10 @@ private TestClass[] unitTestFunctions() testClass.tests = ["unittest"]; testClass.create = () => null; - testClass.beforeAll = (o) {}; + testClass.beforeAll = () {}; testClass.beforeEach = (o) {}; testClass.afterEach = (o) {}; - testClass.afterAll = (o) {}; + testClass.afterAll = () {}; foreach (moduleInfo; ModuleInfo) { @@ -712,9 +711,9 @@ mixin template UnitTest() mixin("return new " ~ typeof(this).stringof ~ "();"); } - static void beforeAll(Object o) + static void beforeAll() { - mixin(_sequence(_members!(typeof(this), BeforeAll))); + mixin(_static(_members!(typeof(this), BeforeAll))); } static void beforeEach(Object o) @@ -732,9 +731,9 @@ mixin template UnitTest() mixin(_sequence(_members!(typeof(this), AfterEach))); } - static void afterAll(Object o) + static void afterAll() { - mixin(_sequence(_members!(typeof(this), AfterAll))); + mixin(_static(_members!(typeof(this), AfterAll))); } testClass.create = &create; @@ -758,6 +757,15 @@ mixin template UnitTest() return block; } + private static string _static(in string[] memberFunctions) + { + string block = null; + + foreach (memberFunction; memberFunctions) + block ~= memberFunction ~ "();\n"; + return block; + } + private static string _sequence(in string[] memberFunctions) { string block = "auto testObject = cast(" ~ typeof(this).stringof ~ ") o;\n"; From 89741aab38d6577d0a7d7a8d7648d57afe044037 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 26 Aug 2016 23:32:52 +0200 Subject: [PATCH 54/73] support for tagging and filtering --- README.md | 6 +- example.d | 4 + src/dunit/attributes.d | 5 + src/dunit/framework.d | 204 ++++++++++++++++++++++++++++++----------- 4 files changed, 161 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 62cbf49..344d675 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,10 @@ Thanks to D's User Defined Attributes, test names no longer have to start with "test". Put `mixin UnitTest;` in your test class and attach `@Test`, -`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, and `@Disabled("...")` -(borrowed from JUnit 5) to the member functions to state their purpose. +`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, `@Tag("...")`, +and `@Disabled("...")` +(borrowed from [JUnit 5](http://junit.org/junit5/docs/current/user-guide/)) +to the member functions to state their purpose. Test Results ------------ diff --git a/example.d b/example.d index 94a06fd..86dbbf9 100755 --- a/example.d +++ b/example.d @@ -143,7 +143,10 @@ class TestingThisAndThat } // test function can even be private + // tagged test functions can be selected to be included or excluded @Test + @Tag("fast") + @Tag("smoke") private void success() { testResult(true); @@ -207,6 +210,7 @@ class TestingAsynchronousCode } @Test + @Tag("slow") public void test() { assertFalse(done); diff --git a/src/dunit/attributes.d b/src/dunit/attributes.d index 4077833..942966a 100644 --- a/src/dunit/attributes.d +++ b/src/dunit/attributes.d @@ -11,6 +11,11 @@ struct Disabled string reason; } +struct Tag +{ + string name; +} + deprecated("use AfterEach instead") alias After = AfterEach; deprecated("use AfterAll instead") alias AfterClass = AfterAll; deprecated("use BeforeEach instead") alias Before = BeforeEach; diff --git a/src/dunit/framework.d b/src/dunit/framework.d index dde24c6..a349b6c 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -24,6 +24,7 @@ struct TestClass string name; string[] tests; Disabled[string] disabled; + Tag[][string] tags; Object function() create; void function() beforeAll; @@ -57,6 +58,8 @@ public int dunit_main(string[] args) GetoptResult result; string[] filters = null; + string[] includeTags = null; + string[] excludeTags = null; bool list = false; string report = null; bool verbose = false; @@ -65,12 +68,15 @@ public int dunit_main(string[] args) try { result = getopt(args, - "list|l", "Display the test functions, then exit", &list, - "filter|f", "Select test functions matching the regular expression", &filters, - "verbose|v", "Display more information as the tests are run", &verbose, - "xml", "Display progressive XML output", &xml, - "report", "Write JUnit-style XML test report", &report, - ); + config.caseSensitive, + "list|l", "Display the test functions, then exit", &list, + "filter|f", "Select test functions matching the regular expression", &filters, + "include|t", "Provide a tag to be included in the test run", &includeTags, + "exclude|T", "Provide a tag to be excluded from the test run", &excludeTags, + "verbose|v", "Display more information as the tests are run", &verbose, + "xml", "Display progressive XML output", &xml, + "report", "Write JUnit-style XML test report", &report, + ); } catch (Exception exception) { @@ -118,6 +124,18 @@ public int dunit_main(string[] args) } } } + if (!includeTags.empty) + { + testSelections = testSelections + .select!"!a.findAmong(b).empty"(includeTags) + .array; + } + if (!excludeTags.empty) + { + testSelections = testSelections + .select!"a.findAmong(b).empty"(excludeTags) + .array; + } if (list) { @@ -157,7 +175,55 @@ public int dunit_main(string[] args) return (reporter.errors > 0) ? 1 : (reporter.failures > 0) ? 2 : 0; } -public static void runTests(TestSelection[] testSelections, TestListener[] testListeners) +private auto select(alias pred)(TestSelection[] testSelections, string[] tags) +{ + import std.functional : binaryFun; + + bool matches(TestClass testClass, string test) + { + auto testTags = testClass.tags.get(test, null) + .map!(tag => tag.name); + + return binaryFun!pred(testTags, tags); + } + + TestSelection select(TestSelection testSelection) + { + string[] tests = testSelection.tests + .filter!(test => matches(testSelection.testClass, test)) + .array; + + return TestSelection(testSelection.testClass, tests); + } + + return testSelections + .map!(testSelection => select(testSelection)) + .filter!(testSelection => !testSelection.tests.empty); +} + +private TestSelection[] restrict(alias pred)(TestSelection[] testSelections, string[] tags) +{ + TestSelection restrict(TestSelection testSelection) + { + string[] tests = testSelection.tests + .filter!(test => pred(testSelection.testClass.tags.get(test, null), tags)) + .array; + + return TestSelection(testSelection.testClass, tests); + } + + return testSelections + .map!(testSelection => restrict(testSelection)) + .filter!(testSelection => !testSelection.tests.empty) + .array; +} + +public bool matches(Tag[] tags, string[] choices) +{ + return tags.any!(tag => choices.canFind(tag.name)); +} + +public void runTests(TestSelection[] testSelections, TestListener[] testListeners) in { assert(all!"a !is null"(testListeners)); @@ -218,7 +284,7 @@ body if (!initialized) { setUp = tryRun("@BeforeAll", - { testClass.beforeAll(); }); + { testClass.beforeAll(); }); initialized = true; } @@ -227,27 +293,27 @@ body if (setUp) { success = tryRun("this", - { testObject = testClass.create(); }); + { testObject = testClass.create(); }); } if (success) { success = tryRun("@BeforeEach", - { testClass.beforeEach(testObject); }); + { testClass.beforeEach(testObject); }); } if (success) { success = tryRun("@Test", - { testClass.test(testObject, test); }); + { testClass.test(testObject, test); }); // run @AfterEach even if @Test failed success = tryRun("@AfterEach", - { testClass.afterEach(testObject); }) - && success; + { testClass.afterEach(testObject); }) + && success; } } if (setUp) { tryRun("@AfterAll", - { testClass.afterAll(); }); + { testClass.afterAll(); }); } } @@ -357,7 +423,7 @@ class IssueReporter : TestListener foreach (i, issue; this.errors) { writefln("%d) %s", i + 1, - prettyOrigin(issue.testClass, issue.test, issue.phase)); + prettyOrigin(issue.testClass, issue.test, issue.phase)); writeln(issue.throwable.toString); writeln("----------------"); } @@ -377,7 +443,7 @@ class IssueReporter : TestListener Throwable throwable = issue.throwable; writefln("%d) %s", i + 1, - prettyOrigin(issue.testClass, issue.test, issue.phase)); + prettyOrigin(issue.testClass, issue.test, issue.phase)); writefln(throwable.description); } } @@ -491,7 +557,7 @@ class ResultReporter : TestListener { writeln(); writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d", - this.tests, this.failures, this.errors, this.skips); + this.tests, this.failures, this.errors, this.skips); if (this.failures + this.errors == 0) { @@ -704,7 +770,8 @@ mixin template UnitTest() testClass.name = this.classinfo.name; testClass.tests = _members!(typeof(this), Test); - testClass.disabled = _attributes!(typeof(this), Disabled); + testClass.disabled = _attributeByMember!(typeof(this), Disabled); + testClass.tags = _attributesByMember!(typeof(this), Tag); static Object create() { @@ -713,7 +780,7 @@ mixin template UnitTest() static void beforeAll() { - mixin(_static(_members!(typeof(this), BeforeAll))); + mixin(_staticSequence(_members!(typeof(this), BeforeAll))); } static void beforeEach(Object o) @@ -733,7 +800,7 @@ mixin template UnitTest() static void afterAll() { - mixin(_static(_members!(typeof(this), AfterAll))); + mixin(_staticSequence(_members!(typeof(this), AfterAll))); } testClass.create = &create; @@ -757,7 +824,7 @@ mixin template UnitTest() return block; } - private static string _static(in string[] memberFunctions) + private static string _staticSequence(in string[] memberFunctions) { string block = null; @@ -775,23 +842,24 @@ mixin template UnitTest() return block; } - template _members(T, Attribute) + template _members(T, alias attribute) { static string[] helper() { + import std.meta : AliasSeq; + import std.traits : hasUDA; + string[] members; foreach (name; __traits(allMembers, T)) { static if (__traits(compiles, __traits(getMember, T, name))) { - import std.typecons; - - alias member = TypeTuple!(__traits(getMember, T, name)); + alias member = AliasSeq!(__traits(getMember, T, name)); - static if (__traits(compiles, _hasAttribute!(member, Attribute))) + static if (__traits(compiles, hasUDA!(member, attribute))) { - static if (_hasAttribute!(member, Attribute)) + static if (hasUDA!(member, attribute)) members ~= name; } } @@ -802,67 +870,91 @@ mixin template UnitTest() enum _members = helper; } - template _attributes(T, Attribute) + template _attributeByMember(T, Attribute) { static Attribute[string] helper() { - Attribute[string] attributes; + import std.format : format; + import std.meta : AliasSeq; + + Attribute[string] attributeByMember; foreach (name; __traits(allMembers, T)) { static if (__traits(compiles, __traits(getMember, T, name))) { - import std.typecons; - - alias member = TypeTuple!(__traits(getMember, T, name)); + alias member = AliasSeq!(__traits(getMember, T, name)); - static if (__traits(compiles, _hasAttribute!(member, Attribute))) + static if (__traits(compiles, _getUDAs!(member, Attribute))) { - static if (_hasAttribute!(member, Attribute)) - attributes[name] = _findAttribute!(member, Attribute); + alias attributes = _getUDAs!(member, Attribute); + + static if (attributes.length > 0) + { + static assert(attributes.length == 1, + format("%s.%s should not have more than one attribute @%s", + T.stringof, name, Attribute.stringof)); + + attributeByMember[name] = attributes[0]; + } } } } - return attributes; + return attributeByMember; } - enum _attributes = helper; + enum _attributeByMember = helper; } - template _findAttribute(alias member, Attribute) + template _attributesByMember(T, Attribute) { - static auto helper() + static Attribute[][string] helper() { - static if (__traits(compiles, __traits(getAttributes, member))) + import std.meta : AliasSeq; + + Attribute[][string] attributesByMember; + + foreach (name; __traits(allMembers, T)) { - foreach (attribute; __traits(getAttributes, member)) + static if (__traits(compiles, __traits(getMember, T, name))) { - static if (is(attribute == Attribute)) - return Attribute.init; - static if (is(typeof(attribute) == Attribute)) - return attribute; + alias member = AliasSeq!(__traits(getMember, T, name)); + + static if (__traits(compiles, _getUDAs!(member, Attribute))) + { + alias attributes = _getUDAs!(member, Attribute); + + static if (attributes.length > 0) + attributesByMember[name] = attributes; + } } } - assert(0); + return attributesByMember; } - enum _findAttribute = helper; + enum _attributesByMember = helper; } - template _hasAttribute(alias member, Attribute) + // Gets user-defined attributes, but also gets Attribute.init for @Attribute. + template _getUDAs(alias member, Attribute) { - static bool helper() + static auto helper() { - foreach (attribute; __traits(getAttributes, member)) + Attribute[] attributes; + + static if (__traits(compiles, __traits(getAttributes, member))) { - static if (is(attribute == Attribute)) - return true; - static if (is(typeof(attribute) == Attribute)) - return true; + foreach (attribute; __traits(getAttributes, member)) + { + static if (is(attribute == Attribute)) + attributes ~= Attribute.init; + static if (is(typeof(attribute) == Attribute)) + attributes ~= attribute; + } } - return false; + return attributes; } - enum bool _hasAttribute = helper; + enum _getUDAs = helper; } } From 8acf65b33ba6c8fe12498445dd96a304e18b0c7d Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 31 Aug 2016 21:35:28 +0200 Subject: [PATCH 55/73] support for unit-threaded's fluent assertions --- README.md | 13 +++++++++++++ dub.json | 10 +++++++++- example.d | 2 +- fluent_assertions.d | 35 +++++++++++++++++++++++++++++++++++ src/dunit/framework.d | 20 +++++++++++++++++++- 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100755 fluent_assertions.d diff --git a/README.md b/README.md index 344d675..31c6d75 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,19 @@ So, if you prefer TestNG's order of arguments, import `dunit.ng` or `dunit.ng.assertion` instead of the conventional `dunit` and `dunit.assertion`. +Fluent Assertions +----------------- + +The xUnit Testing Framework also supports the "fluent assertions" from +[unit-threaded](https://github.com/atilaneves/unit-threaded). + +For an example, have a look at [fluent-assertions](fluent_assertions.d). +Build and run the example using + + dub --config=fluent-assertions + +(When you get three failures, everything works fine.) + Related Projects ---------------- diff --git a/dub.json b/dub.json index 86304f0..de1e102 100644 --- a/dub.json +++ b/dub.json @@ -1,7 +1,7 @@ { "name": "d-unit", "description": "xUnit Testing Framework for D", - "copyright": "Copyright © 2013, Mario Kröplin", + "copyright": "Copyright © 2016, Mario Kröplin", "authors": ["Juan Manuel Cabo", "Mario Kröplin"], "license" : "BSL-1.0", "dependencies": {}, @@ -15,6 +15,14 @@ "name": "example", "targetType": "executable", "sourceFiles": ["example.d"] + }, + { + "name": "fluent-assertions", + "targetType": "executable", + "sourceFiles": ["fluent_assertions.d"], + "dependencies": { + "unit-threaded": ">=0.6.27" + } } ] } diff --git a/example.d b/example.d index 86dbbf9..3cdce66 100755 --- a/example.d +++ b/example.d @@ -1,7 +1,7 @@ #!/usr/bin/env dub /+ dub.sdl: name "example" -dependency "d-unit" version=">=0.7.2" +dependency "d-unit" version=">=0.8.0" +/ // Copyright Juan Manuel Cabo 2012. diff --git a/fluent_assertions.d b/fluent_assertions.d new file mode 100755 index 0000000..9674dea --- /dev/null +++ b/fluent_assertions.d @@ -0,0 +1,35 @@ +// Copyright Mario Kröplin 2016. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +module fluent_assertion; + +import dunit; +import unit_threaded.should : shouldBeIn, shouldEqual, shouldNotEqual; + +class Test +{ + mixin UnitTest; + + @Test + public void shouldEqualFailure() + { + "bar".shouldEqual("baz"); + } + + @Test + public void shouldNotEqualFailure() + { + "foo".shouldNotEqual("foo"); + } + + @Test + public void shouldBeInFailure() + { + 42.shouldBeIn([0, 1, 2]); + } +} + +// either use the 'Main' mixin or call 'dunit_main(args)' +mixin Main; diff --git a/src/dunit/framework.d b/src/dunit/framework.d index a349b6c..f9719ba 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -234,7 +234,25 @@ body { try { - action(); + static if (__traits(compiles, { import unit_threaded.should : UnitTestException; })) + { + import unit_threaded.should : UnitTestException; + + try + { + action(); + } + catch (UnitTestException exception) + { + // convert exception to "fix" the message format + throw new AssertException('\n' ~ exception.msg, + exception.file, exception.line, exception); + } + } + else + { + action(); + } return true; } catch (AssertException exception) From d993607b047550c98ea40f92041d9c6912963fa5 Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 31 Aug 2016 21:58:08 +0200 Subject: [PATCH 56/73] fixed documentation --- example.d | 2 +- fluent_assertions.d | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/example.d b/example.d index 3cdce66..47e1d36 100755 --- a/example.d +++ b/example.d @@ -65,7 +65,7 @@ class Test /** * This example demonstrates the order in which the fixture functions run. * The functions 'setUp' and 'tearDown' run before and after each test. - * The functions 'setUpClass' and 'tearDownClass' run once before and after + * The functions 'setUpAll' and 'tearDownAll' run once before and after * all tests in the class. */ class TestFixture diff --git a/fluent_assertions.d b/fluent_assertions.d index 9674dea..52a8979 100755 --- a/fluent_assertions.d +++ b/fluent_assertions.d @@ -8,6 +8,10 @@ module fluent_assertion; import dunit; import unit_threaded.should : shouldBeIn, shouldEqual, shouldNotEqual; +/** + * This example demonstrates the reporting of test failures + * with unit-threaded's fluent assertions. + */ class Test { mixin UnitTest; From 22498d4a47649e9a9b0f8d5186579fe9fc242085 Mon Sep 17 00:00:00 2001 From: linkrope Date: Mon, 5 Sep 2016 23:52:59 +0200 Subject: [PATCH 57/73] made `assertTrue` and `assertFalse` more flexible --- src/dunit/assertion.d | 41 ++++++++++++++++++++++++++++++---------- src/dunit/ng/assertion.d | 3 ++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 4cb72b2..564c356 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -72,11 +72,11 @@ class AssertAllException : AssertException * Asserts that a condition is true. * Throws: AssertException otherwise */ -void assertTrue(bool condition, lazy string msg = null, +void assertTrue(T)(T condition, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { - if (condition) + if (cast(bool) condition) return; fail(msg, file, line); @@ -86,6 +86,7 @@ void assertTrue(bool condition, lazy string msg = null, unittest { assertTrue(true); + assertTrue("foo" in ["foo": "bar"]); auto exception = expectThrows!AssertException(assertTrue(false)); @@ -96,11 +97,11 @@ unittest * Asserts that a condition is false. * Throws: AssertException otherwise */ -void assertFalse(bool condition, lazy string msg = null, +void assertFalse(T)(T condition, lazy string msg = null, string file = __FILE__, size_t line = __LINE__) { - if (!condition) + if (!cast(bool) condition) return; fail(msg, file, line); @@ -110,6 +111,7 @@ void assertFalse(bool condition, lazy string msg = null, unittest { assertFalse(false); + assertFalse("foo" in ["bar": "foo"]); auto exception = expectThrows!AssertException(assertFalse(true)); @@ -245,15 +247,15 @@ void assertArrayEquals(T, U, V)(in T[V] expected, in U[V] actual, lazy string ms if (key in actual) { assertEquals(expected[key], actual[key], - // format string key with double quotes - format(header ~ "mismatch at key %(%s%)", [key]), + format(header ~ "mismatch at key %s", key.repr), file, line); } auto difference = setSymmetricDifference(expected.keys.sort(), actual.keys.sort()); assertEmpty(difference, - "key mismatch; difference: %(%s, %)".format(difference)); + format("key mismatch; difference: %(%s, %)", difference), + file, line); } /// @@ -579,6 +581,8 @@ alias assertGreaterThan = assertOp!">"; alias assertGreaterThanOrEqual = assertOp!">="; alias assertLessThan = assertOp!"<"; alias assertLessThanOrEqual = assertOp!"<="; +alias assertIn = assertOp!"in"; +alias assertNotIn = assertOp!"!in"; /** * Asserts that the condition (lhs op rhs) is satisfied. @@ -595,7 +599,8 @@ template assertOp(string op) string header = (msg.empty) ? null : msg ~ "; "; - fail("%scondition (%s %s %s) not satisfied".format(header, lhs, op, rhs), + fail(format("%scondition (%s %s %s) not satisfied", + header, lhs.repr, op, rhs.repr), file, line); } } @@ -603,13 +608,23 @@ template assertOp(string op) /// unittest { - assertOp!"<"(2, 3); + assertLessThan(2, 3); - auto exception = expectThrows!AssertException(assertOp!">="(2, 3)); + auto exception = expectThrows!AssertException(assertGreaterThanOrEqual(2, 3)); assertEquals("condition (2 >= 3) not satisfied", exception.msg); } +/// +unittest +{ + assertIn("foo", ["foo": "bar"]); + + auto exception = expectThrows!AssertException(assertNotIn("foo", ["foo": "bar"])); + + assertEquals(`condition ("foo" !in ["foo":"bar"]) not satisfied`, exception.msg); +} + /** * Checks a probe until the timeout expires. The assert error is produced * if the probe fails to return 'true' before the timeout. @@ -653,3 +668,9 @@ unittest assertEquals("timed out", exception.msg); } + +private string repr(T)(T value) +{ + // format string key with double quotes + return format("%(%s%)", [value]); +} diff --git a/src/dunit/ng/assertion.d b/src/dunit/ng/assertion.d index 609a7f0..f8b37cf 100644 --- a/src/dunit/ng/assertion.d +++ b/src/dunit/ng/assertion.d @@ -7,7 +7,8 @@ module dunit.ng.assertion; public import dunit.assertion : assertTrue, assertFalse, assertEmpty, assertNotEmpty, assertNull, assertNotNull, assertAll, expectThrows, fail, - assertGreaterThan, assertGreaterThanOrEqual, assertLessThan, assertLessThanOrEqual, assertOp, + assertGreaterThan, assertGreaterThanOrEqual, assertLessThan, assertLessThanOrEqual, + assertIn, assertNotIn, assertOp, assertEventually; /** From 12073f1bdb01a1d9815e91c9bb6b73d99b1bf57d Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 15 Mar 2017 16:11:21 +0100 Subject: [PATCH 58/73] fixed issue with `writefln` instead of `writeln` --- src/dunit/framework.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index f9719ba..be2339e 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -462,7 +462,7 @@ class IssueReporter : TestListener writefln("%d) %s", i + 1, prettyOrigin(issue.testClass, issue.test, issue.phase)); - writefln(throwable.description); + writeln(throwable.description); } } } From 57b666675da16e3762e905d669ece34c5a74c295 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 7 Apr 2017 17:21:50 +0200 Subject: [PATCH 59/73] @safe pure --- dub.json | 4 ++-- example.d | 24 ++++++++++---------- fluent_assertions.d | 10 ++++----- src/dunit/assertion.d | 52 +++++++++++++++++++++---------------------- src/dunit/diff.d | 8 +++---- 5 files changed, 48 insertions(+), 50 deletions(-) diff --git a/dub.json b/dub.json index de1e102..3e81ca1 100644 --- a/dub.json +++ b/dub.json @@ -1,7 +1,7 @@ { "name": "d-unit", "description": "xUnit Testing Framework for D", - "copyright": "Copyright © 2016, Mario Kröplin", + "copyright": "Copyright © 2017, Mario Kröplin", "authors": ["Juan Manuel Cabo", "Mario Kröplin"], "license" : "BSL-1.0", "dependencies": {}, @@ -21,7 +21,7 @@ "targetType": "executable", "sourceFiles": ["fluent_assertions.d"], "dependencies": { - "unit-threaded": ">=0.6.27" + "unit-threaded": ">=0.6.35" } } ] diff --git a/example.d b/example.d index 47e1d36..ef07808 100755 --- a/example.d +++ b/example.d @@ -5,7 +5,7 @@ dependency "d-unit" version=">=0.8.0" +/ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2016. +// Copyright Mario Kröplin 2017. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -26,7 +26,7 @@ class Test mixin UnitTest; @Test - public void assertEqualsFailure() + public void assertEqualsFailure() @safe pure { string expected = "bar"; string actual = "baz"; @@ -35,7 +35,7 @@ class Test } @Test - public void assertAssocArrayEqualsFailure() + public void assertAssocArrayEqualsFailure() pure { string[int] expected = [1: "foo", 2: "bar"]; string[int] actual = [1: "foo", 2: "baz"]; @@ -44,7 +44,7 @@ class Test } @Test - public void assertRangeEqualsFailure() + public void assertRangeEqualsFailure() @safe pure { int[] expected = [0, 1, 1, 2]; auto actual = iota(0, 3); @@ -53,7 +53,7 @@ class Test } @Test - public void assertAllFailure() + public void assertAllFailure() @safe { assertAll( assertLessThan(6 * 7, 42), @@ -102,13 +102,13 @@ class TestFixture } @Test - public void test1() + public void test1() @safe pure { debug writeln("@test1()"); } @Test - public void test2() + public void test2() @safe pure { debug writeln("@test2()"); } @@ -137,7 +137,7 @@ class TestingThisAndThat // test function can have default arguments @Test - public void testResult(bool actual = true) + public void testResult(bool actual = true) @safe pure { assertTrue(actual); } @@ -147,7 +147,7 @@ class TestingThisAndThat @Test @Tag("fast") @Tag("smoke") - private void success() + private void success() @safe pure { testResult(true); } @@ -155,21 +155,21 @@ class TestingThisAndThat // disabled test function @Test @Disabled("not ready yet") - public void failure() + public void failure() @safe pure { testResult(false); } // failed contracts are errors, not failures @Test - public void error() + public void error() @safe pure { assert(false); } // expected exception can be further verified @Test - public void testException() + public void testException() @safe pure { import std.exception : enforce; diff --git a/fluent_assertions.d b/fluent_assertions.d index 52a8979..0675ed8 100755 --- a/fluent_assertions.d +++ b/fluent_assertions.d @@ -1,4 +1,4 @@ -// Copyright Mario Kröplin 2016. +// Copyright Mario Kröplin 2017. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -6,7 +6,7 @@ module fluent_assertion; import dunit; -import unit_threaded.should : shouldBeIn, shouldEqual, shouldNotEqual; +import unit_threaded.should; /** * This example demonstrates the reporting of test failures @@ -17,19 +17,19 @@ class Test mixin UnitTest; @Test - public void shouldEqualFailure() + public void shouldEqualFailure() @safe pure { "bar".shouldEqual("baz"); } @Test - public void shouldNotEqualFailure() + public void shouldNotEqualFailure() @safe pure { "foo".shouldNotEqual("foo"); } @Test - public void shouldBeInFailure() + public void shouldBeInFailure() @safe pure { 42.shouldBeIn([0, 1, 2]); } diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 564c356..071b9b0 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2016. +// Copyright Mario Kröplin 2017. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -83,7 +83,7 @@ void assertTrue(T)(T condition, lazy string msg = null, } /// -unittest +@safe pure unittest { assertTrue(true); assertTrue("foo" in ["foo": "bar"]); @@ -108,7 +108,7 @@ void assertFalse(T)(T condition, lazy string msg = null, } /// -unittest +@safe pure unittest { assertFalse(false); assertFalse("foo" in ["bar": "foo"]); @@ -139,7 +139,7 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, } /// -unittest +@safe pure unittest { assertEquals("foo", "foo"); @@ -169,7 +169,7 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, } /// -unittest +@safe /*pure*/ unittest // format is impure for floating point values { assertEquals(1, 1.01); @@ -197,7 +197,7 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, } /// -unittest +@safe pure unittest { assertEquals(42, 42); @@ -207,7 +207,7 @@ unittest } /// -unittest +unittest // Object.opEquals is impure { Object foo = new Object(); Object bar = null; @@ -259,9 +259,9 @@ void assertArrayEquals(T, U, V)(in T[V] expected, in U[V] actual, lazy string ms } /// -unittest +pure unittest // keys, values, byKey, byValue not usable in @safe context { - double[string] expected = ["foo": 1, "bar": 2]; + int[string] expected = ["foo": 1, "bar": 2]; assertArrayEquals(expected, ["foo": 1, "bar": 2]); @@ -302,16 +302,14 @@ void assertRangeEquals(R1, R2)(R1 expected, R2 actual, lazy string msg = null, } /// -unittest +@safe pure unittest { - double[] expected = [0, 1]; + int[] expected = [0, 1]; assertRangeEquals(expected, [0, 1]); AssertException exception; - exception = expectThrows!AssertException(assertRangeEquals(expected, [0, 1.2, 3])); - assertEquals("mismatch at index 1; expected: <1> but was: <1.2>", exception.msg); exception = expectThrows!AssertException(assertRangeEquals(expected, [0])); assertEquals("length mismatch at index 1; expected: <1> but was: empty", exception.msg); exception = expectThrows!AssertException(assertRangeEquals(expected, [0, 1, 2])); @@ -335,7 +333,7 @@ void assertEmpty(T)(T actual, lazy string msg = null, } /// -unittest +@safe pure unittest { assertEmpty([]); @@ -359,7 +357,7 @@ void assertNotEmpty(T)(T actual, lazy string msg = null, } /// -unittest +@safe pure unittest { assertNotEmpty([1, 2, 3]); @@ -383,7 +381,7 @@ void assertNull(T)(T actual, lazy string msg = null, } /// -unittest +@safe pure unittest { Object foo = new Object(); @@ -409,7 +407,7 @@ void assertNotNull(T)(T actual, lazy string msg = null, } /// -unittest +@safe pure unittest { Object foo = new Object(); @@ -438,7 +436,7 @@ void assertSame(T, U)(T expected, U actual, lazy string msg = null, } /// -unittest +unittest // format is impure and not safe for Object { Object foo = new Object(); Object bar = new Object(); @@ -468,7 +466,7 @@ void assertNotSame(T, U)(T expected, U actual, lazy string msg = null, } /// -unittest +@safe pure unittest { Object foo = new Object(); Object bar = new Object(); @@ -484,7 +482,7 @@ unittest * Asserts that all assertions pass. * Throws: AssertAllException otherwise */ -void assertAll(void delegate()[] assertions ...) +void assertAll(void delegate() @safe [] assertions ...) @safe { AssertException[] exceptions = null; @@ -504,7 +502,7 @@ void assertAll(void delegate()[] assertions ...) } /// -unittest +@safe unittest { assertAll( assertTrue(true), @@ -541,7 +539,7 @@ T expectThrows(T : Throwable = Exception, E)(lazy E expression, lazy string msg } /// -unittest +@safe pure unittest { import std.exception : enforce; @@ -551,7 +549,7 @@ unittest } /// -unittest +@safe pure unittest { auto exception = expectThrows!AssertException(expectThrows(42)); @@ -564,13 +562,13 @@ unittest */ void fail(string msg = null, string file = __FILE__, - size_t line = __LINE__) + size_t line = __LINE__) @safe pure { throw new AssertException(msg, file, line); } /// -unittest +@safe pure unittest { auto exception = expectThrows!AssertException(fail()); @@ -606,7 +604,7 @@ template assertOp(string op) } /// -unittest +@safe pure unittest { assertLessThan(2, 3); @@ -616,7 +614,7 @@ unittest } /// -unittest +@safe pure unittest { assertIn("foo", ["foo": "bar"]); diff --git a/src/dunit/diff.d b/src/dunit/diff.d index 9398b55..817850e 100644 --- a/src/dunit/diff.d +++ b/src/dunit/diff.d @@ -1,4 +1,4 @@ -// Copyright Mario Kröplin 2013. +// Copyright Mario Kröplin 2017. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -12,7 +12,7 @@ import std.typecons; /** * Returns a description of the difference between the strings. */ -string description(string expected, string actual) +string description(string expected, string actual) @safe pure { const MAX_LENGTH = 20; auto result = diff(expected, actual); @@ -27,7 +27,7 @@ string description(string expected, string actual) } /// -unittest +@safe pure unittest { assert(description("ab", "Ab") == "expected: <b> but was: <b>"); assert(description("a\nb", "A\nb") == "expected:\n\nb\nbut was:\n\nb"); @@ -60,7 +60,7 @@ Tuple!(string, string) diff(string)(string lhs, string rhs) } /// -unittest +@safe pure unittest { assert(diff("abc", "abc") == tuple("abc", "abc")); // highlight difference From b0c9bf4204948aef778d44a14ba7b79f065028f2 Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 20 Jun 2017 18:01:58 +0200 Subject: [PATCH 60/73] fluent_assertions.d is now a single-file dub package --- fluent_assertions.d | 7 +++++++ src/dunit/assertion.d | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/fluent_assertions.d b/fluent_assertions.d index 0675ed8..4db1cce 100755 --- a/fluent_assertions.d +++ b/fluent_assertions.d @@ -1,3 +1,10 @@ +#!/usr/bin/env dub +/+ dub.sdl: +name "example" +dependency "d-unit" version=">=0.8.0" +dependency "unit-threaded" version=">=0.6.35" ++/ + // Copyright Mario Kröplin 2017. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 071b9b0..3bf2441 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -670,5 +670,5 @@ unittest private string repr(T)(T value) { // format string key with double quotes - return format("%(%s%)", [value]); + return format("%(%s%)", value.only); } From 0737ad479a22f926473714569c164b2147a2b96c Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 10 Nov 2017 10:45:03 +0100 Subject: [PATCH 61/73] fixed style --- src/dunit/assertion.d | 7 +++++-- src/dunit/color.d | 8 +++++++- src/dunit/diff.d | 4 ++-- src/dunit/framework.d | 48 +++++++++++++++++++++++++++++++------------ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 3bf2441..087cff1 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -34,7 +34,7 @@ class AssertException : Exception */ class AssertAllException : AssertException { - AssertException[] exceptions; + private AssertException[] exceptions; @safe pure nothrow this(AssertException[] exceptions, string file = __FILE__, @@ -48,7 +48,7 @@ class AssertAllException : AssertException super(msg, file, line, next); } - @safe pure nothrow static string heading(size_t count) + private @safe pure nothrow static string heading(size_t count) { if (count == 1) return "1 assertion failure:"; @@ -57,6 +57,9 @@ class AssertAllException : AssertException } } +/** + * Returns a description of the throwable. + */ @safe pure nothrow string description(Throwable throwable) { with (throwable) diff --git a/src/dunit/color.d b/src/dunit/color.d index 9ba9151..b7f0f1e 100644 --- a/src/dunit/color.d +++ b/src/dunit/color.d @@ -11,6 +11,9 @@ enum Color { red, green, yellow, onRed, onGreen, onYellow } version (Posix) { + /** + * Writes the text in the given color, if possible. + */ public void writec(Color color, string text) { if (canUseColor()) @@ -57,7 +60,7 @@ version (Posix) if (!computed) { // disable colors if the output is written to a file or pipe instead of a tty - import core.sys.posix.unistd; + import core.sys.posix.unistd : isatty; useColor = isatty(stdout.fileno()) != 0; computed = true; @@ -68,6 +71,9 @@ version (Posix) version (Windows) { + /** + * Writes the text in the given color, if possible. + */ public void writec(Color color, string text) { if (canUseColor()) diff --git a/src/dunit/diff.d b/src/dunit/diff.d index 817850e..0d3eb56 100644 --- a/src/dunit/diff.d +++ b/src/dunit/diff.d @@ -15,8 +15,8 @@ import std.typecons; string description(string expected, string actual) @safe pure { const MAX_LENGTH = 20; - auto result = diff(expected, actual); - bool oneLiner = max(result[0].length, result[1].length) <= MAX_LENGTH + const result = diff(expected, actual); + const oneLiner = max(result[0].length, result[1].length) <= MAX_LENGTH && !result[0].canFind("\n", "\r") && !result[1].canFind("\n", "\r"); diff --git a/src/dunit/framework.d b/src/dunit/framework.d index be2339e..c265ef6 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -50,11 +50,14 @@ mixin template Main() } } +/** + * Runs the tests according to the command-line arguments. + */ public int dunit_main(string[] args) { - import std.getopt; - import std.path; - import std.regex; + import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult; + import std.path : baseName; + import std.regex : match; GetoptResult result; string[] filters = null; @@ -341,6 +344,10 @@ body private __gshared TestListener[] testListeners = null; +/** + * Registered implementations of this interface will be notified + * about events that occur during the test run. + */ interface TestListener { public void enterClass(string className); @@ -377,6 +384,9 @@ interface TestListener } } +/** + * Writes a "progress bar", followed by the errors and the failures. + */ class IssueReporter : TestListener { private struct Issue @@ -468,6 +478,9 @@ class IssueReporter : TestListener } } +/** + * Writes a detailed test report. + */ class DetailReporter : TestListener { private string test; @@ -511,7 +524,7 @@ class DetailReporter : TestListener { if (success) { - double elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0; + const elapsed = (TickDuration.currSystemTick() - this.startTime).usecs() / 1_000.0; writec(Color.green, " OK: "); writefln("%6.2f ms %s", elapsed, this.test); @@ -529,6 +542,9 @@ class DetailReporter : TestListener } } +/** + * Writes a summary about the tests run. + */ class ResultReporter : TestListener { private uint tests = 0; @@ -590,9 +606,12 @@ class ResultReporter : TestListener } } +/** + * Writes progressive XML output. + */ class XmlReporter : TestListener { - import std.xml; + import std.xml : Document, Element, Tag; private Document testCase; private string className; @@ -622,7 +641,7 @@ class XmlReporter : TestListener public void addFailure(string phase, AssertException exception) { auto element = new Element("failure"); - string message = format("%s %s", phase, exception.description); + const message = format("%s %s", phase, exception.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -631,7 +650,7 @@ class XmlReporter : TestListener public void addError(string phase, Throwable throwable) { auto element = new Element("error", throwable.info.toString); - string message = format("%s %s", phase, throwable.description); + const message = format("%s %s", phase, throwable.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -639,7 +658,7 @@ class XmlReporter : TestListener public void exitTest(bool success) { - double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; + const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; this.testCase.tag.attr["time"] = format("%.3f", elapsed); @@ -654,9 +673,12 @@ class XmlReporter : TestListener } } +/** + * Writes a JUnit-style XML test report. + */ class ReportReporter : TestListener { - import std.xml; + import std.xml : Document, Element, Tag; private string fileName; private Document document; @@ -706,7 +728,7 @@ class ReportReporter : TestListener if (this.testCase.elements.empty) { auto element = new Element("failure"); - string message = format("%s %s", phase, exception.description); + const message = format("%s %s", phase, exception.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -719,7 +741,7 @@ class ReportReporter : TestListener if (this.testCase.elements.empty) { auto element = new Element("error", throwable.info.toString); - string message = format("%s %s", phase, throwable.description); + const message = format("%s %s", phase, throwable.description); element.tag.attr["message"] = message; this.testCase ~= element; @@ -728,14 +750,14 @@ class ReportReporter : TestListener public void exitTest(bool success) { - double elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; + const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; this.testCase.tag.attr["time"] = format("%.3f", elapsed); } public void exit() { - import std.file; + import std.file : write; string report = join(this.document.pretty(4), "\n") ~ "\n"; From c59d6da1e47074038fbf01caf50f2bff14f6d6d6 Mon Sep 17 00:00:00 2001 From: linkrope Date: Fri, 10 Nov 2017 18:15:20 +0100 Subject: [PATCH 62/73] simplified DUB package configuration --- README.md | 7 +------ dub.json | 21 +-------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 31c6d75..ab54b03 100644 --- a/README.md +++ b/README.md @@ -107,11 +107,6 @@ Or just focus on the issues: ./example.d --filter Test.assert --filter error -Alternatively, build and run the example using -[dub](https://github.com/rejectedsoftware/dub): - - dub --build=plain --config=example -- --verbose - "Next Generation" ----------------- @@ -135,7 +130,7 @@ The xUnit Testing Framework also supports the "fluent assertions" from For an example, have a look at [fluent-assertions](fluent_assertions.d). Build and run the example using - dub --config=fluent-assertions + ./fluent_assertions.d (When you get three failures, everything works fine.) diff --git a/dub.json b/dub.json index 3e81ca1..462be31 100644 --- a/dub.json +++ b/dub.json @@ -5,24 +5,5 @@ "authors": ["Juan Manuel Cabo", "Mario Kröplin"], "license" : "BSL-1.0", "dependencies": {}, - "configurations": - [ - { - "name": "library", - "targetType": "library" - }, - { - "name": "example", - "targetType": "executable", - "sourceFiles": ["example.d"] - }, - { - "name": "fluent-assertions", - "targetType": "executable", - "sourceFiles": ["fluent_assertions.d"], - "dependencies": { - "unit-threaded": ">=0.6.35" - } - } - ] + "targetType": "library" } From 778da4a0e92f432930a85a3b323fc3733e67dd06 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jan 2018 20:56:30 +0100 Subject: [PATCH 63/73] Create directories of report filePath if necessary --- src/dunit/framework.d | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index c265ef6..3c4586a 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -757,9 +757,14 @@ class ReportReporter : TestListener public void exit() { - import std.file : write; + import std.file : write, mkdirRecurse, exists; + import std.path: dirName; string report = join(this.document.pretty(4), "\n") ~ "\n"; + string dirPath = dirName(this.fileName); + + if (!exists(dirPath)) + mkdirRecurse(dirPath); write(this.fileName, report); } From cdb1cba9871d868e13ff0d51c88afb9e2d1627b1 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 28 Jan 2018 18:03:52 +0100 Subject: [PATCH 64/73] fixed style --- src/dunit/framework.d | 91 +++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 3c4586a..ed72964 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -1,5 +1,5 @@ // Copyright Juan Manuel Cabo 2012. -// Copyright Mario Kröplin 2016. +// Copyright Mario Kröplin 2018. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -360,7 +360,7 @@ interface TestListener public static string prettyOrigin(string className, string test, string phase) { - string origin = prettyOrigin(test, phase); + const origin = prettyOrigin(test, phase); if (origin.startsWith('@')) return className ~ origin; @@ -402,40 +402,40 @@ class IssueReporter : TestListener private string className; private string test; - public void enterClass(string className) + public override void enterClass(string className) { this.className = className; } - public void enterTest(string test) + public override void enterTest(string test) { this.test = test; } - public void skip(string reason) + public override void skip(string reason) { writec(Color.onYellow, "S"); } - public void addFailure(string phase, AssertException exception) + public override void addFailure(string phase, AssertException exception) { this.failures ~= Issue(this.className, this.test, phase, exception); writec(Color.onRed, "F"); } - public void addError(string phase, Throwable throwable) + public override void addError(string phase, Throwable throwable) { this.errors ~= Issue(this.className, this.test, phase, throwable); writec(Color.onRed, "E"); } - public void exitTest(bool success) + public override void exitTest(bool success) { if (success) writec(Color.onGreen, "."); } - public void exit() + public override void exit() { writeln(); @@ -486,18 +486,18 @@ class DetailReporter : TestListener private string test; private TickDuration startTime; - public void enterClass(string className) + public override void enterClass(string className) { writeln(className); } - public void enterTest(string test) + public override void enterTest(string test) { this.test = test; this.startTime = TickDuration.currSystemTick(); } - public void skip(string reason) + public override void skip(string reason) { writec(Color.yellow, " SKIP: "); writeln(this.test); @@ -505,14 +505,14 @@ class DetailReporter : TestListener writeln(indent(format(`"%s"`, reason))); } - public void addFailure(string phase, AssertException exception) + public override void addFailure(string phase, AssertException exception) { writec(Color.red, " FAILURE: "); writeln(prettyOrigin(this.test, phase)); writeln(indent(exception.description)); } - public void addError(string phase, Throwable throwable) + public override void addError(string phase, Throwable throwable) { writec(Color.red, " ERROR: "); writeln(prettyOrigin(this.test, phase)); @@ -520,7 +520,7 @@ class DetailReporter : TestListener writeln("----------------"); } - public void exitTest(bool success) + public override void exitTest(bool success) { if (success) { @@ -531,12 +531,12 @@ class DetailReporter : TestListener } } - public void exit() + public override void exit() { // do nothing } - private string indent(string s, string indent = " ") + private static string indent(string s, string indent = " ") { return s.splitLines(KeepTerminator.yes).map!(line => indent ~ line).join; } @@ -552,42 +552,42 @@ class ResultReporter : TestListener private uint errors = 0; private uint skips = 0; - public void enterClass(string className) + public override void enterClass(string className) { // do nothing } - public void enterTest(string test) + public override void enterTest(string test) { ++this.tests; } - public void skip(string reason) + public override void skip(string reason) { ++this.skips; } - public void addFailure(string phase, AssertException exception) + public override void addFailure(string phase, AssertException exception) { ++this.failures; } - public void addError(string phase, Throwable throwable) + public override void addError(string phase, Throwable throwable) { ++this.errors; } - public void exitTest(bool success) + public override void exitTest(bool success) { // do nothing } - public void exit() + public override void exit() { // do nothing } - public void write() + public void write() const { writeln(); writefln("Tests run: %d, Failures: %d, Errors: %d, Skips: %d", @@ -617,12 +617,12 @@ class XmlReporter : TestListener private string className; private TickDuration startTime; - public void enterClass(string className) + public override void enterClass(string className) { this.className = className; } - public void enterTest(string test) + public override void enterTest(string test) { this.testCase = new Document(new Tag("testcase")); this.testCase.tag.attr["classname"] = this.className; @@ -630,7 +630,7 @@ class XmlReporter : TestListener this.startTime = TickDuration.currSystemTick(); } - public void skip(string reason) + public override void skip(string reason) { auto element = new Element("skipped"); @@ -638,7 +638,7 @@ class XmlReporter : TestListener this.testCase ~= element; } - public void addFailure(string phase, AssertException exception) + public override void addFailure(string phase, AssertException exception) { auto element = new Element("failure"); const message = format("%s %s", phase, exception.description); @@ -647,7 +647,7 @@ class XmlReporter : TestListener this.testCase ~= element; } - public void addError(string phase, Throwable throwable) + public override void addError(string phase, Throwable throwable) { auto element = new Element("error", throwable.info.toString); const message = format("%s %s", phase, throwable.description); @@ -656,18 +656,18 @@ class XmlReporter : TestListener this.testCase ~= element; } - public void exitTest(bool success) + public override void exitTest(bool success) { const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; this.testCase.tag.attr["time"] = format("%.3f", elapsed); - string report = join(this.testCase.pretty(4), "\n"); + const report = join(this.testCase.pretty(4), "\n"); writeln(report); } - public void exit() + public override void exit() { // do nothing } @@ -680,7 +680,7 @@ class ReportReporter : TestListener { import std.xml : Document, Element, Tag; - private string fileName; + private const string fileName; private Document document; private Element testSuite; private Element testCase; @@ -696,12 +696,12 @@ class ReportReporter : TestListener this.document ~= this.testSuite; } - public void enterClass(string className) + public override void enterClass(string className) { this.className = className; } - public void enterTest(string test) + public override void enterTest(string test) { this.testCase = new Element("testcase"); this.testCase.tag.attr["classname"] = this.className; @@ -710,7 +710,7 @@ class ReportReporter : TestListener this.startTime = TickDuration.currSystemTick(); } - public void skip(string reason) + public override void skip(string reason) { // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) @@ -722,7 +722,7 @@ class ReportReporter : TestListener } } - public void addFailure(string phase, AssertException exception) + public override void addFailure(string phase, AssertException exception) { // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) @@ -735,7 +735,7 @@ class ReportReporter : TestListener } } - public void addError(string phase, Throwable throwable) + public override void addError(string phase, Throwable throwable) { // avoid wrong interpretation of more than one child if (this.testCase.elements.empty) @@ -748,24 +748,21 @@ class ReportReporter : TestListener } } - public void exitTest(bool success) + public override void exitTest(bool success) { const elapsed = (TickDuration.currSystemTick() - this.startTime).msecs() / 1_000.0; this.testCase.tag.attr["time"] = format("%.3f", elapsed); } - public void exit() + public override void exit() { - import std.file : write, mkdirRecurse, exists; + import std.file : mkdirRecurse, write; import std.path: dirName; - string report = join(this.document.pretty(4), "\n") ~ "\n"; - string dirPath = dirName(this.fileName); - - if (!exists(dirPath)) - mkdirRecurse(dirPath); + const report = join(this.document.pretty(4), "\n") ~ "\n"; + mkdirRecurse(this.fileName.dirName); write(this.fileName, report); } } From 8aef48d791445f2519f52c8fb7940a6f6422c14e Mon Sep 17 00:00:00 2001 From: andre2007 Date: Thu, 5 Apr 2018 21:29:46 +0200 Subject: [PATCH 65/73] Add default test suite argument --- src/dunit/framework.d | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index ed72964..638102c 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -67,6 +67,7 @@ public int dunit_main(string[] args) string report = null; bool verbose = false; bool xml = false; + string defaultTestSuite = "dunit"; try { @@ -79,6 +80,7 @@ public int dunit_main(string[] args) "verbose|v", "Display more information as the tests are run", &verbose, "xml", "Display progressive XML output", &xml, "report", "Write JUnit-style XML test report", &report, + "defaultTestSuite", "Test suite name to be used in JUnit-style XML test report", &defaultTestSuite, ); } catch (Exception exception) @@ -167,7 +169,7 @@ public int dunit_main(string[] args) } if (!report.empty) - testListeners ~= new ReportReporter(report); + testListeners ~= new ReportReporter(report, defaultTestSuite); auto reporter = new ResultReporter(); @@ -687,12 +689,12 @@ class ReportReporter : TestListener private string className; private TickDuration startTime; - public this(string fileName) + public this(string fileName, string defaultTestSuite) { this.fileName = fileName; this.document = new Document(new Tag("testsuites")); this.testSuite = new Element("testsuite"); - this.testSuite.tag.attr["name"] = "dunit"; + this.testSuite.tag.attr["name"] = defaultTestSuite; this.document ~= this.testSuite; } From e78971a27395169158458e3ab1c35b61c67079f4 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 8 Apr 2018 22:13:54 +0200 Subject: [PATCH 66/73] simplified naming --- src/dunit/framework.d | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 638102c..4ce4951 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -67,7 +67,7 @@ public int dunit_main(string[] args) string report = null; bool verbose = false; bool xml = false; - string defaultTestSuite = "dunit"; + string testSuiteName = "dunit"; try { @@ -80,7 +80,7 @@ public int dunit_main(string[] args) "verbose|v", "Display more information as the tests are run", &verbose, "xml", "Display progressive XML output", &xml, "report", "Write JUnit-style XML test report", &report, - "defaultTestSuite", "Test suite name to be used in JUnit-style XML test report", &defaultTestSuite, + "testsuite", "Provide a test-suite name for the JUnit-style XML test report", &testSuiteName, ); } catch (Exception exception) @@ -169,7 +169,7 @@ public int dunit_main(string[] args) } if (!report.empty) - testListeners ~= new ReportReporter(report, defaultTestSuite); + testListeners ~= new ReportReporter(report, testSuiteName); auto reporter = new ResultReporter(); @@ -689,12 +689,12 @@ class ReportReporter : TestListener private string className; private TickDuration startTime; - public this(string fileName, string defaultTestSuite) + public this(string fileName, string testSuiteName) { this.fileName = fileName; this.document = new Document(new Tag("testsuites")); this.testSuite = new Element("testsuite"); - this.testSuite.tag.attr["name"] = defaultTestSuite; + this.testSuite.tag.attr["name"] = testSuiteName; this.document ~= this.testSuite; } From 9fb7902dec13951728019ad441bb37914cd7ad4a Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 23 Feb 2020 13:17:11 +0100 Subject: [PATCH 67/73] Fix #27 --- src/dunit/framework.d | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 4ce4951..bdd16c8 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -817,42 +817,30 @@ mixin template UnitTest() testClass.disabled = _attributeByMember!(typeof(this), Disabled); testClass.tags = _attributesByMember!(typeof(this), Tag); - static Object create() + testClass.create = () { mixin("return new " ~ typeof(this).stringof ~ "();"); - } - - static void beforeAll() + }; + testClass.beforeAll = () { mixin(_staticSequence(_members!(typeof(this), BeforeAll))); - } - - static void beforeEach(Object o) + }; + testClass.beforeEach = (Object o) { mixin(_sequence(_members!(typeof(this), BeforeEach))); - } - - void test(Object o, string name) + }; + testClass.test = (Object o, string name) { mixin(_choice(_members!(typeof(this), Test))); - } - - static void afterEach(Object o) + }; + testClass.afterEach = (Object o) { mixin(_sequence(_members!(typeof(this), AfterEach))); - } - - static void afterAll() + }; + testClass.afterAll = () { mixin(_staticSequence(_members!(typeof(this), AfterAll))); - } - - testClass.create = &create; - testClass.beforeAll = &beforeAll; - testClass.beforeEach = &beforeEach; - testClass.test = &test; - testClass.afterEach = &afterEach; - testClass.afterAll = &afterAll; + }; testClasses ~= testClass; } From 0f35253e4dbdf260e72d73f8653732e57eb045be Mon Sep 17 00:00:00 2001 From: linkrope Date: Sun, 15 Mar 2020 18:51:53 +0100 Subject: [PATCH 68/73] Fix #28: write file and line in hyperlink format --- src/dunit/assertion.d | 23 ++++++++++++++++++++++- src/dunit/framework.d | 12 +++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index 087cff1..c06a3a8 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -62,12 +62,33 @@ class AssertAllException : AssertException */ @safe pure nothrow string description(Throwable throwable) { + import std.path : baseName, buildPath; + with (throwable) { if (file.empty) return text(typeid(throwable).name, ": ", msg); + else if (file.baseName == file) // "foo.d:42" is not rendered as link + return text(buildPath(".", file), ":", line, " ", typeid(throwable).name, ": ", msg); else - return text(typeid(throwable).name, "@", file, "(", line, "): ", msg); + return text(file, ":", line, " ", typeid(throwable).name, ": ", msg); + } +} + +/** + * Writes the optional trace info of a throwable. + */ +void description(Output)(auto ref Output output, Throwable.TraceInfo traceInfo) +{ + if (traceInfo !is null) + { + output.put("----------------\n"); + foreach (line; traceInfo) + { + output.put(line); + output.put("\n"); + } + output.put("----------------\n"); } } diff --git a/src/dunit/framework.d b/src/dunit/framework.d index bdd16c8..8c39a52 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -454,8 +454,8 @@ class IssueReporter : TestListener { writefln("%d) %s", i + 1, prettyOrigin(issue.testClass, issue.test, issue.phase)); - writeln(issue.throwable.toString); - writeln("----------------"); + writeln(issue.throwable.description); + stdout.lockingTextWriter.description(issue.throwable.info); } } @@ -470,11 +470,9 @@ class IssueReporter : TestListener foreach (i, issue; this.failures) { - Throwable throwable = issue.throwable; - writefln("%d) %s", i + 1, prettyOrigin(issue.testClass, issue.test, issue.phase)); - writeln(throwable.description); + writeln(issue.throwable.description); } } } @@ -518,8 +516,8 @@ class DetailReporter : TestListener { writec(Color.red, " ERROR: "); writeln(prettyOrigin(this.test, phase)); - writeln(" ", throwable.toString); - writeln("----------------"); + writeln(indent(throwable.description)); + stdout.lockingTextWriter.description(throwable.info); } public override void exitTest(bool success) From 04d02c2dc68c0767408b7924dad67dd8a8dfee27 Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 11 Jul 2020 23:17:47 +0200 Subject: [PATCH 69/73] Fix #29 using undeaD --- dub.json | 4 +++- src/dunit/framework.d | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dub.json b/dub.json index 462be31..33dbc68 100644 --- a/dub.json +++ b/dub.json @@ -4,6 +4,8 @@ "copyright": "Copyright © 2017, Mario Kröplin", "authors": ["Juan Manuel Cabo", "Mario Kröplin"], "license" : "BSL-1.0", - "dependencies": {}, + "dependencies": { + "undead": ">=1.1.1" + }, "targetType": "library" } diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 8c39a52..1fa4345 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -611,7 +611,7 @@ class ResultReporter : TestListener */ class XmlReporter : TestListener { - import std.xml : Document, Element, Tag; + import undead.xml : Document, Element, Tag; private Document testCase; private string className; @@ -678,7 +678,7 @@ class XmlReporter : TestListener */ class ReportReporter : TestListener { - import std.xml : Document, Element, Tag; + import undead.xml : Document, Element, Tag; private const string fileName; private Document document; From 0d952fbc582d7a0e7df37b9c3bc25f5ce8839e0f Mon Sep 17 00:00:00 2001 From: linkrope Date: Sat, 11 Jul 2020 23:52:58 +0200 Subject: [PATCH 70/73] Use dshould for fluent assertions --- README.md | 94 +++++++++++++++++++++++-------------------- dub.json | 4 ++ fluent_assertions.d | 14 +++---- src/dunit/framework.d | 6 +-- 4 files changed, 64 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index ab54b03..6dfa203 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,27 @@ -xUnit Testing Framework for D -============================= +# xUnit Testing Framework for D [![Build Status](https://travis-ci.org/linkrope/dunit.svg?branch=master)](https://travis-ci.org/linkrope/dunit) This is a simple implementation of the xUnit Testing Framework -for the [D Programming Language](http://dlang.org). -Being based on [JUnit](http://junit.org) it allows to organize tests -according to the [xUnit Test Patterns](http://xunitpatterns.com). +for the [D Programming Language]. +Being based on [JUnit] it allows to organize tests +according to the [xUnit Test Patterns]. -Looking for a replacement of -[DUnit](http://www.dsource.org/projects/dmocks/wiki/DUnit) for D1, -I found [jmcabo/dunit](https://github.com/jmcabo/dunit) promising. +Looking for a replacement of [DUnit] for D1, I found [jmcabo/dunit] promising. First, I had to fix some issues, but by now the original implementation has been largely revised. -Testing Functions vs. Interactions ----------------------------------- +## Testing Functions vs. Interactions D's built-in support for unittests is best suited for testing functions, when the test cases can be expressed as one-liners. -(Have a look at the documented unittests for the -[`dunit.assertion`](src/dunit/assertion.d) functions.) +(Have a look at the documented unittests for the [`dunit.assertion`] functions.) But you're on your own, when you have to write a lot more code per test case, for example for testing interactions of objects. So, here is what the xUnit Testing Framework has to offer: + - tests are organized in classes - tests are always named - tests can reuse a shared fixture @@ -33,8 +29,7 @@ So, here is what the xUnit Testing Framework has to offer: - you see all failed tests at once - you get more information about failures -Failures vs. Errors -------------------- +## Failures vs. Errors Specialized assertion functions provide more information about failures than the built-in `assert` expression. @@ -51,8 +46,7 @@ The more general assertOp!">="(a, b); // alias assertGreaterThanOrEqual -(borrowed from -[Issue 4653](http://d.puremagic.com/issues/show_bug.cgi?id=4653)) +(borrowed from [Issue 4653]) will at least report the concrete values in case of a failure: condition (2 >= 3) not satisfied @@ -63,11 +57,10 @@ violated contracts and other exceptions from deep down the unit under test you may wish for the stack trace. That's why the xUnit Testing Framework distinguishes failures from errors, -and why [`dunit.assertion`](src/dunit/assertion.d) doesn't use `AssertError` +and why [`dunit.assertion`] doesn't use `AssertError` but introduces its own `AssertException`. -User Defined Attributes ------------------------ +## User Defined Attributes Thanks to D's User Defined Attributes, test names no longer have to start with "test". @@ -75,25 +68,22 @@ Thanks to D's User Defined Attributes, test names no longer have to start with Put `mixin UnitTest;` in your test class and attach `@Test`, `@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, `@Tag("...")`, and `@Disabled("...")` -(borrowed from [JUnit 5](http://junit.org/junit5/docs/current/user-guide/)) -to the member functions to state their purpose. +(borrowed from [JUnit 5]) to the member functions to state their purpose. -Test Results ------------- +## Test Results Test results are reported while the tests are run. A "progress bar" is written with a `.` for each passed test, an `F` for each failure, an `E` for each error, and an `S` for each skipped test. -In addition, an XML test report is available that uses the JUnitReport format. -The continuous integration tool [Jenkins](http://jenkins-ci.org), for example, -understands this JUnitReport format. Thus, Jenkins can be used to browse +In addition, an XML test report is available that uses the _JUnitReport_ format. +The continuous integration tool [Jenkins], for example, +understands this _JUnitReport_ format. Thus, Jenkins can be used to browse test reports, track failures and errors, and even provide trends over time. -Examples --------- +## Examples -Run the included [example](example.d) to see the xUnit Testing Framework in action: +Run the included [example] to see the xUnit Testing Framework in action: ./example.d @@ -107,8 +97,7 @@ Or just focus on the issues: ./example.d --filter Test.assert --filter error -"Next Generation" ------------------ +## "Next Generation" JUnit's `assertEquals(expected, actual)` got changed into TestNG's `assertEquals(actual, expected)`, which feels more natural. @@ -121,28 +110,45 @@ So, if you prefer TestNG's order of arguments, import `dunit.ng` or `dunit.ng.assertion` instead of the conventional `dunit` and `dunit.assertion`. -Fluent Assertions ------------------ +## Fluent Assertions -The xUnit Testing Framework also supports the "fluent assertions" from -[unit-threaded](https://github.com/atilaneves/unit-threaded). +The xUnit Testing Framework also supports the "fluent assertions" from [dshould]. -For an example, have a look at [fluent-assertions](fluent_assertions.d). +For an example, have a look at [fluent-assertions]. Build and run the example using ./fluent_assertions.d (When you get three failures, everything works fine.) -Related Projects ----------------- +## Related Projects -- [DMocks-revived](https://github.com/QAston/DMocks-revived): +- [DMocks-revived]: a mock-object framework that allows to mock interfaces or classes -- [specd](https://github.com/jostly/specd): - a unit testing framework inspired by [specs2](http://etorreborre.github.io/specs2/) - and [ScalaTest](http://www.scalatest.org) -- [DUnit](https://github.com/kalekold/dunit): +- [specd]: + a unit testing framework inspired by [specs2] and [ScalaTest] +- [DUnit]: a toolkit of test assertions and a template mixin to enable mocking -- [unit-threaded](https://github.com/atilaneves/unit-threaded): +- [unit-threaded]: a multi-threaded unit testing framework + +[d programming language]: http://dlang.org +[dunit]: http://www.dsource.org/projects/dmocks/wiki/DUnit +[issue 4653]: http://d.puremagic.com/issues/show_bug.cgi?id=4653 +[jenkins]: http://jenkins-ci.org +[junit]: http://junit.org +[junit 5]: http://junit.org/junit5/docs/current/user-guide/ +[scalatest]: http://www.scalatest.org +[specs2]: http://etorreborre.github.io/specs2/ +[xunit test patterns]: http://xunitpatterns.com + +[dmocks-revived]: https://github.com/QAston/DMocks-revived +[dshould]: https://github.com/funkwerk/dshould +[jmcabo/dunit]: https://github.com/jmcabo/dunit +[dunit]: https://github.com/kalekold/dunit +[specd]: https://github.com/jostly/specd +[unit-threaded]: https://github.com/atilaneves/unit-threaded + +[`dunit.assertion`]: src/dunit/assertion.d +[example]: example.d +[fluent-assertions]: fluent_assertions.d diff --git a/dub.json b/dub.json index 33dbc68..b5275f7 100644 --- a/dub.json +++ b/dub.json @@ -5,6 +5,10 @@ "authors": ["Juan Manuel Cabo", "Mario Kröplin"], "license" : "BSL-1.0", "dependencies": { + "dshould": { + "optional": true, + "version": ">=1.3.2" + }, "undead": ">=1.1.1" }, "targetType": "library" diff --git a/fluent_assertions.d b/fluent_assertions.d index 4db1cce..90ad729 100755 --- a/fluent_assertions.d +++ b/fluent_assertions.d @@ -2,10 +2,10 @@ /+ dub.sdl: name "example" dependency "d-unit" version=">=0.8.0" -dependency "unit-threaded" version=">=0.6.35" +dependency "dshould" version=">=1.3.2" +/ -// Copyright Mario Kröplin 2017. +// Copyright Mario Kröplin 2020. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) @@ -13,11 +13,11 @@ dependency "unit-threaded" version=">=0.6.35" module fluent_assertion; import dunit; -import unit_threaded.should; +import dshould; /** * This example demonstrates the reporting of test failures - * with unit-threaded's fluent assertions. + * with dshould's fluent assertions. */ class Test { @@ -26,19 +26,19 @@ class Test @Test public void shouldEqualFailure() @safe pure { - "bar".shouldEqual("baz"); + "bar".should.equal("baz"); } @Test public void shouldNotEqualFailure() @safe pure { - "foo".shouldNotEqual("foo"); + "foo".should.not.equal("foo"); } @Test public void shouldBeInFailure() @safe pure { - 42.shouldBeIn([0, 1, 2]); + [0, 1, 2].should.contain(42); } } diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 1fa4345..df290be 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -239,15 +239,15 @@ body { try { - static if (__traits(compiles, { import unit_threaded.should : UnitTestException; })) + version(Have_dshould) { - import unit_threaded.should : UnitTestException; + import dshould.ShouldType : FluentException; try { action(); } - catch (UnitTestException exception) + catch (FluentException exception) { // convert exception to "fix" the message format throw new AssertException('\n' ~ exception.msg, From 2d5a5ee393debad4f79f9cd4d1178ec825a2e728 Mon Sep 17 00:00:00 2001 From: linkrope Date: Tue, 29 Dec 2020 17:19:14 +0100 Subject: [PATCH 71/73] Conditional disabled/enabled attributes (#33) * Add enabled/disabled attributes * Add enabled/disabled attributes * Update README.md * Review comments * Extract helper function Co-authored-by: Andre Pany Co-authored-by: linkrope --- .gitignore | 2 + README.md | 10 +++-- example.d | 81 ++++++++++++++++++++++++++++++++++++ src/dunit/attributes.d | 46 +++++++++++++++++++++ src/dunit/framework.d | 93 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 225 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 5caff3b..af00a57 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ main exampleTests *.o *.swp +*.exe +.dub diff --git a/README.md b/README.md index 6dfa203..5820f7c 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,12 @@ Thanks to D's User Defined Attributes, test names no longer have to start with "test". Put `mixin UnitTest;` in your test class and attach `@Test`, -`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, `@Tag("...")`, -and `@Disabled("...")` +`@BeforeEach`, `@AfterEach`, `@BeforeAll`, `@AfterAll`, +`@Tag("...")`, `@Disabled("...")`, +`@DisabledIf(() => ..., "...")`, `@EnabledIf(() => ..., "...")`, +`@DisabledIfEnvironmentVariable("VARIABLE", "pattern")`, +`@EnabledIfEnvironmentVariable("VARIABLE", "pattern")`, +`@DisabledOnOs(OS.win32, OS.win64)`, `@EnabledOnOs(OS.linux)` (borrowed from [JUnit 5]) to the member functions to state their purpose. ## Test Results @@ -87,7 +91,7 @@ Run the included [example] to see the xUnit Testing Framework in action: ./example.d -(When you get four failures, one error, and one skip, everything works fine.) +(When you get four failures, one error, and six skips, everything works fine.) Have a look at the debug output of the example in "verbose" style: diff --git a/example.d b/example.d index ef07808..02a0ffb 100755 --- a/example.d +++ b/example.d @@ -17,6 +17,7 @@ import core.thread; import core.time; import std.range; import std.stdio; +import std.system : os; /** * This example demonstrates the reporting of test failures. @@ -160,6 +161,86 @@ class TestingThisAndThat testResult(false); } + // disabled, because condition is true + @Test + @DisabledIf(() => true, "disabled by condition") + public void disabledByCondition() @safe pure + { + testResult(false); + } + + // not disabled, because condition is false + @Test + @DisabledIf(() => false, "disabled by condition") + public void notDisabledByCondition() @safe pure + { + testResult(true); + } + + // not disabled, because condition is true + @Test + @EnabledIf(() => true, "not enabled by condition") + public void enabledByCondition() @safe pure + { + testResult(true); + } + + // disabled, because condition is false + @Test + @EnabledIf(() => false, "not enabled by condition") + public void notEnabledByCondition() @safe pure + { + testResult(false); + } + + // disabled, because environment variable matches pattern + @Test + @DisabledIfEnvironmentVariable("PATH", ".*") + public void disabledByEnvironmentVariable() @safe pure + { + testResult(false); + } + + // not disabled, because environment variable does not match pattern + @Test + @DisabledIfEnvironmentVariable("PATH", "42") + public void notDisabledByEnvironmentVariable() @safe pure + { + testResult(true); + } + + // not disabled, because environment variable matches pattern + @Test + @EnabledIfEnvironmentVariable("PATH", ".*") + public void enabledByEnvironmentVariable() @safe pure + { + testResult(true); + } + + // disabled, because environment variable does not match pattern + @Test + @EnabledIfEnvironmentVariable("PATH", "42") + public void notEnabledByEnvironmentVariable() @safe pure + { + testResult(false); + } + + // disabled on the operating system on which the program runs + @Test + @DisabledOnOs(os) + public void disabledByOs() @safe pure + { + testResult(false); + } + + // not disabled on the operating system on which the program runs + @Test + @EnabledOnOs(os) + public void enabledByOs() @safe pure + { + testResult(true); + } + // failed contracts are errors, not failures @Test public void error() @safe pure diff --git a/src/dunit/attributes.d b/src/dunit/attributes.d index 942966a..527360c 100644 --- a/src/dunit/attributes.d +++ b/src/dunit/attributes.d @@ -1,5 +1,7 @@ module dunit.attributes; +import std.system : OS; + enum AfterEach; enum AfterAll; enum BeforeEach; @@ -16,6 +18,50 @@ struct Tag string name; } +struct EnabledIf +{ + bool function() condition; + string reason; +} + +struct DisabledIf +{ + bool function() condition; + string reason; +} + +struct EnabledIfEnvironmentVariable +{ + string named; + string matches = ".*"; +} + +struct DisabledIfEnvironmentVariable +{ + string named; + string matches = ".*"; +} + +struct EnabledOnOs +{ + OS[] value; + + this(OS[] value...) + { + this.value = value; + } +} + +struct DisabledOnOs +{ + OS[] value; + + this(OS[] value...) + { + this.value = value; + } +} + deprecated("use AfterEach instead") alias After = AfterEach; deprecated("use AfterAll instead") alias AfterClass = AfterAll; deprecated("use BeforeEach instead") alias Before = BeforeEach; diff --git a/src/dunit/framework.d b/src/dunit/framework.d index df290be..46bf0d0 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -24,6 +24,12 @@ struct TestClass string name; string[] tests; Disabled[string] disabled; + EnabledIf[string] enabledIf; + DisabledIf[string] disabledIf; + EnabledIfEnvironmentVariable[string] enabledIfEnvVar; + DisabledIfEnvironmentVariable[string] disabledIfEnvVar; + EnabledOnOs[string] enabledOnOs; + DisabledOnOs[string] disabledOnOs; Tag[][string] tags; Object function() create; @@ -57,7 +63,7 @@ public int dunit_main(string[] args) { import std.getopt : config, defaultGetoptPrinter, getopt, GetoptResult; import std.path : baseName; - import std.regex : match; + import std.regex : matchFirst; GetoptResult result; string[] filters = null; @@ -116,7 +122,7 @@ public int dunit_main(string[] args) { string fullyQualifiedName = testClass.name ~ '.' ~ test; - if (match(fullyQualifiedName, filter)) + if (matchFirst(fullyQualifiedName, filter)) { auto foundTestSelections = testSelections.find!"a.testClass.name == b"(testClass.name); @@ -293,9 +299,13 @@ body foreach (testListener; testListeners) testListener.exitTest(success); - if (test in testClass.disabled || (initialized && !setUp)) + const disablingCauses = findDisablingCauses(testClass, test); + + if ((initialized && !setUp) || !disablingCauses.empty) { - string reason = testClass.disabled.get(test, Disabled.init).reason; + const reason = (initialized && !setUp) + ? "running @BeforeAll failed for preceding test case" + : disablingCauses.front.reason; foreach (testListener; testListeners) testListener.skip(reason); @@ -344,6 +354,75 @@ body testListener.exit(); } +private Disabled[] findDisablingCauses(TestClass testClass, string test) +{ + import std.process : environment; + import std.regex : regex, matchFirst; + import std.system : os, OS; + + Disabled[] disablingCauses; + + if (test in testClass.disabled) + { + disablingCauses ~= testClass.disabled[test]; + } + if (test in testClass.disabledOnOs) with (testClass.disabledOnOs[test]) + { + if (value.canFind(os)) + { + const reason = format("operating system %s included in %s", os, value); + + disablingCauses ~= Disabled(reason); + } + } + if (test in testClass.enabledOnOs) with (testClass.enabledOnOs[test]) + { + if (!value.canFind(os)) + { + const reason = format("operating system %s not included in %s", os, value); + + disablingCauses ~= Disabled(reason); + } + } + if (test in testClass.disabledIfEnvVar) with (testClass.disabledIfEnvVar[test]) + { + if (environment.get(named) !is null && matchFirst(environment.get(named), regex(matches))) + { + const reason = format(`value "%s" of environment variable %s matches %s`, + environment.get(named), named, matches); + + disablingCauses ~= Disabled(reason); + } + } + if (test in testClass.enabledIfEnvVar) with (testClass.enabledIfEnvVar[test]) + { + if (environment.get(named) is null) + { + const reason = format("environment variable %s not set", named); + + disablingCauses ~= Disabled(reason); + } + else if (!matchFirst(environment.get(named), regex(matches))) + { + const reason = format(`value "%s" of environment variable %s does not match %s`, + environment.get(named), named, matches); + + disablingCauses ~= Disabled(reason); + } + } + if (test in testClass.disabledIf) with (testClass.disabledIf[test]) + { + if (condition()) + disablingCauses ~= Disabled(reason); + } + if (test in testClass.enabledIf) with (testClass.enabledIf[test]) + { + if (!condition()) + disablingCauses ~= Disabled(reason); + } + return disablingCauses; +} + private __gshared TestListener[] testListeners = null; /** @@ -813,6 +892,12 @@ mixin template UnitTest() testClass.name = this.classinfo.name; testClass.tests = _members!(typeof(this), Test); testClass.disabled = _attributeByMember!(typeof(this), Disabled); + testClass.enabledIf = _attributeByMember!(typeof(this), EnabledIf); + testClass.disabledIf = _attributeByMember!(typeof(this), DisabledIf); + testClass.enabledIfEnvVar = _attributeByMember!(typeof(this), EnabledIfEnvironmentVariable); + testClass.disabledIfEnvVar = _attributeByMember!(typeof(this), DisabledIfEnvironmentVariable); + testClass.enabledOnOs = _attributeByMember!(typeof(this), EnabledOnOs); + testClass.disabledOnOs = _attributeByMember!(typeof(this), DisabledOnOs); testClass.tags = _attributesByMember!(typeof(this), Tag); testClass.create = () From 2d1a467073dc150122df58a2f8fc52261962c6e2 Mon Sep 17 00:00:00 2001 From: linkrope Date: Wed, 19 May 2021 22:35:23 +0200 Subject: [PATCH 72/73] Replace deprecated approxEqual with isClose --- .gitignore | 5 +++-- dub.json | 12 +++++++++++- src/dunit/assertion.d | 5 +++-- test/main.d | 3 +++ 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 test/main.d diff --git a/.gitignore b/.gitignore index af00a57..e0dc92d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -main -exampleTests +*.a *.o *.swp *.exe .dub +dub.selections.json +d-unit diff --git a/dub.json b/dub.json index b5275f7..c5a4ac2 100644 --- a/dub.json +++ b/dub.json @@ -11,5 +11,15 @@ }, "undead": ">=1.1.1" }, - "targetType": "library" + "configurations": [ + { + "name": "library", + "targetType": "library" + }, + { + "name": "unittest", + "targetType": "executable", + "sourceFiles": ["test/main.d"] + } + ] } diff --git a/src/dunit/assertion.d b/src/dunit/assertion.d index c06a3a8..8bb60e1 100644 --- a/src/dunit/assertion.d +++ b/src/dunit/assertion.d @@ -181,9 +181,10 @@ void assertEquals(T, U)(T expected, U actual, lazy string msg = null, size_t line = __LINE__) if (isFloatingPoint!T || isFloatingPoint!U) { - import std.math : approxEqual; + import std.math : isClose; - if (approxEqual(expected, actual)) + // keep defaults of deprecated approxEqual + if (isClose(expected, actual, 1e-2, 1e-5)) return; string header = (msg.empty) ? null : msg ~ "; "; diff --git a/test/main.d b/test/main.d new file mode 100644 index 0000000..66337c2 --- /dev/null +++ b/test/main.d @@ -0,0 +1,3 @@ +import dunit; + +mixin Main; From 89c11ce252ec6699f136cdcf32621d7f1fe50191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Kr=C3=B6plin?= Date: Thu, 23 Dec 2021 11:30:46 +0100 Subject: [PATCH 73/73] Avoid deprecation warning --- src/dunit/framework.d | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dunit/framework.d b/src/dunit/framework.d index 46bf0d0..d75594c 100644 --- a/src/dunit/framework.d +++ b/src/dunit/framework.d @@ -235,11 +235,7 @@ public bool matches(Tag[] tags, string[] choices) } public void runTests(TestSelection[] testSelections, TestListener[] testListeners) -in -{ - assert(all!"a !is null"(testListeners)); -} -body +in (testListeners.all!"a !is null") { bool tryRun(string phase, void delegate() action) {