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.splitter(".")
80             .array[0 .. $ - 1].
81             reduce!((a, b) => a ~ "." ~ b);
82 
83         // create one if not already there
84         if(moduleName !in serialComposites) {
85             serialComposites[moduleName] = new CompositeTestCase;
86         }
87 
88         // add the current test to the composite
89         serialComposites[moduleName] ~= testCase;
90         return serialComposites[moduleName];
91     }
92 
93     assert(testCase !is null || testData.testFunction is null,
94            "Could not create TestCase object for test " ~ testData.name);
95 
96     return testCase;
97 }
98 
99 
100 
101 bool isWantedTest(in from!"unit_threaded.runner.reflection".TestData testData,
102                   in string[] testsToRun)
103 {
104 
105     import std.algorithm: filter, all, startsWith, canFind;
106     import std.array: array;
107 
108     bool isTag(in string t) { return t.startsWith("@") || t.startsWith("~@"); }
109 
110     auto normalToRun = testsToRun.filter!(a => !isTag(a)).array;
111     auto tagsToRun = testsToRun.filter!isTag;
112 
113     bool matchesTags(in string tag) { //runs all tests with the specified tags
114         assert(isTag(tag));
115         return tag[0] == '@' && testData.tags.canFind(tag[1..$]) ||
116             (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2..$]));
117     }
118 
119     return isWantedNonTagTest(testData, normalToRun) &&
120         (tagsToRun.empty || tagsToRun.all!(t => matchesTags(t)));
121 }
122 
123 private bool isWantedNonTagTest(in from!"unit_threaded.runner.reflection".TestData testData,
124                                 in string[] testsToRun)
125 {
126 
127     import std.algorithm: any, startsWith, canFind;
128 
129     if(!testsToRun.length) return !testData.hidden; // all tests except the hidden ones
130 
131     bool matchesExactly(in string t) {
132         return t == testData.getPath;
133     }
134 
135     bool matchesPackage(in string t) { //runs all tests in package if it matches
136         with(testData)
137             return !hidden && getPath.length > t.length &&
138                            getPath.startsWith(t) && getPath[t.length .. $].canFind(".");
139     }
140 
141     return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a));
142 }
143 
144 
145 unittest {
146     assert(false);
147 }