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