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     @safe
20 {
21     import unit_threaded.runner.testcase: TestCase;
22     import std.algorithm: sort;
23     import std.array: array;
24 
25     serialComposites = null;
26     bool[TestCase] tests;
27     foreach(const data; testData) {
28         if(!isWantedTest(data, testsToRun)) continue;
29         auto test = createTestCase(data);
30         if(test !is null) tests[test] = true; //can be null if abtract base class
31     }
32 
33     return () @trusted { return tests.keys.sort!((a, b) => a.getPath < b.getPath).array; }();
34 }
35 
36 
37 from!"unit_threaded.runner.testcase".TestCase createTestCase(
38     in from!"unit_threaded.runner.reflection".TestData testData)
39     @safe
40 {
41     import unit_threaded.runner.testcase: TestCase;
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.moduleName;
73 
74         // create one if not already there
75         if(moduleName !in serialComposites) {
76             serialComposites[moduleName] = new CompositeTestCase;
77         }
78 
79         // add the current test to the composite
80         serialComposites[moduleName] ~= testCase;
81         return serialComposites[moduleName];
82     }
83 
84     assert(testCase !is null || testData.testFunction is null,
85            "Could not create TestCase object for test " ~ testData.name);
86 
87     return testCase;
88 }
89 
90 
91 
92 bool isWantedTest(in from!"unit_threaded.runner.reflection".TestData testData,
93                   in string[] testsToRun)
94     @safe pure
95 {
96 
97     import std.algorithm: filter, all, startsWith, canFind;
98     import std.array: array;
99 
100     bool isTag(in string t) { return t.startsWith("@") || t.startsWith("~@"); }
101 
102     auto normalToRun = testsToRun.filter!(a => !isTag(a)).array;
103     auto tagsToRun = testsToRun.filter!isTag;
104 
105     bool matchesTags(in string tag) { //runs all tests with the specified tags
106         assert(isTag(tag));
107         return tag[0] == '@' && testData.tags.canFind(tag[1..$]) ||
108             (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2..$]));
109     }
110 
111     return isWantedNonTagTest(testData, normalToRun) &&
112         (tagsToRun.empty || tagsToRun.all!(t => matchesTags(t)));
113 }
114 
115 private bool isWantedNonTagTest(in from!"unit_threaded.runner.reflection".TestData testData,
116                                 in string[] testsToRun)
117     @safe pure
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 () @trusted { return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a)); }();
135 }