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