forked from dlang/dmd
-
Notifications
You must be signed in to change notification settings - Fork 1
/
run.d
executable file
·656 lines (562 loc) · 19.2 KB
/
run.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
#!/usr/bin/env rdmd
/**
DMD testsuite runner
Usage:
./run.d <test-file>...
Example:
./run.d runnable/template2962.d fail_compilation/fail282.d
See the README.md for all available test targets
*/
import std.algorithm, std.conv, std.datetime, std.exception, std.file, std.format,
std.getopt, std.parallelism, std.path, std.process, std.range, std.stdio,
std.string, std.traits, core.atomic;
import tools.paths;
const scriptDir = __FILE_FULL_PATH__.dirName.buildNormalizedPath;
immutable testDirs = ["runnable", "runnable_cxx", "dshell", "compilable", "fail_compilation"];
shared bool verbose; // output verbose logging
shared bool force; // always run all tests (ignores timestamp checking)
shared string hostDMD; // path to host DMD binary (used for building the tools)
shared string unitTestRunnerCommand;
// These long-running runnable tests will be put in front, in this order, to
// make parallelization more effective.
immutable slowRunnableTests = [
"test34.d",
"test28.d",
"issue8671.d",
"test20855.d",
"test18545.d",
"test42.d",
"lazy.d",
"xtest46_gc.d",
"argufilem.d",
"xtest46.d",
"sdtor.d",
"arrayop.d",
"testgc3.d",
"link14588.d",
"link13415.d",
"paranoia.d",
"template9.d",
];
enum toolsDir = testPath("tools");
enum TestTool unitTestRunner = { name: "unit_test_runner", extraArgs: [toolsDir.buildPath("paths")] };
enum TestTool testRunner = { name: "d_do_test", extraArgs: ["-I" ~ toolsDir, "-i", "-version=NoMain"] };
enum TestTool testRunnerUnittests = { name: "d_do_test-ut",
customSourceFile: toolsDir.buildPath("d_do_test.d"),
extraArgs: testRunner.extraArgs ~ ["-g", "-unittest"],
runAfterBuild: true };
enum TestTool jsonSanitizer = { name: "sanitize_json" };
enum TestTool dshellPrebuilt = { name: "dshell_prebuilt", linksWithTests: true };
immutable struct TestTool
{
/// The name of the tool.
string name;
string customSourceFile;
/// Extra arguments that should be supplied to the compiler when compiling the tool.
string[] extraArgs;
/// Indicates the tool is a binary that links with tests
bool linksWithTests;
bool runAfterBuild;
alias name this;
}
int main(string[] args)
{
try
return tryMain(args);
catch (SilentQuit sq)
return sq.exitCode;
}
int tryMain(string[] args)
{
bool runUnitTests, dumpEnvironment;
int jobs = 2 * totalCPUs;
auto res = getopt(args,
std.getopt.config.passThrough,
"j|jobs", "Specifies the number of jobs (commands) to run simultaneously (default: %d)".format(jobs), &jobs,
"v", "Verbose command output", (cast(bool*) &verbose),
"f", "Force run (ignore timestamps and always run all tests)", (cast(bool*) &force),
"u|unit-tests", "Runs the unit tests", &runUnitTests,
"e|environment", "Print current environment variables", &dumpEnvironment,
);
if (res.helpWanted)
{
defaultGetoptPrinter(`./run.d <test-file>...
Examples:
./run.d runnable/template2962.d # runs a specific tests
./run.d runnable/template2962.d fail_compilation/fail282.d # runs multiple specific tests
./run.d fail_compilation # runs all tests in fail_compilation
./run.d all # runs all tests
./run.d clean # remove all test results
./run.d -u -- unit/deinitialization.d -f Module # runs the unit tests in the file "unit/deinitialization.d" with a UDA containing "Module"
Options:
`, res.options);
"\nSee the README.md for a more in-depth explanation of the test-runner.".writeln;
return 0;
}
defaultPoolThreads = jobs - 1; // main thread executes tasks as well
// parse arguments
args.popFront;
args2Environment(args);
// Run the test suite without default permutations
if (args == ["quick"])
{
args = null;
environment["ARGS"] = "";
}
// allow overwrites from the environment
hostDMD = environment.get("HOST_DMD", "dmd");
unitTestRunnerCommand = resultsDir.buildPath("unit_test_runner").exeName;
// bootstrap all needed environment variables
const env = getEnvironment();
// Dump environnment
if (verbose || dumpEnvironment)
{
writefln("================================================================================");
foreach (key; env.keys.sort())
writefln("%s=%s", key, env[key]);
writefln("================================================================================");
stdout.flush();
}
verifyCompilerExists(env);
prepareOutputDirectory(env);
if (runUnitTests)
{
ensureToolsExists(env, unitTestRunner);
return spawnProcess(unitTestRunnerCommand ~ args, env, Config.none, scriptDir).wait();
}
ensureToolsExists(env, unitTestRunner, testRunner, testRunnerUnittests, jsonSanitizer, dshellPrebuilt);
if (args == ["tools"])
return 0;
// default target
if (!args.length)
args = ["all"];
// move any long-running tests to the front
static size_t sortKey(in ref Target target)
{
const name = target.normalizedTestName;
if (name.startsWith("runnable"))
{
const i = slowRunnableTests.countUntil(name[9 .. $]);
if (i != -1)
return i;
}
return size_t.max;
}
auto targets = args
.predefinedTargets // preprocess
.array
.filterTargets(env);
// Do a manual schwartzSort until all host compilers have a fix
// for the invalid "lhs internal pointer" error (probably >= v2.093)
// See https://github.com/dlang/phobos/pull/7524
foreach (ref target; targets)
target.sortKey = sortKey(target);
targets.sort!("a.sortKey < b.sortKey", SwapStrategy.stable);
if (targets.length > 0)
{
shared string[] failedTargets;
foreach (target; parallel(targets, 1))
{
log("run: %-(%s %)", target.args);
int status = spawnProcess(target.args, env, Config.none, scriptDir).wait;
if (status != 0)
{
const string name = target.filename
? target.normalizedTestName
: "`unit` tests";
writeln(">>> TARGET FAILED: ", name);
synchronized failedTargets ~= name;
}
}
if (failedTargets.length > 0)
{
// print overview of failed targets (for CIs)
writeln("FAILED targets:");
failedTargets.each!(l => writeln("- ", l));
return 1;
}
}
return 0;
}
/// Verify that the compiler has been built.
void verifyCompilerExists(const string[string] env)
{
if (!env["DMD"].exists)
{
stderr.writefln("%s doesn't exist, try building dmd with:\nmake -fposix.mak -j8 -C%s", env["DMD"], scriptDir.dirName.relativePath);
quitSilently(1);
}
}
/// Creates the necessary directories and files for the test runner(s)
void prepareOutputDirectory(const string[string] env)
{
// ensure output directories exist
foreach (dir; testDirs)
resultsDir.buildPath(dir).mkdirRecurse;
version (Windows)
{{
// Environment variables are not properly propagated when using bash from WSL
// Create an additional configuration file that exports `env` entries if missing
File wrapper = File(env["RESULTS_DIR"] ~ "/setup_env.sh", "wb");
foreach (const key, string value; env)
{
// Detect windows paths and translate them to POSIX compatible relative paths
static immutable PATHS = [
"DMD",
"HOST_DMD",
"LIB",
"RESULTS_DIR",
];
if (PATHS.canFind(key))
value = relativePosixPath(value, scriptDir);
// Export as env. variable if unset
wrapper.write(`[ -z "${`, key, `+x}" ] && export `, key, `='`, value, "' ;\n");
}
}}
}
/**
Builds the binaries of the tools required by the testsuite.
Does nothing if the tools already exist and are newer than their source.
*/
void ensureToolsExists(const string[string] env, const TestTool[] tools ...)
{
shared uint failCount = 0;
foreach (tool; tools.parallel(1))
{
string targetBin;
string sourceFile = tool.customSourceFile;
if (tool.linksWithTests)
{
targetBin = resultsDir.buildPath(tool).objName;
if (sourceFile is null)
sourceFile = toolsDir.buildPath(tool, tool ~ ".d");
}
else
{
targetBin = resultsDir.buildPath(tool).exeName;
if (sourceFile is null)
sourceFile = toolsDir.buildPath(tool ~ ".d");
}
if (targetBin.timeLastModified.ifThrown(SysTime.init) >= sourceFile.timeLastModified)
{
log("%s is already up-to-date", tool);
continue;
}
string[] buildCommand;
bool overrideEnv;
if (tool.linksWithTests)
{
// This will compile the dshell library thus needs the actual
// DMD compiler under test
buildCommand = [
env["DMD"],
"-conf=",
"-m"~env["MODEL"],
"-of" ~ targetBin,
"-c",
sourceFile
] ~ getPicFlags(env);
overrideEnv = true;
}
else
{
string model = env["MODEL"];
if (model == "32omf") model = "32";
buildCommand = [
hostDMD,
"-m"~model,
"-of"~targetBin,
sourceFile
] ~ getPicFlags(env) ~ tool.extraArgs;
}
writefln("Executing: %-(%s %)", buildCommand);
stdout.flush();
if (spawnProcess(buildCommand, overrideEnv ? env : null).wait)
{
stderr.writefln("failed to build '%s'", targetBin);
atomicOp!"+="(failCount, 1);
continue;
}
if (tool.runAfterBuild)
{
writefln("Executing: %s", targetBin);
stdout.flush();
if (spawnProcess([targetBin], null).wait)
{
stderr.writefln("'%s' failed", targetBin);
atomicOp!"+="(failCount, 1);
}
}
}
if (failCount > 0)
quitSilently(1); // error already printed
}
/// A single target to execute.
struct Target
{
/**
The filename of the target.
Might be `null` if the target is not for a single file.
*/
string filename;
/// The arguments how to execute the target.
string[] args;
/// Returns: the normalized test name
static string normalizedTestName(string filename)
{
return filename
.absolutePath
.dirName
.baseName
.buildPath(filename.baseName);
}
string normalizedTestName() const
{
return Target.normalizedTestName(filename);
}
/// Returns: `true` if the test exists
bool exists() const
{
// This is assumed to be the `unit_tests` target which always exists
if (filename.empty)
return true;
return testPath(normalizedTestName).exists;
}
size_t sortKey;
}
/**
Goes through the target list and replaces short-hand targets with their expanded version.
Special targets:
- clean -> removes resultsDir + immediately stops the runner
*/
Target[] predefinedTargets(string[] targets)
{
static findFiles(string dir)
{
return testPath(dir).dirEntries("*{.d,.c,.i,.sh}", SpanMode.shallow).map!(e => e.name);
}
static Target createUnitTestTarget()
{
Target target = { args: [unitTestRunnerCommand] };
return target;
}
static Target createTestTarget(string filename)
{
Target target = {
filename: filename,
args: [
resultsDir.buildPath(testRunner.name.exeName),
Target.normalizedTestName(filename)
]
};
return target;
}
Appender!(Target[]) newTargets;
foreach (t; targets)
{
t = t.buildNormalizedPath; // remove trailing slashes
switch (t)
{
case "clean":
if (resultsDir.exists)
resultsDir.rmdirRecurse;
quitSilently(0);
break;
case "run_runnable_tests", "runnable":
newTargets.put(findFiles("runnable").map!createTestTarget);
break;
case "run_runnable_cxx_tests", "runnable_cxx":
newTargets.put(findFiles("runnable_cxx").map!createTestTarget);
break;
case "run_fail_compilation_tests", "fail_compilation", "fail":
newTargets.put(findFiles("fail_compilation").map!createTestTarget);
break;
case "run_compilable_tests", "compilable", "compile":
newTargets.put(findFiles("compilable").map!createTestTarget);
break;
case "run_dshell_tests", "dshell":
newTargets.put(findFiles("dshell").map!createTestTarget);
break;
case "all":
version (FreeBSD) { /* ??? unittest runner fails for no good reason on GHA. */ }
else
newTargets ~= createUnitTestTarget();
foreach (testDir; testDirs)
newTargets.put(findFiles(testDir).map!createTestTarget);
break;
case "unit_tests":
newTargets ~= createUnitTestTarget();
break;
default:
newTargets ~= createTestTarget(t);
}
}
return newTargets.data;
}
// Removes targets that do not need updating (i.e. their .out file exists and is newer than the source file)
Target[] filterTargets(Target[] targets, const string[string] env)
{
bool error;
foreach (target; targets)
{
if (!target.exists)
{
writefln("Warning: %s can't be found", target.normalizedTestName);
error = true;
}
}
if (error)
quitSilently(1);
Target[] targetsThatNeedUpdating;
foreach (t; targets)
{
immutable testName = t.normalizedTestName;
auto resultRunTime = resultsDir.buildPath(testName ~ ".out").timeLastModified.ifThrown(SysTime.init);
if (!force && resultRunTime > testPath(testName).timeLastModified &&
resultRunTime > env["DMD"].timeLastModified.ifThrown(SysTime.init))
log("%s is already up-to-date", testName);
else
targetsThatNeedUpdating ~= t;
}
return targetsThatNeedUpdating;
}
// Add additional make-like assignments to the environment
// e.g. ./run.d ARGS=foo -> sets ARGS to 'foo'
void args2Environment(ref string[] args)
{
bool tryToAdd(string arg)
{
const sep = arg.indexOf('=');
if (sep == -1)
return false;
environment[arg[0 .. sep]] = arg[sep+1 .. $];
return true;
}
args = args.filter!(a => !tryToAdd(a)).array;
}
/**
Checks whether the environment already contains a value for key and if so, sets
the found value to the new environment object.
Otherwise uses the `default_` value as fallback.
Params:
env = environment to write the check to
key = key to check for existence and write into the new env
default_ = fallback value if the key doesn't exist in the global environment
*/
string setDefault(string[string] env, string key, string default_)
{
if (key in environment)
env[key] = environment[key];
else
env[key] = default_;
return env[key];
}
// Sets the environment variables required by d_do_test and sh_do_test.sh
string[string] getEnvironment()
{
string[string] env;
env["RESULTS_DIR"] = resultsDir;
env["OS"] = os;
env["MODEL"] = model;
env["DMD_MODEL"] = dmdModel;
env["BUILD"] = build;
env["EXE"] = exeExtension;
env["DMD"] = dmdPath;
env.setDefault("DMD_TEST_COVERAGE", "0");
const generatedSuffix = "generated/%s/%s/%s".format(os, build, model);
version(Windows)
{
env.setDefault("ARGS", "-inline -release -g -O");
env["OBJ"] = ".obj";
env["DSEP"] = `\\`;
env["SEP"] = `\`;
auto druntimePath = environment.get("DRUNTIME_PATH", testPath(`..\..\druntime`));
auto phobosPath = environment.get("PHOBOS_PATH", testPath(`..\..\..\phobos`));
env["DFLAGS"] = `-I"%s\import" -I"%s"`.format(druntimePath, phobosPath);
env["LIB"] = phobosPath ~ ";" ~ environment.get("LIB");
}
else
{
env.setDefault("ARGS", "-inline -release -g -O -fPIC");
env["OBJ"] = ".o";
env["DSEP"] = "/";
env["SEP"] = "/";
auto druntimePath = environment.get("DRUNTIME_PATH", testPath(`../../druntime`));
auto phobosPath = environment.get("PHOBOS_PATH", testPath(`../../../phobos`));
// default to PIC, use PIC=1/0 to en-/disable PIC.
// Note that shared libraries and C files are always compiled with PIC.
bool pic = true;
if (environment.get("PIC", "") == "0")
pic = false;
env["PIC_FLAG"] = pic ? "-fPIC" : "";
env["DFLAGS"] = "-I%s/import -I%s".format(druntimePath, phobosPath)
~ " -L-L%s/%s".format(phobosPath, generatedSuffix);
bool isShared = environment.get("SHARED") != "0" && os.among("linux", "freebsd") > 0;
if (isShared)
env["DFLAGS"] = env["DFLAGS"] ~ " -defaultlib=libphobos2.so -L-rpath=%s/%s".format(phobosPath, generatedSuffix);
if (pic)
env["REQUIRED_ARGS"] = environment.get("REQUIRED_ARGS") ~ " " ~ env["PIC_FLAG"];
version(OSX)
version(X86_64)
env["D_OBJC"] = "1";
}
return env;
}
// Logging primitive
void log(T...)(T args)
{
if (verbose)
writefln(args);
}
// Add the executable filename extension to the given `name` for the current OS.
string exeName(string name)
{
version(Windows)
name ~= ".exe";
return name;
}
// Add the object filename extension to the given `name` for the current OS.
string objName(string name)
{
version(Windows)
return name ~ ".obj";
else
return name ~ ".o";
}
/// Return the correct pic flags as an array of strings
string[] getPicFlags(const string[string] env)
{
version(Windows) {} else
{
const picFlags = env["PIC_FLAG"];
if (picFlags.length)
return picFlags.split();
}
return cast(string[])[];
}
/++
Signals a silent termination while still retaining a controlled shutdown
(including destructors, scope guards, etc).
quitSilently(...) should be used instead of exit(...)
++/
class SilentQuit : Exception
{
/// The exit code
const int exitCode;
///
this(const int exitCode)
{
super(null, null, null);
this.exitCode = exitCode;
}
}
/++
Aborts the current execution by throwing an exception
Params:
exitCode = the exit code
Throws: a SilentQuit instance wrapping exitCode
++/
void quitSilently(const int exitCode)
{
throw new SilentQuit(exitCode);
}