1 #!/usr/bin/env rdmd
2 /*
3  * Distributed under the Boost Software License, Version 1.0.
4  *    (See accompanying file LICENSE_1_0.txt or copy at
5  *          http://www.boost.org/LICENSE_1_0.txt)
6  */
7 module rdmd_test;
8 
9 /**
10     RDMD Test-suite.
11 
12     Authors: Andrej Mitrovic
13 
14     Note:
15     While `rdmd_test` can be run directly, it is recommended to run
16     it via the tools build scripts using the `make test_rdmd` target.
17 
18     When running directly, pass the rdmd binary as the first argument.
19 */
20 
21 import std.algorithm;
22 import std.exception;
23 import std.file;
24 import std.format;
25 import std.getopt;
26 import std.path;
27 import std.process;
28 import std.range;
29 import std.string;
30 import std.stdio;
31 
32 version (Posix)
33 {
34     enum objExt = ".o";
35     enum binExt = "";
36     enum libExt = ".a";
37 }
38 else version (Windows)
39 {
40     enum objExt = ".obj";
41     enum binExt = ".exe";
42     enum libExt = ".lib";
43 }
44 else
45 {
46     static assert(0, "Unsupported operating system.");
47 }
48 
49 bool verbose = false;
50 
51 int main(string[] args)
52 {
53     string defaultCompiler; // name of default compiler expected by rdmd
54     bool concurrencyTest;
55     string model = "64"; // build architecture for dmd
56     string testCompilerList; // e.g. "ldmd2,gdmd" (comma-separated list of compiler names)
57 
58     auto helpInfo = getopt(args,
59         "rdmd-default-compiler", "[REQUIRED] default D compiler used by rdmd executable", &defaultCompiler,
60         "concurrency", "whether to perform the concurrency test cases", &concurrencyTest,
61         "m|model", "architecture to run the tests for [32 or 64]", &model,
62         "test-compilers", "comma-separated list of D compilers to test with rdmd", &testCompilerList,
63         "v|verbose", "verbose output", &verbose,
64     );
65 
66     void reportHelp(string errorMsg = null, string file = __FILE__, size_t line = __LINE__)
67     {
68         defaultGetoptPrinter("rdmd_test: a test suite for rdmd\n\n" ~
69                              "USAGE:\trdmd_test [OPTIONS] <rdmd_binary>\n",
70                              helpInfo.options);
71         enforce(errorMsg is null, errorMsg, file, line);
72     }
73 
74     if (helpInfo.helpWanted || args.length == 1)
75     {
76         reportHelp();
77         return 1;
78     }
79 
80     if (args.length > 2)
81     {
82         writefln("Error: too many non-option arguments, expected 1 but got %s", args.length - 1);
83         return 1; // fail
84     }
85     string rdmd = args[1]; // path to rdmd executable
86 
87     if (rdmd.length == 0)
88         reportHelp("ERROR: missing required --rdmd flag");
89 
90     if (defaultCompiler.length == 0)
91         reportHelp("ERROR: missing required --rdmd-default-compiler flag");
92 
93     enforce(rdmd.exists,
94             format("rdmd executable path '%s' does not exist", rdmd));
95 
96     // copy rdmd executable to temp dir: this enables us to set
97     // up its execution environment with other features, e.g. a
98     // dummy fallback compiler
99     string rdmdApp = tempDir().buildPath("rdmd_app_") ~ binExt;
100     scope (exit) std.file.remove(rdmdApp);
101     copy(rdmd, rdmdApp, Yes.preserveAttributes);
102 
103     runCompilerAgnosticTests(rdmdApp, defaultCompiler, model);
104 
105     // if no explicit list of test compilers is set,
106     // use the default compiler expected by rdmd
107     if (testCompilerList is null)
108         testCompilerList = defaultCompiler;
109 
110     // run the test suite for each specified test compiler
111     foreach (testCompiler; testCompilerList.split(','))
112     {
113         // if compiler is a relative filename it must be converted
114         // to absolute because this test changes directories
115         if (testCompiler.canFind!isDirSeparator || testCompiler.exists)
116             testCompiler = buildNormalizedPath(testCompiler.absolutePath);
117 
118         runTests(rdmdApp, testCompiler, model);
119         if (concurrencyTest)
120             runConcurrencyTest(rdmdApp, testCompiler, model);
121     }
122 
123     return 0;
124 }
125 
126 string compilerSwitch(string compiler) { return "--compiler=" ~ compiler; }
127 
128 string modelSwitch(string model) { return "-m" ~ model; }
129 
130 auto execute(T...)(T args)
131 {
132     import std.stdio : writefln;
133     if (verbose)
134         writefln("[execute] %s", args[0]);
135     return std.process.execute(args);
136 }
137 
138 void runCompilerAgnosticTests(string rdmdApp, string defaultCompiler, string model)
139 {
140     /* Test help string output when no arguments passed. */
141     auto res = execute([rdmdApp]);
142     enforce(res.status == 1, res.output);
143     enforce(res.output.canFind("Usage: rdmd [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]..."));
144 
145     /* Test --help. */
146     res = execute([rdmdApp, "--help"]);
147     enforce(res.status == 0, res.output);
148     enforce(res.output.canFind("Usage: rdmd [RDMD AND DMD OPTIONS]... program [PROGRAM OPTIONS]..."));
149 
150     string helpText = res.output;
151 
152     // verify help text matches expected defaultCompiler
153     {
154         version (Windows) helpText = helpText.replace("\r\n", "\n");
155         enum compilerHelpLine = "  --compiler=comp    use the specified compiler (e.g. gdmd) instead of ";
156         auto offset = helpText.indexOf(compilerHelpLine);
157         enforce(offset >= 0);
158         auto compilerInHelp = helpText[offset + compilerHelpLine.length .. $];
159         compilerInHelp = compilerInHelp[0 .. compilerInHelp.indexOf('\n')];
160         enforce(defaultCompiler.baseName == compilerInHelp,
161             "Expected to find " ~ compilerInHelp ~ " in help text, found " ~ defaultCompiler ~ " instead");
162     }
163 
164     /* Test that unsupported -o... options result in failure */
165     res = execute([rdmdApp, "-o-"]);  // valid option for dmd but unsupported by rdmd
166     enforce(res.status == 1, res.output);
167     enforce(res.output.canFind("Option -o- currently not supported by rdmd"), res.output);
168 
169     res = execute([rdmdApp, "-o-foo"]); // should not be treated the same as -o-
170     enforce(res.status == 1, res.output);
171     enforce(res.output.canFind("Unrecognized option: o-foo"), res.output);
172 
173     res = execute([rdmdApp, "-opbreak"]); // should not be treated like valid -op
174     enforce(res.status == 1, res.output);
175     enforce(res.output.canFind("Unrecognized option: opbreak"), res.output);
176 
177     // run the fallback compiler test (this involves
178     // searching for the default compiler, so cannot
179     // be run with other test compilers)
180     runFallbackTest(rdmdApp, defaultCompiler, model);
181 }
182 
183 auto rdmdArguments(string rdmdApp, string compiler, string model)
184 {
185     return [rdmdApp, compilerSwitch(compiler), modelSwitch(model)];
186 }
187 
188 void runTests(string rdmdApp, string compiler, string model)
189 {
190     // path to rdmd + common arguments (compiler, model)
191     auto rdmdArgs = rdmdArguments(rdmdApp, compiler, model);
192 
193     /* Test --force. */
194     string forceSrc = tempDir().buildPath("force_src_.d");
195     std.file.write(forceSrc, `void main() { pragma(msg, "compile_force_src"); }`);
196 
197     auto res = execute(rdmdArgs ~ [forceSrc]);
198     enforce(res.status == 0, res.output);
199     enforce(res.output.canFind("compile_force_src"));
200 
201     res = execute(rdmdArgs ~ [forceSrc]);
202     enforce(res.status == 0, res.output);
203     enforce(!res.output.canFind("compile_force_src"));  // second call will not re-compile
204 
205     res = execute(rdmdArgs ~ ["--force", forceSrc]);
206     enforce(res.status == 0, res.output);
207     enforce(res.output.canFind("compile_force_src"));  // force will re-compile
208 
209     /* Test --build-only. */
210     string failRuntime = tempDir().buildPath("fail_runtime_.d");
211     std.file.write(failRuntime, "void main() { assert(0); }");
212 
213     res = execute(rdmdArgs ~ ["--force", "--build-only", failRuntime]);
214     enforce(res.status == 0, res.output);  // only built, enforce(0) not called.
215 
216     res = execute(rdmdArgs ~ ["--force", failRuntime]);
217     enforce(res.status == 1, res.output);  // enforce(0) called, rdmd execution failed.
218 
219     string failComptime = tempDir().buildPath("fail_comptime_.d");
220     std.file.write(failComptime, "void main() { static assert(0); }");
221 
222     res = execute(rdmdArgs ~ ["--force", "--build-only", failComptime]);
223     enforce(res.status == 1, res.output);  // building will fail for static enforce(0).
224 
225     res = execute(rdmdArgs ~ ["--force", failComptime]);
226     enforce(res.status == 1, res.output);  // ditto.
227 
228     /* Test --chatty. */
229     string voidMain = tempDir().buildPath("void_main_.d");
230     std.file.write(voidMain, "void main() { }");
231 
232     res = execute(rdmdArgs ~ ["--force", "--chatty", voidMain]);
233     enforce(res.status == 0, res.output);
234     enforce(res.output.canFind("stat "));  // stat should be called.
235 
236     /* Test --dry-run. */
237     res = execute(rdmdArgs ~ ["--force", "--dry-run", failComptime]);
238     enforce(res.status == 0, res.output);  // static enforce(0) not called since we did not build.
239     enforce(res.output.canFind("mkdirRecurse "), res.output);  // --dry-run implies chatty
240 
241     res = execute(rdmdArgs ~ ["--force", "--dry-run", "--build-only", failComptime]);
242     enforce(res.status == 0, res.output);  // --build-only should not interfere with --dry-run
243 
244     /* Test --eval. */
245     res = execute(rdmdArgs ~ ["--force", "-de", "--eval=writeln(`eval_works`);"]);
246     enforce(res.status == 0, res.output);
247     enforce(res.output.canFind("eval_works"));  // there could be a "DMD v2.xxx header in the output"
248 
249     // compiler flags
250     res = execute(rdmdArgs ~ ["--force", "-debug",
251         "--eval=debug {} else assert(false);"]);
252     enforce(res.status == 0, res.output);
253 
254     // When using eval, extra arguments are program arguments
255     res = execute(rdmdArgs ~ ["--force",
256         format("--eval=assert(args[1] == `%s`);", voidMain), voidMain]);
257     enforce(res.status == 0, res.output);
258 
259     /* Test --exclude. */
260     string packFolder = tempDir().buildPath("dsubpack");
261     if (packFolder.exists) packFolder.rmdirRecurse();
262     packFolder.mkdirRecurse();
263     scope (exit) packFolder.rmdirRecurse();
264 
265     string subModObj = packFolder.buildPath("submod") ~ objExt;
266     string subModSrc = packFolder.buildPath("submod.d");
267     std.file.write(subModSrc, "module dsubpack.submod; void foo() { }");
268 
269     // build an object file out of the dependency
270     res = execute([compiler, modelSwitch(model), "-c", "-of" ~ subModObj, subModSrc]);
271     enforce(res.status == 0, res.output);
272 
273     string subModUser = tempDir().buildPath("subModUser_.d");
274     std.file.write(subModUser, "module subModUser_; import dsubpack.submod; void main() { foo(); }");
275 
276     res = execute(rdmdArgs ~ ["--force", "--exclude=dsubpack", subModUser]);
277     enforce(res.status == 1, res.output);  // building without the dependency fails
278 
279     res = execute(rdmdArgs ~ ["--force", "--exclude=dsubpack", subModObj, subModUser]);
280     enforce(res.status == 0, res.output);  // building with the dependency succeeds
281 
282     /* Test --include. */
283     auto packFolder2 = tempDir().buildPath("std");
284     if (packFolder2.exists) packFolder2.rmdirRecurse();
285     packFolder2.mkdirRecurse();
286     scope (exit) packFolder2.rmdirRecurse();
287 
288     string subModSrc2 = packFolder2.buildPath("foo.d");
289     std.file.write(subModSrc2, "module std.foo; void foobar() { }");
290 
291     std.file.write(subModUser, "import std.foo; void main() { foobar(); }");
292 
293     res = execute(rdmdArgs ~ ["--force", subModUser]);
294     enforce(res.status == 1, res.output);  // building without the --include fails
295 
296     res = execute(rdmdArgs ~ ["--force", "--include=std", subModUser]);
297     enforce(res.status == 0, res.output);  // building with the --include succeeds
298 
299     /* Test --extra-file. */
300 
301     string extraFileDi = tempDir().buildPath("extraFile_.di");
302     std.file.write(extraFileDi, "module extraFile_; void f();");
303     string extraFileD = tempDir().buildPath("extraFile_.d");
304     std.file.write(extraFileD, "module extraFile_; void f() { return; }");
305     string extraFileMain = tempDir().buildPath("extraFileMain_.d");
306     std.file.write(extraFileMain,
307             "module extraFileMain_; import extraFile_; void main() { f(); }");
308 
309     res = execute(rdmdArgs ~ ["--force", extraFileMain]);
310     enforce(res.status == 1, res.output); // undefined reference to f()
311 
312     res = execute(rdmdArgs ~ ["--force",
313             "--extra-file=" ~ extraFileD, extraFileMain]);
314     enforce(res.status == 0, res.output); // now OK
315 
316     /* Test --loop. */
317     {
318     auto testLines = "foo\nbar\ndoo".split("\n");
319 
320     auto pipes = pipeProcess(rdmdArgs ~ ["--force", "--loop=writeln(line);"], Redirect.stdin | Redirect.stdout);
321     foreach (input; testLines)
322         pipes.stdin.writeln(input);
323     pipes.stdin.close();
324 
325     while (!testLines.empty)
326     {
327         auto line = pipes.stdout.readln.strip;
328         if (line.empty || line.startsWith("DMD v")) continue;  // git-head header
329         enforce(line == testLines.front, "Expected %s, got %s".format(testLines.front, line));
330         testLines.popFront;
331     }
332     auto status = pipes.pid.wait();
333     enforce(status == 0);
334     }
335 
336     // vs program file
337     res = execute(rdmdArgs ~ ["--force",
338         "--loop=assert(true);", voidMain]);
339     enforce(res.status != 0);
340     enforce(res.output.canFind("Cannot have both --loop and a program file ('" ~
341             voidMain ~ "')."));
342 
343     /* Test --main. */
344     string noMain = tempDir().buildPath("no_main_.d");
345     std.file.write(noMain, "module no_main_; void foo() { }");
346 
347     // test disabled: Optlink creates a dialog box here instead of erroring.
348     /+ res = execute([rdmdApp, " %s", noMain));
349     enforce(res.status == 1, res.output);  // main missing +/
350 
351     res = execute(rdmdArgs ~ ["--main", noMain]);
352     enforce(res.status == 0, res.output);  // main added
353 
354     string intMain = tempDir().buildPath("int_main_.d");
355     std.file.write(intMain, "int main(string[] args) { return args.length; }");
356 
357     res = execute(rdmdArgs ~ ["--main", intMain]);
358     enforce(res.status == 1, res.output);  // duplicate main
359 
360     /* Test --makedepend. */
361 
362     string packRoot = packFolder.buildPath("../").buildNormalizedPath();
363 
364     string depMod = packRoot.buildPath("depMod_.d");
365     std.file.write(depMod, "module depMod_; import dsubpack.submod; void main() { }");
366 
367     res = execute(rdmdArgs ~ ["-I" ~ packRoot, "--makedepend",
368             "-of" ~ depMod[0..$-2], depMod]);
369 
370     import std.ascii : newline;
371 
372     // simplistic checks
373     enforce(res.output.canFind(depMod[0..$-2] ~ ": \\" ~ newline));
374     enforce(res.output.canFind(newline ~ " " ~ depMod ~ " \\" ~ newline));
375     enforce(res.output.canFind(newline ~ " " ~ subModSrc));
376     enforce(res.output.canFind(newline ~  subModSrc ~ ":" ~ newline));
377     enforce(!res.output.canFind("\\" ~ newline ~ newline));
378 
379     /* Test --makedepfile. */
380 
381     string depModFail = packRoot.buildPath("depModFail_.d");
382     std.file.write(depModFail, "module depMod_; import dsubpack.submod; void main() { assert(0); }");
383 
384     string depMak = packRoot.buildPath("depMak_.mak");
385     res = execute(rdmdArgs ~ ["--force", "--build-only",
386             "-I" ~ packRoot, "--makedepfile=" ~ depMak,
387             "-of" ~ depModFail[0..$-2], depModFail]);
388     scope (exit) std.file.remove(depMak);
389 
390     string output = std.file.readText(depMak);
391 
392     // simplistic checks
393     enforce(output.canFind(depModFail[0..$-2] ~ ": \\" ~ newline));
394     enforce(output.canFind(newline ~ " " ~ depModFail ~ " \\" ~ newline));
395     enforce(output.canFind(newline ~ " " ~ subModSrc));
396     enforce(output.canFind(newline ~ "" ~ subModSrc ~ ":" ~ newline));
397     enforce(!output.canFind("\\" ~ newline ~ newline));
398     enforce(res.status == 0, res.output);  // only built, enforce(0) not called.
399 
400     /* Test signal propagation through exit codes */
401 
402     version (Posix)
403     {
404         import core.sys.posix.signal;
405         string crashSrc = tempDir().buildPath("crash_src_.d");
406         std.file.write(crashSrc, `void main() { int *p; *p = 0; }`);
407         res = execute(rdmdArgs ~ [crashSrc]);
408         enforce(res.status == -SIGSEGV, format("%s", res));
409     }
410 
411     /* -of doesn't append .exe on Windows: https://d.puremagic.com/issues/show_bug.cgi?id=12149 */
412 
413     version (Windows)
414     {
415         string outPath = tempDir().buildPath("test_of_app");
416         string exePath = outPath ~ ".exe";
417         res = execute(rdmdArgs ~ ["--build-only", "-of" ~ outPath, voidMain]);
418         enforce(exePath.exists(), exePath);
419     }
420 
421     /* Current directory change should not trigger rebuild */
422 
423     res = execute(rdmdArgs ~ [forceSrc]);
424     enforce(res.status == 0, res.output);
425     enforce(!res.output.canFind("compile_force_src"));
426 
427     {
428         auto cwd = getcwd();
429         scope(exit) chdir(cwd);
430         chdir(tempDir);
431 
432         res = execute(rdmdArgs ~ [forceSrc.baseName()]);
433         enforce(res.status == 0, res.output);
434         enforce(!res.output.canFind("compile_force_src"));
435     }
436 
437     auto conflictDir = forceSrc.setExtension(".dir");
438     if (exists(conflictDir))
439     {
440         if (isFile(conflictDir))
441             remove(conflictDir);
442         else
443             rmdirRecurse(conflictDir);
444     }
445     mkdir(conflictDir);
446     res = execute(rdmdArgs ~ ["-of" ~ conflictDir, forceSrc]);
447     enforce(res.status != 0, "-of set to a directory should fail");
448 
449     res = execute(rdmdArgs ~ ["-of=" ~ conflictDir, forceSrc]);
450     enforce(res.status != 0, "-of= set to a directory should fail");
451 
452     /* rdmd should force rebuild when --compiler changes: https://issues.dlang.org/show_bug.cgi?id=15031 */
453 
454     res = execute(rdmdArgs ~ [forceSrc]);
455     enforce(res.status == 0, res.output);
456     enforce(!res.output.canFind("compile_force_src"));
457 
458     auto fullCompilerPath = environment["PATH"]
459         .splitter(pathSeparator)
460         .map!(dir => dir.buildPath(compiler ~ binExt))
461         .filter!exists
462         .front;
463 
464     res = execute([rdmdApp, "--compiler=" ~ fullCompilerPath, modelSwitch(model), forceSrc]);
465     enforce(res.status == 0, res.output ~ "\nCan't run with --compiler=" ~ fullCompilerPath);
466 
467     // Create an empty temporary directory and clean it up when exiting scope
468     static struct TmpDir
469     {
470         string name;
471         this(string name)
472         {
473             this.name = name;
474             if (exists(name)) rmdirRecurse(name);
475             mkdir(name);
476         }
477         @disable this(this);
478         ~this()
479         {
480             version (Windows)
481             {
482                 import core.thread;
483                 Thread.sleep(100.msecs); // Hack around Windows locking the directory
484             }
485             rmdirRecurse(name);
486         }
487         alias name this;
488     }
489 
490     /* tmpdir */
491     {
492         res = execute(rdmdArgs ~ [forceSrc, "--build-only"]);
493         enforce(res.status == 0, res.output);
494 
495         TmpDir tmpdir = "rdmdTest";
496         res = execute(rdmdArgs ~ ["--tmpdir=" ~ tmpdir, forceSrc, "--build-only"]);
497         enforce(res.status == 0, res.output);
498         enforce(res.output.canFind("compile_force_src"));
499     }
500 
501     /* RDMD fails at building a lib when the source is in a subdir: https://issues.dlang.org/show_bug.cgi?id=14296 */
502     {
503         TmpDir srcDir = "rdmdTest";
504         string srcName = srcDir.buildPath("test.d");
505         std.file.write(srcName, `void fun() {}`);
506         if (exists("test" ~ libExt)) std.file.remove("test" ~ libExt);
507 
508         res = execute(rdmdArgs ~ ["--build-only", "--force", "-lib", srcName]);
509         enforce(res.status == 0, res.output);
510         enforce(exists(srcDir.buildPath("test" ~ libExt)));
511         enforce(!exists("test" ~ libExt));
512     }
513 
514     // Test with -od
515     {
516         TmpDir srcDir = "rdmdTestSrc";
517         TmpDir libDir = "rdmdTestLib";
518 
519         string srcName = srcDir.buildPath("test.d");
520         std.file.write(srcName, `void fun() {}`);
521 
522         res = execute(rdmdArgs ~ ["--build-only", "--force", "-lib", "-od" ~ libDir, srcName]);
523         enforce(res.status == 0, res.output);
524         enforce(exists(libDir.buildPath("test" ~ libExt)));
525 
526         // test with -od= too
527         TmpDir altLibDir = "rdmdTestAltLib";
528         res = execute(rdmdArgs ~ ["--build-only", "--force", "-lib", "-od=" ~ altLibDir, srcName]);
529         enforce(res.status == 0, res.output);
530         enforce(exists(altLibDir.buildPath("test" ~ libExt)));
531     }
532 
533     // Test with -of
534     {
535         TmpDir srcDir = "rdmdTestSrc";
536         TmpDir libDir = "rdmdTestLib";
537 
538         string srcName = srcDir.buildPath("test.d");
539         std.file.write(srcName, `void fun() {}`);
540         string libName = libDir.buildPath("libtest" ~ libExt);
541 
542         res = execute(rdmdArgs ~ ["--build-only", "--force", "-lib", "-of" ~ libName, srcName]);
543         enforce(res.status == 0, res.output);
544         enforce(exists(libName));
545 
546         // test that -of= works too
547         string altLibName = libDir.buildPath("altlibtest" ~ libExt);
548 
549         res = execute(rdmdArgs ~ ["--build-only", "--force", "-lib", "-of=" ~ altLibName, srcName]);
550         enforce(res.status == 0, res.output);
551         enforce(exists(altLibName));
552     }
553 
554     /* rdmd --build-only --force -c main.d fails: ./main: No such file or directory: https://issues.dlang.org/show_bug.cgi?id=16962 */
555     {
556         TmpDir srcDir = "rdmdTest";
557         string srcName = srcDir.buildPath("test.d");
558         std.file.write(srcName, `void main() {}`);
559         string objName = srcDir.buildPath("test" ~ objExt);
560 
561         res = execute(rdmdArgs ~ ["--force", "-c", srcName]);
562         enforce(res.status == 0, res.output);
563         enforce(exists(objName));
564     }
565 
566     /* [REG2.072.0] pragma(lib) is broken with rdmd: https://issues.dlang.org/show_bug.cgi?id=16978 */
567     /* GDC does not support `pragma(lib)`, so disable when test compiler is gdmd: https://issues.dlang.org/show_bug.cgi?id=18421
568        (this constraint can be removed once GDC support for `pragma(lib)` is implemented) */
569 
570     version (linux)
571     if (compiler.baseName != "gdmd")
572     {{
573         TmpDir srcDir = "rdmdTest";
574         string libSrcName = srcDir.buildPath("libfun.d");
575         std.file.write(libSrcName, `extern(C) void fun() {}`);
576 
577         res = execute(rdmdArgs ~ ["-lib", libSrcName]);
578         enforce(res.status == 0, res.output);
579         enforce(exists(srcDir.buildPath("libfun" ~ libExt)));
580 
581         string mainSrcName = srcDir.buildPath("main.d");
582         std.file.write(mainSrcName, `extern(C) void fun(); pragma(lib, "fun"); void main() { fun(); }`);
583 
584         res = execute(rdmdArgs ~ ["-L-L" ~ srcDir, mainSrcName]);
585         enforce(res.status == 0, res.output);
586     }}
587 
588     /* https://issues.dlang.org/show_bug.cgi?id=16966 */
589     {
590         immutable voidMainExe = setExtension(voidMain, binExt);
591         res = execute(rdmdArgs ~ [voidMain]);
592         enforce(res.status == 0, res.output);
593         enforce(!exists(voidMainExe));
594         res = execute(rdmdArgs ~ ["--build-only", voidMain]);
595         enforce(res.status == 0, res.output);
596         enforce(exists(voidMainExe));
597         remove(voidMainExe);
598     }
599 
600     /* https://issues.dlang.org/show_bug.cgi?id=17198 - rdmd does not recompile
601     when --extra-file is added */
602     {
603         TmpDir srcDir = "rdmdTest";
604         immutable string src1 = srcDir.buildPath("test.d");
605         immutable string src2 = srcDir.buildPath("test2.d");
606         std.file.write(src1, "int x = 1; int main() { return x; }");
607         std.file.write(src2, "import test; static this() { x = 0; }");
608 
609         res = execute(rdmdArgs ~ [src1]);
610         enforce(res.status == 1, res.output);
611 
612         res = execute(rdmdArgs ~ ["--extra-file=" ~ src2, src1]);
613         enforce(res.status == 0, res.output);
614 
615         res = execute(rdmdArgs ~ [src1]);
616         enforce(res.status == 1, res.output);
617     }
618 
619     version (Posix)
620     {
621         import std.conv : to;
622         auto makeVersion = execute(["make", "--version"]).output.splitLines()[0];
623         if (!makeVersion.skipOver("GNU Make "))
624             stderr.writeln("rdmd_test: Can't detect Make version or not GNU Make, skipping Make SHELL/SHELLFLAGS test");
625         else if (makeVersion.split(".").map!(to!int).array < [3, 82])
626             stderr.writefln("rdmd_test: Make version (%s) is too old, skipping Make SHELL/SHELLFLAGS test", makeVersion);
627         else
628         {
629             auto textOutput = tempDir().buildPath("rdmd_makefile_test.txt");
630             if (exists(textOutput))
631             {
632                 remove(textOutput);
633             }
634             enum makefileFormatter = `.ONESHELL:
635 SHELL = %s
636 .SHELLFLAGS = %-(%s %) --eval
637 %s:
638 	import std.file;
639 	write("$@","hello world\n");`;
640             string makefileString = format!makefileFormatter(rdmdArgs[0], rdmdArgs[1 .. $], textOutput);
641             auto makefilePath = tempDir().buildPath("rdmd_makefile_test.mak");
642             std.file.write(makefilePath, makefileString);
643             auto make = environment.get("MAKE") is null ? "make" : environment.get("MAKE");
644             res = execute([make, "-f", makefilePath]);
645             enforce(res.status == 0, res.output);
646             enforce(std.file.read(textOutput) == "hello world\n");
647         }
648     }
649 }
650 
651 void runConcurrencyTest(string rdmdApp, string compiler, string model)
652 {
653     // path to rdmd + common arguments (compiler, model)
654     auto rdmdArgs = rdmdArguments(rdmdApp, compiler, model);
655 
656     string sleep100 = tempDir().buildPath("delay_.d");
657     std.file.write(sleep100, "void main() { import core.thread; Thread.sleep(100.msecs); }");
658     auto argsVariants =
659     [
660         rdmdArgs ~ [sleep100],
661         rdmdArgs ~ ["--force", sleep100],
662     ];
663     import std.parallelism, std.range, std.random;
664     foreach (rnd; rndGen.parallel(1))
665     {
666         try
667         {
668             auto args = argsVariants[rnd % $];
669             auto res = execute(args);
670             enforce(res.status == 0, res.output);
671         }
672         catch (Exception e)
673         {
674             import std.stdio;
675             writeln(e);
676             break;
677         }
678     }
679 }
680 
681 void runFallbackTest(string rdmdApp, string buildCompiler, string model)
682 {
683     /* https://issues.dlang.org/show_bug.cgi?id=11997
684        if an explicit --compiler flag is not provided, rdmd should
685        search its own binary path first when looking for the default
686        compiler (determined by the compiler used to build it) */
687     string localDMD = buildPath(tempDir(), baseName(buildCompiler).setExtension(binExt));
688     std.file.write(localDMD, ""); // An empty file avoids the "Not a valid 16-bit application" pop-up on Windows
689     scope(exit) std.file.remove(localDMD);
690 
691     auto res = execute(rdmdApp ~ [modelSwitch(model), "--force", "--chatty", "--eval=writeln(`Compiler found.`);"]);
692     enforce(res.status == 1, res.output);
693     enforce(res.output.canFind(format(`spawn [%(%s%),`, localDMD.only)), localDMD ~ " would not have been executed");
694 }