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 }