1 #!/usr/bin/env rdmd 2 module unit_test_runner; 3 4 import std.algorithm : filter, map, joiner, substitute; 5 import std.array : array, join; 6 import std.conv : to; 7 import std.exception : enforce; 8 import std.file : dirEntries, exists, SpanMode, mkdirRecurse, write; 9 import std.format : format; 10 import std.getopt : getopt; 11 import std.path : absolutePath, buildPath, dirSeparator, stripExtension, 12 setExtension; 13 import std.process : environment, spawnProcess, spawnShell, wait; 14 import std.range : empty; 15 import std.stdio; 16 import std..string : join, outdent; 17 18 import tools.paths; 19 20 enum unitTestDir = testPath("unit"); 21 22 string[] testFiles(Range)(Range givenFiles) 23 { 24 if (!givenFiles.empty) 25 return givenFiles.map!(testPath).array; 26 27 return unitTestDir 28 .dirEntries("*.d", SpanMode.depth) 29 .map!(e => e.name) 30 .array; 31 } 32 33 auto moduleNames(const string[] testFiles) 34 { 35 return testFiles 36 .map!(e => e[unitTestDir.length + 1 .. $]) 37 .map!stripExtension 38 .array 39 .map!(e => e.substitute(dirSeparator, ".")); 40 } 41 42 void writeRunnerFile(Range)(Range moduleNames, string path, string filter) 43 { 44 enum codeTemplate = q{ 45 import core.runtime : Runtime, UnitTestResult; 46 import std.meta : AliasSeq; 47 48 // modules to unit test starts here: 49 %s 50 51 alias modules = AliasSeq!( 52 %s 53 ); 54 55 enum filter = %s; 56 57 version(unittest) shared static this() 58 { 59 Runtime.extendedModuleUnitTester = &unitTestRunner; 60 } 61 62 UnitTestResult unitTestRunner() 63 { 64 import std.algorithm : canFind, each, map; 65 import std.conv : text; 66 import std.format : format; 67 import std.meta : Alias; 68 import std.range : empty, front, enumerate; 69 import std.stdio : writeln, writefln, stderr, stdout; 70 import std..string : join; 71 import std.traits : hasUDA, isCallable; 72 73 static import support; 74 75 alias TestCallback = void function(); 76 77 struct Test 78 { 79 Throwable throwable; 80 string name; 81 82 string toString() 83 { 84 return format!"%%s\n%%s"(name, throwable); 85 } 86 87 string fileInfo() 88 { 89 with (throwable) 90 return format!"%%s:%%s"(file, line); 91 } 92 } 93 94 Test[] failedTests; 95 size_t testCount; 96 97 void printReport() 98 { 99 if (!failedTests.empty) 100 { 101 alias formatTest = t => 102 format!"%%s) %%s"(t.index + 1, t.value.toString); 103 104 const failedTestsMessage = failedTests 105 .enumerate 106 .map!(formatTest) 107 .join("\n\n"); 108 109 stderr.writefln!"Failures:\n\n%%s\n"(failedTestsMessage); 110 } 111 112 auto output = failedTests.empty ? stdout : stderr; 113 output.writefln!"%%s tests, %%s failures"(testCount, failedTests.length); 114 115 if (failedTests.empty) 116 return; 117 118 stderr.writefln!"\nFailed tests:\n%%s"( 119 failedTests.map!(t => t.fileInfo).join("\n")); 120 } 121 122 TestCallback[] getTestCallbacks(alias module_, alias uda)() 123 { 124 enum isMemberAccessible(string memberName) = 125 is(typeof(__traits(getMember, module_, memberName))); 126 127 TestCallback[] callbacks; 128 129 static foreach(mem ; __traits(allMembers, module_)) 130 { 131 static if (isMemberAccessible!(mem)) 132 {{ 133 alias member = __traits(getMember, module_, mem); 134 135 static if (isCallable!member && hasUDA!(member, uda)) 136 callbacks ~= &member; 137 }} 138 } 139 140 return callbacks; 141 } 142 143 void executeCallbacks(const TestCallback[] callbacks) 144 { 145 callbacks.each!(c => c()); 146 } 147 148 static foreach (module_ ; modules) 149 { 150 foreach (unitTest ; __traits(getUnitTests, module_)) 151 { 152 enum attributes = [__traits(getAttributes, unitTest)]; 153 154 const beforeEachCallbacks = getTestCallbacks!(module_, support.beforeEach); 155 const afterEachCallbacks = getTestCallbacks!(module_, support.afterEach); 156 157 Test test; 158 159 try 160 { 161 static if (!attributes.empty) 162 { 163 test.name = attributes.front; 164 165 if (attributes.front.canFind(filter)) 166 { 167 testCount++; 168 executeCallbacks(beforeEachCallbacks); 169 unitTest(); 170 } 171 } 172 173 else static if (filter.length == 0) 174 { 175 testCount++; 176 executeCallbacks(beforeEachCallbacks); 177 unitTest(); 178 } 179 } 180 181 catch (Throwable t) 182 { 183 test.throwable = t; 184 failedTests ~= test; 185 } 186 187 finally 188 executeCallbacks(afterEachCallbacks); 189 } 190 } 191 192 printReport(); 193 194 UnitTestResult result = { 195 runMain: false, 196 executed: testCount, 197 passed: testCount - failedTests.length 198 }; 199 200 return result; 201 } 202 }.outdent; 203 204 const imports = moduleNames 205 .map!(e => format!"static import %s;"(e)) 206 .joiner("\n") 207 .to!string; 208 209 const modules = moduleNames 210 .map!(e => format!"%s"(e)) 211 .joiner(",\n") 212 .to!string; 213 214 const content = format!codeTemplate(imports, modules, format!`"%s"`(filter)); 215 write(path, content); 216 } 217 218 /** 219 Writes a cmdfile with all the compiler flags to the given `path`. 220 221 Params: 222 path = the path where to write the cmdfile file 223 runnerPath = the path of the unit test runner file outputted by `writeRunnerFile` 224 outputPath = the path where to place the compiled binary 225 testFiles = the test files to compile 226 */ 227 void writeCmdfile(string path, string runnerPath, string outputPath, 228 const string[] testFiles) 229 { 230 auto flags = [ 231 "-version=NoBackend", 232 "-version=GC", 233 "-version=NoMain", 234 "-version=MARS", 235 "-unittest", 236 "-J" ~ buildOutputPath, 237 "-J" ~ projectRootDir.buildPath("res"), 238 "-I" ~ projectRootDir.buildPath("src"), 239 "-I" ~ unitTestDir, 240 "-i", 241 "-main", 242 "-of" ~ outputPath, 243 "-m" ~ model 244 ] ~ testFiles ~ runnerPath; 245 246 // older versions of Optlink causes: "Error 45: Too Much DEBUG Data for Old CodeView format" 247 if (!usesOptlink) 248 flags ~= "-g"; 249 250 write(path, flags.join("\n")); 251 } 252 253 /** 254 Returns `true` if any of the given files don't exist. 255 256 Also prints an error message. 257 */ 258 bool missingTestFiles(Range)(Range givenFiles) 259 { 260 const nonExistingTestFiles = givenFiles 261 .filter!(file => !file.exists) 262 .join("\n"); 263 264 if (!nonExistingTestFiles.empty) 265 { 266 stderr.writefln("The following test files don't exist:\n\n%s", 267 nonExistingTestFiles); 268 269 return true; 270 } 271 272 return false; 273 } 274 275 void execute(const string[] args ...) 276 { 277 try 278 { 279 enforce(spawnProcess(args).wait() == 0); 280 } 281 catch(Exception e) 282 { 283 throw new Exception("Failed to execute command: " ~ args.join(" "), e); 284 } 285 } 286 287 bool usesOptlink() 288 { 289 version (DigitalMars) 290 return os == "windows" && model == "32"; 291 292 else 293 return false; 294 } 295 296 int main(string[] args) 297 { 298 string unitTestFilter; 299 getopt(args, "filter|f", &unitTestFilter); 300 301 auto givenFiles = args[1 .. $].map!absolutePath; 302 303 if (missingTestFiles(givenFiles)) 304 return 1; 305 306 const runnerPath = resultsDir.buildPath("runner.d"); 307 const testFiles = givenFiles.testFiles; 308 309 mkdirRecurse(resultsDir); 310 testFiles 311 .moduleNames 312 .writeRunnerFile(runnerPath, unitTestFilter); 313 314 const cmdfilePath = resultsDir.buildPath("cmdfile"); 315 const outputPath = resultsDir.buildPath("runner").setExtension(exeExtension); 316 writeCmdfile(cmdfilePath, runnerPath, outputPath, testFiles); 317 318 execute(dmdPath, "@" ~ cmdfilePath); 319 320 return spawnProcess(outputPath).wait(); 321 }