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 }