dmd/compiler/test/run.d
2023-10-28 00:36:33 +02:00

636 lines
18 KiB
D
Executable file

#!/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 TestTools
{
unitTestRunner = TestTool("unit_test_runner", [toolsDir.buildPath("paths")]),
testRunner = TestTool("d_do_test", ["-I" ~ toolsDir, "-i", "-version=NoMain"]),
jsonSanitizer = TestTool("sanitize_json"),
dshellPrebuilt = TestTool("dshell_prebuilt", null, Yes.linksWithTests),
}
immutable struct TestTool
{
/// The name of the tool.
string name;
/// 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
Flag!"linksWithTests" linksWithTests;
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, TestTools.unitTestRunner);
return spawnProcess(unitTestRunnerCommand ~ args, env, Config.none, scriptDir).wait();
}
ensureToolsExists(env, EnumMembers!TestTools);
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;
if (tool.linksWithTests)
{
targetBin = resultsDir.buildPath(tool).objName;
sourceFile = toolsDir.buildPath(tool, tool ~ ".d");
}
else
{
targetBin = resultsDir.buildPath(tool).exeName;
sourceFile = toolsDir.buildPath(tool ~ ".d");
}
if (targetBin.timeLastModified.ifThrown(SysTime.init) >= sourceFile.timeLastModified)
log("%s is already up-to-date", tool);
else
{
string[] command;
bool overrideEnv;
if (tool.linksWithTests)
{
// This will compile the dshell library thus needs the actual
// DMD compiler under test
command = [
env["DMD"],
"-conf=",
"-m"~env["MODEL"],
"-of" ~ targetBin,
"-c",
sourceFile
] ~ getPicFlags(env);
overrideEnv = true;
}
else
{
string model = env["MODEL"];
if (model == "32omf") model = "32";
command = [
hostDMD,
"-m"~model,
"-of"~targetBin,
sourceFile
] ~ getPicFlags(env) ~ tool.extraArgs;
}
writefln("Executing: %-(%s %)", command);
stdout.flush();
if (spawnProcess(command, overrideEnv ? env : null).wait)
{
stderr.writefln("failed to build '%s'", 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(TestTools.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);
}