1 module unit_threaded.factory;
2 
3 import unit_threaded.testcase: TestCase, CompositeTestCase;
4 import unit_threaded.reflection: TestData;
5 
6 
7 private CompositeTestCase[string] serialComposites;
8 
9 /**
10  * Creates tests cases from the given modules.
11  * If testsToRun is empty, it means run all tests.
12  */
13 TestCase[] createTestCases(in TestData[] testData, in string[] testsToRun = []) {
14     import std.algorithm: sort;
15     import std.array: array;
16 
17     serialComposites = null;
18     bool[TestCase] tests;
19     foreach(const data; testData) {
20         if(!isWantedTest(data, testsToRun)) continue;
21         auto test = createTestCase(data);
22          if(test !is null) tests[test] = true; //can be null if abtract base class
23     }
24 
25     return tests.keys.sort!((a, b) => a.getPath < b.getPath).array;
26 }
27 
28 
29 package TestCase createTestCase(in TestData testData) {
30 
31     import std.algorithm: splitter, reduce;
32     import std.array: array;
33 
34     TestCase createImpl() {
35         import unit_threaded.testcase: BuiltinTestCase, FunctionTestCase, ShouldFailTestCase;
36         import std.conv: text;
37 
38         TestCase testCase;
39         if(testData.isTestClass)
40             testCase = cast(TestCase) Object.factory(testData.name);
41          else
42             testCase = testData.builtin ? new BuiltinTestCase(testData) : new FunctionTestCase(testData);
43 
44         version(unitThreadedLight) {}
45         else
46             assert(testCase !is null,
47                    text("Error creating test case with ",
48                         testData.isTestClass ? "test class data: " : "data: ",
49                         testData));
50 
51         if(testData.shouldFail) {
52             testCase = new ShouldFailTestCase(testCase, testData.exceptionTypeInfo);
53         }
54 
55         return testCase;
56     }
57 
58     auto testCase = createImpl();
59 
60     if(testData.singleThreaded) {
61         // @Serial tests in the same module run sequentially.
62         // A CompositeTestCase is created for each module with at least
63         // one @Serial test and subsequent @Serial tests
64         // appended to it
65         //const moduleName = testData.name.dup.splitter(".")
66         const moduleName = testData.name.splitter(".")
67             .array[0 .. $ - 1].
68             reduce!((a, b) => a ~ "." ~ b);
69 
70         // create one if not already there
71         if(moduleName !in serialComposites) {
72             serialComposites[moduleName] = new CompositeTestCase;
73         }
74 
75         // add the current test to the composite
76         serialComposites[moduleName] ~= testCase;
77         return serialComposites[moduleName];
78     }
79 
80     assert(testCase !is null || testData.testFunction is null,
81            "Could not create TestCase object for test " ~ testData.name);
82 
83     return testCase;
84 }
85 
86 
87 
88 private bool isWantedTest(in TestData testData, in string[] testsToRun) {
89 
90     import std.algorithm: filter, all, startsWith, canFind;
91     import std.array: array;
92 
93     bool isTag(in string t) { return t.startsWith("@") || t.startsWith("~@"); }
94 
95     auto normalToRun = testsToRun.filter!(a => !isTag(a)).array;
96     auto tagsToRun = testsToRun.filter!isTag;
97 
98     bool matchesTags(in string tag) { //runs all tests with the specified tags
99         assert(isTag(tag));
100         return tag[0] == '@' && testData.tags.canFind(tag[1..$]) ||
101             (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2..$]));
102     }
103 
104     return isWantedNonTagTest(testData, normalToRun) &&
105         (tagsToRun.empty || tagsToRun.all!(t => matchesTags(t)));
106 }
107 
108 private bool isWantedNonTagTest(in TestData testData, in string[] testsToRun) {
109 
110     import std.algorithm: any, startsWith, canFind;
111 
112     if(!testsToRun.length) return !testData.hidden; //all tests except the hidden ones
113 
114     bool matchesExactly(in string t) {
115         return t == testData.name;
116     }
117 
118     bool matchesPackage(in string t) { //runs all tests in package if it matches
119         with(testData) return !hidden && name.length > t.length &&
120                            name.startsWith(t) && name[t.length .. $].canFind(".");
121     }
122 
123     return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a));
124 }
125 
126 
127 unittest {
128     //existing, wanted
129     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests"]));
130     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests."]));
131     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server.testSubscribe"]));
132     assert(!isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server.testSubscribeWithMessage"]));
133     assert(!isWantedTest(TestData("tests.stream.testMqttInTwoPackets"), ["tests.server"]));
134     assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server"]));
135     assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests"]));
136     assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.testEqual"]));
137     assert(isWantedTest(TestData("pass_tests.testEqual"), []));
138     assert(!isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.foo"]));
139     assert(!isWantedTest(TestData("example.tests.pass.normal.unittest"),
140                          ["example.tests.pass.io.TestFoo"]));
141     assert(isWantedTest(TestData("example.tests.pass.normal.unittest"), []));
142     assert(!isWantedTest(TestData("tests.pass.attributes.testHidden", null, true /*hidden*/), ["tests.pass"]));
143     assert(!isWantedTest(TestData("", null, false /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
144                                   false /*builtin*/, "" /*suffix*/),
145                          ["@foo"]));
146     assert(isWantedTest(TestData("", null, false /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
147                                  false /*builtin*/, "" /*suffix*/, ["foo"]),
148                         ["@foo"]));
149 
150     assert(!isWantedTest(TestData("", null, false /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
151                                  false /*builtin*/, "" /*suffix*/, ["foo"]),
152                         ["~@foo"]));
153 
154     assert(isWantedTest(TestData("", null, false /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
155                                   false /*builtin*/, "" /*suffix*/),
156                          ["~@foo"]));
157 
158     assert(isWantedTest(TestData("", null, false /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
159                                  false /*builtin*/, "" /*suffix*/, ["bar"]),
160                          ["~@foo"]));
161 
162     // if hidden, don't run by default
163     assert(!isWantedTest(TestData("", null, true /*hidden*/, false /*shouldFail*/, false /*singleThreaded*/,
164                                   false /*builtin*/, "" /*suffix*/, ["bar"]),
165                         ["~@foo"]));
166 
167 
168 }