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 = testData.builtin
49             ? new BuiltinTestCase(testData)
50             : new FunctionTestCase(testData);
51 
52         version(unitThreadedLight) {}
53         else
54             assert(testCase !is null,
55                    text("Error creating test case with data: ", testData));
56 
57         if(testData.shouldFail) {
58             testCase = new ShouldFailTestCase(testCase, testData.exceptionTypeInfo);
59         } else if(testData.flakyRetries > 0)
60             testCase = new FlakyTestCase(testCase, testData.flakyRetries);
61 
62         return testCase;
63     }
64 
65     auto testCase = createImpl();
66 
67     if(testData.singleThreaded) {
68         // @Serial tests in the same module run sequentially.
69         // A CompositeTestCase is created for each module with at least
70         // one @Serial test and subsequent @Serial tests
71         // appended to it
72         const moduleName = testData.name.splitter(".")
73             .array[0 .. $ - 1].
74             reduce!((a, b) => a ~ "." ~ b);
75 
76         // create one if not already there
77         if(moduleName !in serialComposites) {
78             serialComposites[moduleName] = new CompositeTestCase;
79         }
80 
81         // add the current test to the composite
82         serialComposites[moduleName] ~= testCase;
83         return serialComposites[moduleName];
84     }
85 
86     assert(testCase !is null || testData.testFunction is null,
87            "Could not create TestCase object for test " ~ testData.name);
88 
89     return testCase;
90 }
91 
92 
93 
94 bool isWantedTest(in from!"unit_threaded.runner.reflection".TestData testData,
95                   in string[] testsToRun)
96 {
97 
98     import std.algorithm: filter, all, startsWith, canFind;
99     import std.array: array;
100 
101     bool isTag(in string t) { return t.startsWith("@") || t.startsWith("~@"); }
102 
103     auto normalToRun = testsToRun.filter!(a => !isTag(a)).array;
104     auto tagsToRun = testsToRun.filter!isTag;
105 
106     bool matchesTags(in string tag) { //runs all tests with the specified tags
107         assert(isTag(tag));
108         return tag[0] == '@' && testData.tags.canFind(tag[1..$]) ||
109             (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2..$]));
110     }
111 
112     return isWantedNonTagTest(testData, normalToRun) &&
113         (tagsToRun.empty || tagsToRun.all!(t => matchesTags(t)));
114 }
115 
116 private bool isWantedNonTagTest(in from!"unit_threaded.runner.reflection".TestData testData,
117                                 in string[] testsToRun)
118 {
119 
120     import std.algorithm: any, startsWith, canFind;
121 
122     if(!testsToRun.length) return !testData.hidden; // all tests except the hidden ones
123 
124     bool matchesExactly(in string t) {
125         return t == testData.getPath;
126     }
127 
128     bool matchesPackage(in string t) { //runs all tests in package if it matches
129         with(testData)
130             return !hidden && getPath.length > t.length &&
131                            getPath.startsWith(t) && getPath[t.length .. $].canFind(".");
132     }
133 
134     return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a));
135 }