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