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