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 }