1 #!/usr/bin/env rdmd
2 module d_do_test;
3 
4 import std.algorithm;
5 import std.array;
6 import std.conv;
7 import std.datetime.stopwatch;
8 import std.datetime.systime;
9 import std.exception;
10 import std.file;
11 import std.format;
12 import std.process;
13 import std.random;
14 import std.range : chain;
15 import std.regex;
16 import std.path;
17 import std.stdio;
18 import std..string;
19 import core.sys.posix.sys.wait;
20 
21 const dmdTestDir = __FILE_FULL_PATH__.dirName.dirName;
22 
23 version(Win32)
24 {
25     extern(C) int putenv(const char*);
26 }
27 
28 void usage()
29 {
30     write("d_do_test <test_file>\n"
31           ~ "\n"
32           ~ "   Note: this program is normally called through the Makefile, it"
33           ~ "         is not meant to be called directly by the user.\n"
34           ~ "\n"
35           ~ "   example: d_do_test runnable/pi.d\n"
36           ~ "\n"
37           ~ "   relevant environment variables:\n"
38           ~ "      ARGS:          set to execute all combinations of\n"
39           ~ "      REQUIRED_ARGS: arguments always passed to the compiler\n"
40           ~ "      DMD:           compiler to use, ex: ../src/dmd (required)\n"
41           ~ "      CC:            C++ compiler to use, ex: dmc, g++\n"
42           ~ "      OS:            windows, linux, freebsd, osx, netbsd, dragonflybsd\n"
43           ~ "      RESULTS_DIR:   base directory for test results\n"
44           ~ "      MODEL:         32 or 64 (required)\n"
45           ~ "      AUTO_UPDATE:   set to 1 to auto-update mismatching test output\n"
46           ~ "      PRINT_RUNTIME: set to 1 to print test runtime\n"
47           ~ "\n"
48           ~ "   windows vs non-windows portability env vars:\n"
49           ~ "      DSEP:          \\\\ or /\n"
50           ~ "      SEP:           \\ or / (required)\n"
51           ~ "      OBJ:          .obj or .o (required)\n"
52           ~ "      EXE:          .exe or <null> (required)\n");
53 }
54 
55 enum TestMode
56 {
57     COMPILE,
58     FAIL_COMPILE,
59     RUN,
60     DSHELL,
61 }
62 
63 struct TestArgs
64 {
65     TestMode mode;
66 
67     bool     compileSeparately;
68     bool     link;
69     bool     clearDflags; /// whether DFLAGS should be cleared before invoking dmd
70     string   executeArgs;
71     string   cxxflags;
72     string[] sources;
73     string[] compiledImports;
74     string[] cppSources;
75     string[] objcSources;
76     string   permuteArgs;
77     string[] argSets;
78     string   compileOutput;
79     string   compileOutputFile; /// file containing the expected output
80     string   runOutput; /// Expected output of the compiled executable
81     string   gdbScript;
82     string   gdbMatch;
83     string   postScript;
84     string[] outputFiles; /// generated files appended to the compilation output
85     string   transformOutput; /// Transformations for the compiler output
86     string   requiredArgs;
87     string   requiredArgsForLink;
88     string   disabledReason; // if empty, the test is not disabled
89 
90     bool isDisabled() const { return disabledReason.length != 0; }
91 }
92 
93 struct EnvData
94 {
95     string all_args;
96     string dmd;
97     string results_dir;
98     string sep;
99     string dsep;
100     string obj;
101     string exe;
102     string os;
103     string compiler;
104     string ccompiler;
105     string model;
106     string required_args;
107     string cxxCompatFlags;      /// Additional flags passed to $(compiler) when `EXTRA_CPP_SOURCES` is present
108     string[] picFlag;           /// Compiler flag for PIC (if requested from environment)
109     bool dobjc;
110     bool coverage_build;
111     bool autoUpdate;
112     bool printRuntime;          /// Print time spent on a single test
113     bool usingMicrosoftCompiler;
114     bool tryDisabled;           /// Silently try disabled tests (ignore failure but report success)
115 }
116 
117 /++
118 Creates a new EnvData instance based on the current environment.
119 Other code should not read from the environment.
120 
121 Returns: an initialized EnvData instance
122 ++/
123 immutable(EnvData) processEnvironment()
124 {
125     static string envGetRequired(in char[] name)
126     {
127         if (auto value = environment.get(name))
128             return value;
129 
130         writefln("Error: Missing environment variable '%s', was this called through the Makefile?",
131             name);
132         throw new SilentQuit();
133     }
134 
135     EnvData envData;
136     envData.all_args       = environment.get("ARGS");
137     envData.results_dir    = envGetRequired("RESULTS_DIR");
138     envData.sep            = envGetRequired ("SEP");
139     envData.dsep           = environment.get("DSEP");
140     envData.obj            = envGetRequired ("OBJ");
141     envData.exe            = envGetRequired ("EXE");
142     envData.os             = environment.get("OS");
143     envData.dmd            = replace(envGetRequired("DMD"), "/", envData.sep);
144     envData.compiler       = "dmd"; //should be replaced for other compilers
145     envData.ccompiler      = environment.get("CC");
146     envData.model          = envGetRequired("MODEL");
147     envData.required_args  = environment.get("REQUIRED_ARGS");
148     envData.dobjc          = environment.get("D_OBJC") == "1";
149     envData.coverage_build = environment.get("DMD_TEST_COVERAGE") == "1";
150     envData.autoUpdate     = environment.get("AUTO_UPDATE", "") == "1";
151     envData.printRuntime   = environment.get("PRINT_RUNTIME", "") == "1";
152     envData.tryDisabled    = environment.get("TRY_DISABLED") == "1";
153 
154     if (envData.ccompiler.empty)
155     {
156         if (envData.os != "windows")
157             envData.ccompiler = "c++";
158         else if (envData.model == "32")
159             envData.ccompiler = "dmc";
160         else if (envData.model == "64")
161             envData.ccompiler = `C:\"Program Files (x86)"\"Microsoft Visual Studio 10.0"\VC\bin\amd64\cl.exe`;
162         else
163         {
164             writeln("Unknown $OS$MODEL combination: ", envData.os, envData.model);
165             throw new SilentQuit();
166         }
167     }
168 
169     envData.usingMicrosoftCompiler = envData.ccompiler.toLower.endsWith("cl.exe");
170 
171     version (Windows) {} else
172     {
173         version(X86_64)
174             envData.picFlag = ["-fPIC"];
175         if (environment.get("PIC", null) == "1")
176             envData.picFlag = ["-fPIC"];
177     }
178 
179     switch (envData.compiler)
180     {
181         case "dmd":
182         case "ldc":
183             if(envData.os != "windows")
184                 envData.cxxCompatFlags = " -L-lstdc++ -L--no-demangle";
185             break;
186 
187         case "gdc":
188             envData.cxxCompatFlags = "-Xlinker -lstdc++ -Xlinker --no-demangle";
189             break;
190 
191         default:
192             writeln("Unknown compiler: ", envData.compiler);
193             throw new SilentQuit();
194     }
195 
196     return cast(immutable) envData;
197 }
198 
199 bool findTestParameter(const ref EnvData envData, string file, string token, ref string result, string multiLineDelimiter = " ")
200 {
201     auto tokenStart = std..string.indexOf(file, token);
202     if (tokenStart == -1) return false;
203 
204     file = file[tokenStart + token.length .. $];
205 
206     auto lineEndR = std..string.indexOf(file, "\r");
207     auto lineEndN = std..string.indexOf(file, "\n");
208     auto lineEnd  = lineEndR == -1 ?
209         (lineEndN == -1 ? file.length : lineEndN) :
210         (lineEndN == -1 ? lineEndR    : min(lineEndR, lineEndN));
211 
212     //writeln("found ", token, " in line: ", file.length, ", ", tokenStart, ", ", tokenStart+lineEnd);
213     //writeln("found ", token, " in line: '", file[tokenStart .. tokenStart+lineEnd], "'");
214 
215     result = file[0 .. lineEnd];
216     const commentStart = std..string.indexOf(result, "//");
217     if (commentStart != -1)
218         result = result[0 .. commentStart];
219     result = strip(result);
220 
221     // filter by OS specific setting (os1 os2 ...)
222     if (result.startsWith("("))
223     {
224         auto close = std..string.indexOf(result, ")");
225         if (close >= 0)
226         {
227             string[] oss = split(result[1 .. close], " ");
228             if (oss.canFind(envData.os))
229                 result = result[close + 1 .. $];
230             else
231                 result = null;
232         }
233     }
234     // skips the :, if present
235     if (result.startsWith(":"))
236         result = strip(result[1 .. $]);
237 
238     //writeln("arg: '", result, "'");
239 
240     string result2;
241     if (findTestParameter(envData, file[lineEnd .. $], token, result2, multiLineDelimiter))
242     {
243         if (result2.length > 0)
244         {
245             if (result.length == 0)
246                 result = result2;
247             else
248                 result ~= multiLineDelimiter ~ result2;
249         }
250     }
251 
252     // fix-up separators
253     result = result.unifyDirSep(envData.sep);
254 
255     return true;
256 }
257 
258 bool findOutputParameter(string file, string token, out string result, string sep)
259 {
260     bool found = false;
261 
262     while (true)
263     {
264         const istart = std..string.indexOf(file, token);
265         if (istart == -1)
266             break;
267         found = true;
268 
269         file = file[istart + token.length .. $];
270 
271         enum embed_sep = "---";
272         auto n = std..string.indexOf(file, embed_sep);
273 
274         enforce(n != -1, "invalid "~token~" format");
275         n += embed_sep.length;
276         while (file[n] == '-') ++n;
277         if (file[n] == '\r') ++n;
278         if (file[n] == '\n') ++n;
279 
280         file = file[n .. $];
281         auto iend = std..string.indexOf(file, embed_sep);
282         enforce(iend != -1, "invalid TEST_OUTPUT format");
283 
284         result ~= file[0 .. iend];
285 
286         while (file[iend] == '-') ++iend;
287         file = file[iend .. $];
288     }
289 
290     if (found)
291     {
292         result = std..string.strip(result);
293         result = result.unifyNewLine().unifyDirSep(sep);
294         result = result ? result : ""; // keep non-null
295     }
296     return found;
297 }
298 
299 void replaceResultsDir(ref string arguments, const ref EnvData envData)
300 {
301     // Bash would expand this automatically on Posix, but we need to manually
302     // perform the replacement for Windows compatibility.
303     arguments = replace(arguments, "${RESULTS_DIR}", envData.results_dir);
304 }
305 
306 string getDisabledReason(string[] disabledPlatforms, const ref EnvData envData)
307 {
308     if (disabledPlatforms.length == 0)
309         return null;
310 
311     const target = ((envData.os == "windows") ? "win" : envData.os) ~ envData.model;
312 
313     // allow partial matching, e.g. `win` to disable both win32 and win64
314     const i = disabledPlatforms.countUntil!(p => target.canFind(p));
315     if (i != -1)
316         return "on " ~ disabledPlatforms[i];
317 
318     return null;
319 }
320 
321 bool gatherTestParameters(ref TestArgs testArgs, string input_dir, string input_file, const ref EnvData envData)
322 {
323     string file = cast(string)std.file.read(input_file);
324 
325     string dflagsStr;
326     testArgs.clearDflags = findTestParameter(envData, file, "DFLAGS", dflagsStr);
327     enforce(dflagsStr.empty, "The DFLAGS test argument must be empty: It is '" ~ dflagsStr ~ "'");
328 
329     findTestParameter(envData, file, "REQUIRED_ARGS", testArgs.requiredArgs);
330     if (envData.required_args.length)
331     {
332         if (testArgs.requiredArgs.length)
333             testArgs.requiredArgs ~= " " ~ envData.required_args;
334         else
335             testArgs.requiredArgs = envData.required_args;
336     }
337     replaceResultsDir(testArgs.requiredArgs, envData);
338 
339     if (! findTestParameter(envData, file, "PERMUTE_ARGS", testArgs.permuteArgs))
340     {
341         if (testArgs.mode == TestMode.RUN)
342             testArgs.permuteArgs = envData.all_args;
343     }
344     replaceResultsDir(testArgs.permuteArgs, envData);
345 
346     // remove permute args enforced as required anyway
347     if (testArgs.requiredArgs.length && testArgs.permuteArgs.length)
348     {
349         const required = split(testArgs.requiredArgs);
350         const newPermuteArgs = split(testArgs.permuteArgs)
351             .filter!(a => !required.canFind(a))
352             .join(" ");
353         testArgs.permuteArgs = newPermuteArgs;
354     }
355 
356     // tests can override -verrors by using REQUIRED_ARGS
357     if (testArgs.mode == TestMode.FAIL_COMPILE)
358         testArgs.requiredArgs = "-verrors=0 " ~ testArgs.requiredArgs;
359 
360     // https://issues.dlang.org/show_bug.cgi?id=10664: exceptions don't work reliably with COMDAT folding
361     // it also slows down some tests drastically, e.g. runnable/test17338.d
362     if (envData.usingMicrosoftCompiler)
363         testArgs.requiredArgs ~= " -L/OPT:NOICF";
364 
365     {
366         string argSetsStr;
367         findTestParameter(envData, file, "ARG_SETS", argSetsStr, ";");
368         foreach(s; split(argSetsStr, ";"))
369         {
370             replaceResultsDir(s, envData);
371             testArgs.argSets ~= s;
372         }
373     }
374 
375     // win(32|64) doesn't support pic
376     if (envData.os == "windows")
377     {
378         auto index = std..string.indexOf(testArgs.permuteArgs, "-fPIC");
379         if (index != -1)
380             testArgs.permuteArgs = testArgs.permuteArgs[0 .. index] ~ testArgs.permuteArgs[index+5 .. $];
381     }
382 
383     // clean up extra spaces
384     testArgs.permuteArgs = strip(replace(testArgs.permuteArgs, "  ", " "));
385 
386     if (findTestParameter(envData, file, "EXECUTE_ARGS", testArgs.executeArgs))
387     {
388         replaceResultsDir(testArgs.executeArgs, envData);
389         // Always run main even if compiled with '-unittest' but let
390         // tests switch to another behaviour if necessary
391         if (!testArgs.executeArgs.canFind("--DRT-testmode"))
392             testArgs.executeArgs ~= " --DRT-testmode=run-main";
393     }
394 
395     string extraSourcesStr;
396     findTestParameter(envData, file, "EXTRA_SOURCES", extraSourcesStr);
397     testArgs.sources = [input_file];
398     // prepend input_dir to each extra source file
399     foreach(s; split(extraSourcesStr))
400         testArgs.sources ~= input_dir ~ "/" ~ s;
401 
402     {
403         string compiledImports;
404         findTestParameter(envData, file, "COMPILED_IMPORTS", compiledImports);
405         foreach(s; split(compiledImports))
406             testArgs.compiledImports ~= input_dir ~ "/" ~ s;
407     }
408 
409     findTestParameter(envData, file, "CXXFLAGS", testArgs.cxxflags);
410     string extraCppSourcesStr;
411     findTestParameter(envData, file, "EXTRA_CPP_SOURCES", extraCppSourcesStr);
412     testArgs.cppSources = split(extraCppSourcesStr);
413 
414     if (testArgs.cppSources.length)
415         testArgs.requiredArgs ~= envData.cxxCompatFlags;
416 
417     string extraObjcSourcesStr;
418     auto objc = findTestParameter(envData, file, "EXTRA_OBJC_SOURCES", extraObjcSourcesStr);
419 
420     if (objc && !envData.dobjc)
421         return false;
422 
423     testArgs.objcSources = split(extraObjcSourcesStr);
424 
425     // swap / with $SEP
426     if (envData.sep && envData.sep != "/")
427         foreach (ref s; testArgs.sources)
428             s = replace(s, "/", to!string(envData.sep));
429     //writeln ("sources: ", testArgs.sources);
430 
431     {
432         string throwAway;
433         testArgs.link = findTestParameter(envData, file, "LINK", throwAway);
434     }
435 
436     // COMPILE_SEPARATELY can take optional compiler switches when link .o files
437     testArgs.compileSeparately = findTestParameter(envData, file, "COMPILE_SEPARATELY", testArgs.requiredArgsForLink);
438 
439     string disabledPlatformsStr;
440     findTestParameter(envData, file, "DISABLED", disabledPlatformsStr);
441 
442     version (DragonFlyBSD)
443     {
444         // DragonFlyBSD is x86_64 only, instead of adding DISABLED to a lot of tests, just exclude them from running
445         if (testArgs.requiredArgs.canFind("-m32"))
446             testArgs.disabledReason = "on DragonFlyBSD (no -m32)";
447     }
448 
449     version (ARM)         enum supportsM64 = false;
450     else version (MIPS32) enum supportsM64 = false;
451     else version (PPC)    enum supportsM64 = false;
452     else                  enum supportsM64 = true;
453 
454     static if (!supportsM64)
455     {
456         if (testArgs.requiredArgs.canFind("-m64"))
457             testArgs.disabledReason = "because target doesn't support -m64";
458     }
459 
460     if (!testArgs.isDisabled)
461         testArgs.disabledReason = getDisabledReason(split(disabledPlatformsStr), envData);
462 
463     findTestParameter(envData, file, "TEST_OUTPUT_FILE", testArgs.compileOutputFile);
464 
465     // Only check for TEST_OUTPUT is no file was given because it would
466     // partially match TEST_OUTPUT_FILE
467     if (testArgs.compileOutputFile)
468     {
469         // Don't require tests to specify the test directory
470         testArgs.compileOutputFile = input_dir.buildPath(testArgs.compileOutputFile);
471         testArgs.compileOutput = readText(testArgs.compileOutputFile)
472                                     .unifyNewLine() // Avoid CRLF issues
473                                     .strip();
474     }
475     else
476         findOutputParameter(file, "TEST_OUTPUT", testArgs.compileOutput, envData.sep);
477 
478     string outFilesStr;
479     findTestParameter(envData, file, "OUTPUT_FILES", outFilesStr);
480     testArgs.outputFiles = outFilesStr.split(';');
481 
482     findTestParameter(envData, file, "TRANSFORM_OUTPUT", testArgs.transformOutput);
483 
484     findOutputParameter(file, "RUN_OUTPUT", testArgs.runOutput, envData.sep);
485 
486     findOutputParameter(file, "GDB_SCRIPT", testArgs.gdbScript, envData.sep);
487     findTestParameter(envData, file, "GDB_MATCH", testArgs.gdbMatch);
488 
489     if (findTestParameter(envData, file, "POST_SCRIPT", testArgs.postScript))
490         testArgs.postScript = replace(testArgs.postScript, "/", to!string(envData.sep));
491 
492     return true;
493 }
494 
495 string[] combinations(string argstr)
496 {
497     string[] results;
498     string[] args = split(argstr);
499     long combinations = 1 << args.length;
500     for (size_t i = 0; i < combinations; i++)
501     {
502         string r;
503         bool printed = false;
504 
505         for (size_t j = 0; j < args.length; j++)
506         {
507             if (i & 1 << j)
508             {
509                 if (printed)
510                     r ~= " ";
511                 r ~= args[j];
512                 printed = true;
513             }
514         }
515 
516         results ~= r;
517     }
518 
519     return results;
520 }
521 
522 string genTempFilename(string result_path)
523 {
524     auto a = appender!string();
525     a.put(result_path);
526     foreach (ref e; 0 .. 8)
527     {
528         formattedWrite(a, "%x", rndGen.front);
529         rndGen.popFront();
530     }
531 
532     return a.data;
533 }
534 
535 int system(string command)
536 {
537     static import core.stdc.stdlib;
538     if (!command) return core.stdc.stdlib.system(null);
539     const commandz = toStringz(command);
540     auto status = core.stdc.stdlib.system(commandz);
541     if (status == -1) return status;
542     version (Windows) status <<= 8;
543     return status;
544 }
545 
546 version(Windows)
547 {
548     extern (D) bool WIFEXITED( int status )    { return ( status & 0x7F ) == 0; }
549     extern (D) int  WEXITSTATUS( int status )  { return ( status & 0xFF00 ) >> 8; }
550     extern (D) int  WTERMSIG( int status )     { return status & 0x7F; }
551     extern (D) bool WIFSIGNALED( int status )
552     {
553         return ( cast(byte) ( ( status & 0x7F ) + 1 ) >> 1 ) > 0;
554     }
555 }
556 
557 void removeIfExists(in char[] filename)
558 {
559     if (std.file.exists(filename))
560         std.file.remove(filename);
561 }
562 
563 string execute(ref File f, string command, bool expectpass, string result_path)
564 {
565     auto filename = genTempFilename(result_path);
566     scope(exit) removeIfExists(filename);
567 
568     auto rc = system(command ~ " > " ~ filename ~ " 2>&1");
569 
570     string output = readText(filename);
571     f.writeln(command);
572     f.write(output);
573 
574     if (WIFSIGNALED(rc))
575     {
576         auto value = WTERMSIG(rc);
577         enforce(0 == value, "caught signal: " ~ to!string(value));
578     }
579     else if (WIFEXITED(rc))
580     {
581         auto value = WEXITSTATUS(rc);
582         if (expectpass)
583             enforce(0 == value, "expected rc == 0, exited with rc == " ~ to!string(value));
584         else
585             enforce(1 == value, "expected rc == 1, but exited with rc == " ~ to!string(value));
586     }
587 
588     return output;
589 }
590 
591 /// add quotes around the whole string if it contains spaces that are not in quotes
592 string quoteSpaces(string str)
593 {
594     if (str.indexOf(' ') < 0)
595         return str;
596     bool inquote = false;
597     foreach(dchar c; str)
598         if (c == '"')
599             inquote = !inquote;
600         else if (c == ' ' && !inquote)
601             return "\"" ~ str ~ "\"";
602     return str;
603 }
604 
605 string unifyNewLine(string str)
606 {
607     // On Windows, Outbuffer.writenl() puts `\r\n` into the buffer,
608     // then fprintf() adds another `\r` when formatting the message.
609     // This is why there's a match for `\r\r\n` in this regex.
610     static re = regex(`\r\r\n|\r\n|\r|\n`, "g");
611     return std.regex.replace(str, re, "\n");
612 }
613 
614 string unifyDirSep(string str, string sep)
615 {
616     static re = regex(`(?<=[-\w{}][-\w{}]*)/(?=[-\w][-\w/]*\.(di?|mixin)\b)`, "g");
617     return std.regex.replace(str, re, sep);
618 }
619 unittest
620 {
621     assert(`fail_compilation/test.d(1) Error: dummy error message for 'test'`.unifyDirSep(`\`)
622         == `fail_compilation\test.d(1) Error: dummy error message for 'test'`);
623     assert(`fail_compilation/test.d(1) Error: at fail_compilation/test.d(2)`.unifyDirSep(`\`)
624         == `fail_compilation\test.d(1) Error: at fail_compilation\test.d(2)`);
625 
626     assert(`fail_compilation/test.d(1) Error: at fail_compilation/imports/test.d(2)`.unifyDirSep(`\`)
627         == `fail_compilation\test.d(1) Error: at fail_compilation\imports\test.d(2)`);
628     assert(`fail_compilation/diag.d(2): Error: fail_compilation/imports/fail.d must be imported`.unifyDirSep(`\`)
629         == `fail_compilation\diag.d(2): Error: fail_compilation\imports\fail.d must be imported`);
630 
631     assert(`{{RESULTS_DIR}}/fail_compilation/mixin_test.mixin(7): Error:`.unifyDirSep(`\`)
632         == `{{RESULTS_DIR}}\fail_compilation\mixin_test.mixin(7): Error:`);
633 }
634 
635 bool collectExtraSources (in string input_dir, in string output_dir, in string[] extraSources,
636                           ref string[] sources, in EnvData envData, in string compiler,
637                           const(char)[] cxxflags, ref File logfile)
638 {
639     foreach (cur; extraSources)
640     {
641         auto curSrc = input_dir ~ envData.sep ~"extra-files" ~ envData.sep ~ cur;
642         auto curObj = output_dir ~ envData.sep ~ cur ~ envData.obj;
643         string command = quoteSpaces(compiler);
644         if (envData.compiler == "dmd")
645         {
646             if (envData.usingMicrosoftCompiler)
647             {
648                 command ~= ` /c /nologo `~curSrc~` /Fo`~curObj;
649             }
650             else if (envData.os == "windows" && envData.model == "32")
651             {
652                 command ~= " -c "~curSrc~" -o"~curObj;
653             }
654             else
655             {
656                 command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj;
657             }
658         }
659         else
660         {
661             command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj;
662         }
663         if (cxxflags)
664             command ~= " " ~ cxxflags;
665 
666         logfile.writeln(command);
667         logfile.flush(); // Avoid reordering due to buffering
668 
669         auto pid = spawnShell(command, stdin, logfile, logfile, null, Config.retainStdout | Config.retainStderr);
670         if(wait(pid))
671         {
672             return false;
673         }
674         sources ~= curObj;
675     }
676 
677     return true;
678 }
679 
680 /++
681 Applies custom transformations defined in transformOutput to testOutput.
682 
683 Currently the following actions are supported:
684  * "sanitize_json"       = replace compiler/plattform specific data from generated JSON
685  * "remove_lines(<re>)" = remove all lines matching a regex <re>
686 
687 Params:
688     testOutput      = the existing output to be modified
689     transformOutput = list of transformation identifiers
690 ++/
691 void applyOutputTransformations(ref string testOutput, string transformOutput)
692 {
693     while (transformOutput.length)
694     {
695         string step, arg;
696 
697         const idx = transformOutput.countUntil(' ', '(');
698         if (idx == -1)
699         {
700             step = transformOutput;
701             transformOutput = null;
702         }
703         else
704         {
705             step = transformOutput[0 .. idx];
706             const hasArgs = transformOutput[idx] == '(';
707             transformOutput = transformOutput[idx + 1 .. $];
708             if (hasArgs)
709             {
710                 // "..." quotes are optional but necessary if the arg contains ')'
711                 const isQuoted = transformOutput[0] == '"';
712                 const end = isQuoted ? `"` : `)`;
713                 auto parts = transformOutput[isQuoted .. $].findSplit(end);
714                 enforce(parts, "Missing closing `" ~ end ~ "`!");
715                 arg = parts[0];
716                 transformOutput = parts[2][isQuoted .. $];
717             }
718 
719             // Skip space between steps
720             import std.ascii : isWhite;
721             transformOutput.skipOver!isWhite();
722         }
723 
724         switch (step)
725         {
726             case "sanitize_json":
727             {
728                 import sanitize_json : sanitize;
729                 sanitize(testOutput);
730                 break;
731             }
732 
733             case "remove_lines":
734             {
735                 auto re = regex(arg);
736                 testOutput = testOutput
737                     .splitter('\n')
738                     .filter!(line => !line.matchFirst(re))
739                     .join('\n');
740                 break;
741             }
742 
743             default:
744                 throw new Exception(format(`Unknown transformation: "%s"!`, step));
745         }
746     }
747 }
748 
749 unittest
750 {
751     static void test(string input, const string transformations, const string expected)
752     {
753         applyOutputTransformations(input, transformations);
754         assert(input == expected);
755     }
756 
757     static void testJson(const string transformations, const string expectedJson)
758     {
759         test(`{
760     "modules": [
761         {
762             "file": "/path/to/the/file",
763             "kind": "module",
764             "members": []
765         }
766     ]
767 }`, transformations, expectedJson);
768     }
769 
770 
771     testJson("sanitize_json", `{
772     "modules": [
773         {
774             "file": "VALUE_REMOVED_FOR_TEST",
775             "kind": "module",
776             "members": []
777         }
778     ]
779 }`);
780 
781     testJson(`sanitize_json  remove_lines("kind")`, `{
782     "modules": [
783         {
784             "file": "VALUE_REMOVED_FOR_TEST",
785             "members": []
786         }
787     ]
788 }`);
789 
790     testJson(`sanitize_json remove_lines("kind") remove_lines("file")`, `{
791     "modules": [
792         {
793             "members": []
794         }
795     ]
796 }`);
797 
798     test(`This is a text containing
799         some words which is a text sample
800         nevertheless`,
801         `remove_lines(text sample)`,
802         `This is a text containing
803         nevertheless`);
804 
805     test(`This is a text with
806         a random ) which should
807         still work`,
808         `remove_lines("random \)")`,
809         `This is a text with
810         still work`);
811 
812     test(`Tom bought
813         12 apples
814         and 6 berries
815         from the store`,
816         `remove_lines("(\d+)")`,
817         `Tom bought
818         from the store`);
819 
820     assertThrown(test("", "unknown", ""));
821 }
822 
823 /++
824 Compares the output string to the reference string by character
825 except parts marked with one of the following special sequences:
826 
827 $n$ = numbers (e.g. compiler generated unique identifiers)
828 $p:<path>$ = real paths ending with <path>
829 $?:<choices>$ = environment dependent content supplied as a list
830                 choices (either <condition>=<content> or <default>),
831                 separated by a '|'. Currently supported conditions are
832                 OS and model as supplied from the environment
833 
834 Params:
835     output    = the real output
836     refoutput = the expected output
837     envData   = test environment
838 
839 Returns: whether output matches the expected refoutput
840 ++/
841 bool compareOutput(string output, string refoutput, const ref EnvData envData)
842 {
843     // If no output is expected, only check that nothing was captured.
844     if (refoutput.length == 0)
845         return (output.length == 0) ? true : false;
846 
847     for ( ; ; )
848     {
849         auto special = refoutput.find("$n$", "$p:", "$?:").rename!("remainder", "id");
850 
851         // Simple equality check if no special tokens remain
852         if (special.id == 0)
853             return refoutput == output;
854 
855         const expected = refoutput[0 .. $ - special.remainder.length];
856 
857         // Check until the special token
858         if (!output.skipOver(expected))
859             return false;
860 
861         // Discard the special token and progress output appropriately
862         refoutput = special.remainder[3 .. $];
863 
864         if (special.id == 1) // $n$
865         {
866             import std.ascii : isDigit;
867             output.skipOver!isDigit();
868             continue;
869         }
870 
871         // $<identifier>:<special content>$
872         /// ( special content, "$", remaining expected output )
873         auto refparts = refoutput.findSplit("$");
874         enforce(refparts, "Malformed special sequence!");
875         refoutput = refparts[2];
876 
877         if (special.id == 2) // $p:<some path>$
878         {
879             // special content is the expected path tail
880             // Substitute / with the appropriate directory separator
881             auto pathEnd = refparts[0].replace("/", envData.sep);
882 
883             /// ( whole path, remaining output )
884             auto parts = output.findSplitAfter(pathEnd);
885 
886             if (!parts || !exists(parts[0]))
887                 return false;
888 
889             output = parts[1];
890             continue;
891         }
892 
893         // $?:<predicate>=<content>(;<predicate>=<content>)*(;<default>)?$
894         string toSkip = null;
895 
896         foreach (const chunk; refparts[0].splitter('|'))
897         {
898             // ( <predicate> , "=", <content> )
899             const conditional = chunk.findSplit("=");
900 
901             if (!conditional) // <default>
902             {
903                 toSkip = chunk;
904                 break;
905             }
906             // Match against OS or model (accepts "32mscoff" as "32")
907             else if (conditional[0].splitter('+').all!(c => c.among(envData.os, envData.model, envData.model[0 .. min(2, $)])))
908             {
909                 toSkip = conditional[2];
910                 break;
911             }
912         }
913 
914         if (toSkip !is null && !output.skipOver(toSkip))
915             return false;
916     }
917 }
918 
919 unittest
920 {
921     EnvData ed;
922     version (Windows)
923         ed.sep = `\`;
924     else
925         ed.sep = `/`;
926 
927     assert( compareOutput(`Grass is green`, `Grass is green`, ed));
928     assert(!compareOutput(`Grass is green`, `Grass was green`, ed));
929 
930     assert( compareOutput(`Bob took 12 apples`, `Bob took $n$ apples`, ed));
931     assert(!compareOutput(`Bob took abc apples`, `Bob took $n$ apples`, ed));
932     assert(!compareOutput(`Bob took 12 berries`, `Bob took $n$ apples`, ed));
933 
934     assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:d_do_test.d$ is important`, ed));
935     assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:test/tools/d_do_test.d$ is important`, ed));
936 
937     ed.sep = "/";
938     assert(!compareOutput(`See /path/to/druntime/import/object.d`, `See $p:druntime/import/object.d$`, ed));
939 
940     assertThrown(compareOutput(`Path /a/b/c.d!`, `Path $p:c.d!`, ed)); // Missing closing $
941 
942     const fmt = "This $?:windows=A|posix=B|C$ uses $?:64=1|32=2|3$ bytes";
943 
944     assert( compareOutput("This C uses 3 bytes", fmt, ed));
945 
946     ed.os = "posix";
947     ed.model = "64";
948     assert( compareOutput("This B uses 1 bytes", fmt, ed));
949     assert(!compareOutput("This C uses 3 bytes", fmt, ed));
950 
951     const emptyFmt = "On <$?:windows=abc|$> use <$?:posix=$>!";
952     assert(compareOutput("On <> use <>!", emptyFmt, ed));
953 
954     ed.model = "32mscoff";
955     assert(compareOutput("size_t is uint!", "size_t is $?:32=uint|64=ulong$!", ed));
956 
957     assert(compareOutput("no", "$?:posix+64=yes|no$", ed));
958     ed.model = "64";
959     assert(compareOutput("yes", "$?:posix+64=yes|no$", ed));
960 }
961 
962 /++
963 Creates a diff of the expected and actual test output.
964 
965 Params:
966     expected     = the expected output
967     expectedFile = file containing expected (if present, null otherwise)
968     actual       = the actual output
969     name         = the test files name
970 
971 Returns: the comparison created by the `diff` utility
972 ++/
973 string generateDiff(const string expected, string expectedFile,
974     const string actual, const string name)
975 {
976     string actualFile = tempDir.buildPath("actual_" ~ name);
977     File(actualFile, "w").writeln(actual); // Append \n
978     scope (exit) remove(actualFile);
979 
980     const needTmp = !expectedFile;
981     if (needTmp) // Create a temporary file
982     {
983         expectedFile = tempDir.buildPath("expected_" ~ name);
984         File(expectedFile, "w").writeln(expected); // Append \n
985     }
986     // Remove temporary file
987     scope (exit) if (needTmp)
988         remove(expectedFile);
989 
990     const cmd = ["diff", "-pu", "--strip-trailing-cr", expectedFile, actualFile];
991     try
992     {
993         string diff = std.process.execute(cmd).output;
994         // Skip diff's status lines listing the diffed files and line count
995         foreach (_; 0..3)
996             diff = diff.findSplitAfter("\n")[1];
997         return diff;
998     }
999     catch (Exception e)
1000         return format(`%-(%s, %) failed: %s`, cmd, e.msg);
1001 }
1002 
1003 class SilentQuit : Exception { this() { super(null); } }
1004 
1005 class CompareException : Exception
1006 {
1007     string expected;
1008     string actual;
1009     bool fromRun; /// Compared execution instead of compilation output
1010 
1011     this(string expected, string actual, string diff, bool fromRun = false) {
1012         string msg = "\nexpected:\n----\n" ~ expected ~
1013             "\n----\nactual:\n----\n" ~ actual ~
1014             "\n----\ndiff:\n----\n" ~ diff ~ "----\n";
1015         super(msg);
1016         this.expected = expected;
1017         this.actual = actual;
1018         this.fromRun = fromRun;
1019     }
1020 }
1021 
1022 version(unittest) void main(){} else
1023 int main(string[] args)
1024 {
1025     try { return tryMain(args); }
1026     catch(SilentQuit) { return 1; }
1027 }
1028 
1029 int tryMain(string[] args)
1030 {
1031     if (args.length != 2)
1032     {
1033         usage();
1034         return 1;
1035     }
1036 
1037     immutable envData = processEnvironment();
1038 
1039     const input_file     = args[1];
1040     const input_dir      = input_file.dirName();
1041     const test_base_name = input_file.baseName();
1042     const test_name      = test_base_name.stripExtension();
1043 
1044     const result_path    = envData.results_dir ~ envData.sep;
1045     const output_dir     = result_path ~ input_dir;
1046     const output_file    = result_path ~ input_file ~ ".out";
1047 
1048     TestArgs testArgs;
1049     switch (input_dir)
1050     {
1051         case "compilable":              testArgs.mode = TestMode.COMPILE;      break;
1052         case "fail_compilation":        testArgs.mode = TestMode.FAIL_COMPILE; break;
1053         case "runnable", "runnable_cxx":
1054             // running & linking costs time - for coverage builds we can save this
1055             testArgs.mode = envData.coverage_build ? TestMode.COMPILE : TestMode.RUN;
1056             break;
1057 
1058         case "dshell":
1059             testArgs.mode = TestMode.DSHELL;
1060             return runDShellTest(input_dir, test_name, envData, output_dir, output_file);
1061 
1062         default:
1063             writefln("Error: invalid test directory '%s', expected 'compilable', 'fail_compilation', 'runnable', 'runnable_cxx' or 'dshell'", input_dir);
1064             return 1;
1065     }
1066 
1067     if (test_base_name.extension() == ".sh")
1068     {
1069         string file = cast(string) std.file.read(input_file);
1070         string disabledPlatforms;
1071         if (findTestParameter(envData, file, "DISABLED", disabledPlatforms))
1072         {
1073             const reason = getDisabledReason(split(disabledPlatforms), envData);
1074             if (reason.length != 0)
1075             {
1076                 writefln(" ... %-30s [DISABLED %s]", input_file, reason);
1077                 return 0;
1078             }
1079         }
1080 
1081         return runBashTest(input_dir, test_name, envData);
1082     }
1083 
1084     // envData.sep is required as the results_dir path can be `generated`
1085     const absoluteResultDirPath = envData.results_dir.absolutePath ~ envData.sep;
1086     const resultsDirReplacement = "{{RESULTS_DIR}}" ~ envData.sep;
1087     const test_app_dmd_base = output_dir ~ envData.sep ~ test_name ~ "_";
1088 
1089     auto stopWatch = StopWatch(AutoStart.no);
1090     if (envData.printRuntime)
1091         stopWatch.start();
1092 
1093     if (!gatherTestParameters(testArgs, input_dir, input_file, envData))
1094         return 0;
1095 
1096     // Clear the DFLAGS environment variable if it was specified in the test file
1097     if (testArgs.clearDflags)
1098     {
1099         // `environment["DFLAGS"] = "";` doesn't seem to work on Win32 (might be a bug
1100         // in std.process). So, resorting to `putenv` in snn.lib
1101         version(Win32)
1102         {
1103             putenv("DFLAGS=");
1104         }
1105         else
1106         {
1107             environment["DFLAGS"] = "";
1108         }
1109     }
1110 
1111     writef(" ... %-30s %s%s(%s)",
1112             input_file,
1113             testArgs.requiredArgs,
1114             (!testArgs.requiredArgs.empty ? " " : ""),
1115             testArgs.permuteArgs);
1116 
1117     if (testArgs.isDisabled)
1118     {
1119         writef("!!! [DISABLED %s]", testArgs.disabledReason);
1120         if (!envData.tryDisabled)
1121         {
1122             writeln();
1123             return 0;
1124         }
1125     }
1126 
1127     removeIfExists(output_file);
1128 
1129     auto f = File(output_file, "a");
1130 
1131     if (
1132         //prepare cpp extra sources
1133         !collectExtraSources(input_dir, output_dir, testArgs.cppSources, testArgs.sources, envData, envData.ccompiler, testArgs.cxxflags, f) ||
1134 
1135         //prepare objc extra sources
1136         !collectExtraSources(input_dir, output_dir, testArgs.objcSources, testArgs.sources, envData, "clang", null, f)
1137     )
1138     {
1139         writeln();
1140 
1141         // Ignore failed test
1142         if (testArgs.isDisabled)
1143             return 0;
1144 
1145         f.close();
1146         printTestFailure(input_file, output_file);
1147         return 1;
1148     }
1149 
1150     enum Result { continue_, return0, return1 }
1151 
1152     // Runs the test with a specific combination of arguments
1153     Result testCombination(bool autoCompileImports, string argSet, size_t permuteIndex, string permutedArgs)
1154     {
1155         string test_app_dmd = test_app_dmd_base ~ to!string(permuteIndex) ~ envData.exe;
1156         string command; // copy of the last executed command so that it can be re-invoked on failures
1157         try
1158         {
1159             string[] toCleanup;
1160 
1161             auto thisRunName = genTempFilename(result_path);
1162             auto fThisRun = File(thisRunName, "w");
1163             scope(exit)
1164             {
1165                 fThisRun.close();
1166                 f.write(readText(thisRunName));
1167                 f.writeln();
1168                 removeIfExists(thisRunName);
1169             }
1170 
1171             string compile_output;
1172             if (!testArgs.compileSeparately)
1173             {
1174                 string objfile = output_dir ~ envData.sep ~ test_name ~ "_" ~ to!string(permuteIndex) ~ envData.obj;
1175                 toCleanup ~= objfile;
1176 
1177                 command = format("%s -conf= -m%s -I%s %s %s -od%s -of%s %s %s%s %s", envData.dmd, envData.model, input_dir,
1178                         testArgs.requiredArgs, permutedArgs, output_dir,
1179                         (testArgs.mode == TestMode.RUN || testArgs.link ? test_app_dmd : objfile),
1180                         argSet,
1181                         (testArgs.mode == TestMode.RUN || testArgs.link ? "" : "-c "),
1182                         join(testArgs.sources, " "),
1183                         (autoCompileImports ? "-i" : join(testArgs.compiledImports, " ")));
1184 
1185                 try
1186                     compile_output = execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path);
1187                 catch (Exception e)
1188                 {
1189                     writeln(""); // We're at "... runnable/xxxx.d (args)"
1190                     printCppSources(testArgs.sources);
1191                     throw e;
1192                 }
1193             }
1194             else
1195             {
1196                 foreach (filename; testArgs.sources ~ (autoCompileImports ? null : testArgs.compiledImports))
1197                 {
1198                     string newo= result_path ~ replace(replace(filename, ".d", envData.obj), envData.sep~"imports"~envData.sep, envData.sep);
1199                     toCleanup ~= newo;
1200 
1201                     command = format("%s -conf= -m%s -I%s %s %s -od%s -c %s %s", envData.dmd, envData.model, input_dir,
1202                         testArgs.requiredArgs, permutedArgs, output_dir, argSet, filename);
1203                     compile_output ~= execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path);
1204                 }
1205 
1206                 if (testArgs.mode == TestMode.RUN || testArgs.link)
1207                 {
1208                     // link .o's into an executable
1209                     command = format("%s -conf= -m%s%s%s %s %s -od%s -of%s %s", envData.dmd, envData.model,
1210                         autoCompileImports ? " -i" : "",
1211                         autoCompileImports ? "extraSourceIncludePaths" : "",
1212                         envData.required_args, testArgs.requiredArgsForLink, output_dir, test_app_dmd, join(toCleanup, " "));
1213 
1214                     execute(fThisRun, command, true, result_path);
1215                 }
1216             }
1217 
1218             void prepare(ref string compile_output)
1219             {
1220                 if (compile_output.empty)
1221                     return;
1222 
1223                 compile_output = compile_output.unifyNewLine();
1224                 compile_output = std.regex.replaceAll(compile_output, regex(`^DMD v2\.[0-9]+.*\n? DEBUG$`, "m"), "");
1225                 compile_output = std..string.strip(compile_output);
1226                 // replace test_result path with fixed ones
1227                 compile_output = compile_output.replace(result_path, resultsDirReplacement);
1228                 compile_output = compile_output.replace(absoluteResultDirPath, resultsDirReplacement);
1229 
1230                 compile_output.applyOutputTransformations(testArgs.transformOutput);
1231             }
1232 
1233             prepare(compile_output);
1234 
1235             auto m = std.regex.match(compile_output, `Internal error: .*$`);
1236             enforce(!m, m.hit);
1237             m = std.regex.match(compile_output, `core.exception.AssertError@dmd.*`);
1238             enforce(!m, m.hit);
1239 
1240             // Prepare and append the content of each OUTPUT_FILE conforming to
1241             // the HAR (https://code.dlang.org/packages/har) format, e.g.
1242             // === <FILENAME_1>
1243             // <CONTENT_1>
1244             // === <FILENAME_2>
1245             // <CONTENT_2>
1246             // ...
1247             foreach (const outfile; testArgs.outputFiles)
1248             {
1249                 string path = outfile;
1250                 replaceResultsDir(path, envData);
1251 
1252                 // Don't abort if a file is missing, at least verify the remaining output.
1253                 string content = readText(path).ifThrown("<< File missing >>");
1254                 prepare(content);
1255 
1256                 // Make sure file starts on a new line
1257                 if (!compile_output.empty && !compile_output.endsWith("\n"))
1258                     compile_output ~= '\n';
1259 
1260                 // Prepend a header listing the explicit file
1261                 compile_output.reserve(outfile.length + content.length + 5);
1262 
1263                 compile_output ~= "=== ";
1264                 compile_output ~= outfile;
1265                 compile_output ~= '\n';
1266                 compile_output ~= content;
1267             }
1268 
1269             if (!compareOutput(compile_output, testArgs.compileOutput, envData))
1270             {
1271                 const diff = generateDiff(testArgs.compileOutput, testArgs.compileOutputFile,
1272                                             compile_output, test_base_name);
1273                 throw new CompareException(testArgs.compileOutput, compile_output, diff);
1274             }
1275 
1276             if (testArgs.mode == TestMode.RUN)
1277             {
1278                 toCleanup ~= test_app_dmd;
1279                 version(Windows)
1280                     if (envData.usingMicrosoftCompiler)
1281                     {
1282                         toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".ilk";
1283                         toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".pdb";
1284                     }
1285 
1286                 if (testArgs.gdbScript is null)
1287                 {
1288                     command = test_app_dmd;
1289                     if (testArgs.executeArgs) command ~= " " ~ testArgs.executeArgs;
1290 
1291                     const output = execute(fThisRun, command, true, result_path)
1292                                     .strip()
1293                                     .unifyNewLine();
1294 
1295                     if (testArgs.runOutput && !compareOutput(output, testArgs.runOutput, envData))
1296                     {
1297                         const diff = generateDiff(testArgs.runOutput, null, output, test_base_name);
1298                         throw new CompareException(testArgs.runOutput, output, diff, true);
1299                     }
1300                 }
1301                 else version (linux)
1302                 {
1303                     // Tests failed on SemaphoreCI when multiple GDB tests were run at once
1304                     scope lockfile = File(envData.results_dir.buildPath("gdb.lock"), "w");
1305                     lockfile.lock();
1306                     scope (exit) lockfile.unlock();
1307 
1308                     auto script = test_app_dmd_base ~ to!string(permuteIndex) ~ ".gdb";
1309                     toCleanup ~= script;
1310                     with (File(script, "w"))
1311                     {
1312                         writeln("set disable-randomization off");
1313                         write(testArgs.gdbScript);
1314                     }
1315                     string gdbCommand = "gdb "~test_app_dmd~" --batch -x "~script;
1316                     auto gdb_output = execute(fThisRun, gdbCommand, true, result_path);
1317                     if (testArgs.gdbMatch !is null)
1318                     {
1319                         enforce(match(gdb_output, regex(testArgs.gdbMatch)),
1320                                 "\nGDB regex: '"~testArgs.gdbMatch~"' didn't match output:\n----\n"~gdb_output~"\n----\n");
1321                     }
1322                 }
1323             }
1324 
1325             fThisRun.close();
1326 
1327             if (testArgs.postScript && !envData.coverage_build)
1328             {
1329                 f.write("Executing post-test script: ");
1330                 string prefix = "";
1331                 version (Windows) prefix = "bash ";
1332                 execute(f, prefix ~ "tools/postscript.sh " ~ testArgs.postScript ~ " " ~ input_dir ~ " " ~ test_name ~ " " ~ thisRunName, true, result_path);
1333             }
1334 
1335             foreach (file; toCleanup) collectException(std.file.remove(file));
1336             return Result.continue_;
1337         }
1338         catch(Exception e)
1339         {
1340             // it failed but it was disabled, exit as if it was successful
1341             if (testArgs.isDisabled)
1342             {
1343                 writeln();
1344                 return Result.return0;
1345             }
1346 
1347             if (envData.autoUpdate)
1348             if (auto ce = cast(CompareException) e)
1349             {
1350                 // remove the output file in test_results as its outdated
1351                 // (might fail for runnable tests on Windows)
1352                 if (output_file.remove().collectException())
1353                     writef("\nWARNING: Failed to remove `%s`!", output_file);
1354 
1355                 if (testArgs.compileOutputFile && !ce.fromRun)
1356                 {
1357                     std.file.write(testArgs.compileOutputFile, ce.actual);
1358                     writefln("\n==> `TEST_OUTPUT_FILE` `%s` has been updated", testArgs.compileOutputFile);
1359                     return Result.return0;
1360                 }
1361 
1362                 auto existingText = input_file.readText;
1363                 auto updatedText = existingText.replace(ce.expected, ce.actual);
1364                 if (existingText != updatedText)
1365                 {
1366                     std.file.write(input_file, updatedText);
1367                     writefln("\n==> `TEST_OUTPUT` of %s has been updated", input_file);
1368                 }
1369                 else
1370                 {
1371                     writefln("\nWARNING: %s has multiple `TEST_OUTPUT` blocks and can't be auto-updated", input_file);
1372                 }
1373                 return Result.return0;
1374             }
1375             f.writeln();
1376             f.writeln("==============================");
1377             f.writef("Test %s failed: ", input_file);
1378             f.writeln(e.msg);
1379             f.close();
1380 
1381             writefln("\nTest %s failed.  The logged output:", input_file);
1382             auto outputText = output_file.readText;
1383             writeln(outputText);
1384             output_file.remove();
1385 
1386             // auto-update if a diff is found and can be updated
1387             if (envData.autoUpdate &&
1388                 outputText.canFind("diff ") && outputText.canFind("--- ") && outputText.canFind("+++ "))
1389             {
1390                 import std.range : dropOne;
1391                 auto newFile = outputText.findSplitAfter("+++ ")[1].until("\t");
1392                 auto baseFile = outputText.findSplitAfter("--- ")[1].until("\t");
1393                 writefln("===> Updating %s with %s", baseFile, newFile);
1394                 newFile.copy(baseFile);
1395                 return Result.return0;
1396             }
1397 
1398             // automatically rerun a segfaulting test and print its stack trace
1399             version(linux)
1400             if (e.msg.canFind("exited with rc == 139"))
1401             {
1402                 auto gdbCommand = "gdb -q -n -ex 'set backtrace limit 100' -ex run -ex bt -batch -args " ~ command;
1403                 spawnShell(gdbCommand).wait;
1404             }
1405 
1406             return Result.return1;
1407         }
1408     }
1409 
1410     size_t index = 0; // index over all tests to avoid identical output names in consecutive tests
1411     auto argSets = (testArgs.argSets.length == 0) ? [""] : testArgs.argSets;
1412     for(auto autoCompileImports = false;; autoCompileImports = true)
1413     {
1414         foreach(argSet; argSets)
1415         {
1416             foreach (c; combinations(testArgs.permuteArgs))
1417             {
1418                 final switch(testCombination(autoCompileImports, argSet, index, c))
1419                 {
1420                     case Result.continue_: break;
1421                     case Result.return0: return 0;
1422                     case Result.return1: return 1;
1423                 }
1424                 index++;
1425             }
1426         }
1427         if(autoCompileImports || testArgs.compiledImports.length == 0)
1428             break;
1429     }
1430 
1431     if (envData.printRuntime)
1432     {
1433         const long ms = stopWatch.peek.total!"msecs";
1434         writefln("   [%.3f secs]", ms / 1000.0);
1435     }
1436     else
1437         writeln();
1438 
1439     // it was disabled but it passed! print an informational message
1440     if (testArgs.isDisabled)
1441         writefln(" !!! %-30s DISABLED but PASSES!", input_file);
1442 
1443     return 0;
1444 }
1445 
1446 int runBashTest(string input_dir, string test_name, const ref EnvData envData)
1447 {
1448     enum script = "tools/sh_do_test.sh";
1449 
1450     version(Windows)
1451     {
1452         const cmd = "bash " ~ script ~ ' ' ~ input_dir ~ ' ' ~  test_name;
1453         const env = [
1454             // Make sure the path is bash-friendly
1455             "DMD": envData.dmd.relativePath(dmdTestDir).replace('\\', '/')
1456         ];
1457 
1458         auto process = spawnShell(cmd, env, Config.none, dmdTestDir);
1459     }
1460     else
1461     {
1462         const scriptPath = dmdTestDir.buildPath(script);
1463         auto process = spawnProcess([scriptPath, input_dir, test_name]);
1464     }
1465     return process.wait();
1466 }
1467 
1468 /// Run a dshell test
1469 int runDShellTest(string input_dir, string test_name, const ref EnvData envData,
1470     string output_dir, string output_file)
1471 {
1472     const testScriptDir = buildPath(dmdTestDir, input_dir);
1473     const testScriptPath = buildPath(testScriptDir, test_name ~ ".d");
1474     const testOutDir = buildPath(output_dir, test_name);
1475     const testLogName = format("%s/%s.d", input_dir, test_name);
1476 
1477     writefln(" ... %s", testLogName);
1478 
1479     removeIfExists(output_file);
1480     if (exists(testOutDir))
1481         rmdirRecurse(testOutDir);
1482     mkdirRecurse(testOutDir);
1483 
1484     // create the "dshell" module for the tests
1485     {
1486         auto dshellFile = File(buildPath(testOutDir, "dshell.d"), "w");
1487         dshellFile.writeln(`module dshell;
1488 public import dshell_prebuilt;
1489 static this()
1490 {
1491     dshellPrebuiltInit("` ~ input_dir ~ `", "`, test_name , `");
1492 }
1493 `);
1494     }
1495 
1496     const testScriptExe = buildPath(testOutDir, "run" ~ envData.exe);
1497     const output_file_temp = output_file ~ ".tmp";
1498 
1499     //
1500     // compile the test
1501     //
1502     {
1503         auto outfile = File(output_file_temp, "w");
1504         const compile = [envData.dmd, "-conf=", "-m"~envData.model] ~
1505             envData.picFlag ~ [
1506             "-od" ~ testOutDir,
1507             "-of" ~ testScriptExe,
1508             "-I=" ~ testScriptDir,
1509             "-I=" ~ testOutDir,
1510             "-I=" ~ buildPath(dmdTestDir, "tools", "dshell_prebuilt"),
1511             "-i",
1512             // Causing linker errors for some reason?
1513             "-i=-dshell_prebuilt", buildPath(envData.results_dir, "dshell_prebuilt" ~ envData.obj),
1514             testScriptPath,
1515         ];
1516         outfile.writeln("[COMPILE_TEST] ", escapeShellCommand(compile));
1517         // Note that spawnprocess closes the file, so it will need to be re-opened
1518         // below when we run the test
1519         auto compileProc = std.process.spawnProcess(compile, stdin, outfile, outfile);
1520         const exitCode = wait(compileProc);
1521         if (exitCode != 0)
1522         {
1523             printTestFailure(testLogName, output_file_temp);
1524             return exitCode;
1525         }
1526     }
1527 
1528     //
1529     // run the test
1530     //
1531     {
1532         auto outfile = File(output_file_temp, "a");
1533         const runTest = [testScriptExe];
1534         outfile.writeln("[RUN_TEST] ", escapeShellCommand(runTest));
1535         auto runTestProc = std.process.spawnProcess(runTest, stdin, outfile, outfile);
1536         const exitCode = wait(runTestProc);
1537 
1538         if (exitCode == 125) // = DISABLED from tools/dshell_prebuilt.d
1539         {
1540             writefln(" !!! %s is disabled!", testLogName);
1541             return 0;
1542         }
1543         else if (exitCode != 0)
1544         {
1545             printTestFailure(testLogName, output_file_temp);
1546             return exitCode;
1547         }
1548     }
1549 
1550     rename(output_file_temp, output_file);
1551     // TODO: should we remove all the test artifacts if the test passes? rmdirRecurse(testOutDir)?
1552     return 0;
1553 }
1554 
1555 void printTestFailure(string testLogName, string output_file_temp)
1556 {
1557     writeln("==============================");
1558     writefln("Test '%s' failed. The logged output:", testLogName);
1559     const output = readText(output_file_temp);
1560     write(output);
1561     if (!output.endsWith("\n"))
1562           writeln();
1563     writeln("==============================");
1564     remove(output_file_temp);
1565 }
1566 
1567 /**
1568  * Print symbols in C++ objects
1569  *
1570  * If linking failed, we print the symbols present in the C++ object file being
1571  * linked it. This is so that C++ `runnable` tests are easier to debug,
1572  * as the CI machines can have different environment than the users,
1573  * and it is generally painful to work with them when trying to support
1574  * newer (C++11, C++14, C++17, etc...) features.
1575  */
1576 void printCppSources (in const(char)[][] compiled)
1577 {
1578     version (Posix)
1579     {
1580         foreach (file; compiled)
1581         {
1582             if (!file.endsWith(".cpp.o"))
1583                 continue;
1584             writeln("========== Symbols for C++ object file: ", file, " ==========");
1585             std.process.spawnProcess(["nm", file]).wait();
1586         }
1587     }
1588 }