mirror of
https://github.com/dlang/dmd.git
synced 2025-04-26 05:00:16 +03:00
357 lines
10 KiB
D
Executable file
357 lines
10 KiB
D
Executable file
#!/usr/bin/env rdmd
|
|
module unit_test_runner;
|
|
|
|
import std.algorithm : filter, map, joiner, substitute;
|
|
import std.array : array, join;
|
|
import std.conv : to;
|
|
import std.exception : enforce;
|
|
import std.file : dirEntries, exists, SpanMode, mkdirRecurse, write;
|
|
import std.format : format;
|
|
import std.getopt : getopt;
|
|
import std.path : absolutePath, buildPath, dirSeparator, relativePath, stripExtension,
|
|
setExtension;
|
|
import std.process : Config, environment, execute;
|
|
import std.range : empty, chain;
|
|
import std.stdio;
|
|
import std.string : join, outdent;
|
|
|
|
import tools.paths;
|
|
|
|
enum unitTestDir = testPath("unit");
|
|
|
|
string[] testFiles(Range)(Range givenFiles)
|
|
{
|
|
if (!givenFiles.empty)
|
|
return givenFiles.map!(testPath).array;
|
|
|
|
return unitTestDir
|
|
.dirEntries("*.d", SpanMode.depth)
|
|
.map!(e => e.name)
|
|
.array;
|
|
}
|
|
|
|
auto moduleNames(const string[] testFiles)
|
|
{
|
|
return testFiles
|
|
.map!(e => e[unitTestDir.length + 1 .. $])
|
|
.map!stripExtension
|
|
.array
|
|
.map!(e => e.substitute(dirSeparator, "."));
|
|
}
|
|
|
|
void writeRunnerFile(Range)(Range moduleNames, string path, string filter)
|
|
{
|
|
enum codeTemplate = q{
|
|
import core.runtime : Runtime, UnitTestResult;
|
|
import std.meta : AliasSeq;
|
|
|
|
// modules to unit test starts here:
|
|
%s
|
|
|
|
alias modules = AliasSeq!(
|
|
%s
|
|
);
|
|
|
|
enum filter = %s;
|
|
|
|
version(unittest) shared static this()
|
|
{
|
|
Runtime.extendedModuleUnitTester = &unitTestRunner;
|
|
}
|
|
|
|
UnitTestResult unitTestRunner()
|
|
{
|
|
import std.algorithm : any, canFind, each, map;
|
|
import std.array : array;
|
|
import std.conv : text;
|
|
import std.format : format;
|
|
import std.meta : Alias;
|
|
import std.range : chain, empty, enumerate, only, repeat;
|
|
import std.stdio : writeln, writefln, stderr, stdout;
|
|
import std.string : join;
|
|
import std.traits : hasUDA, isCallable;
|
|
|
|
static import support;
|
|
|
|
alias TestCallback = void function();
|
|
|
|
struct Test
|
|
{
|
|
Throwable throwable;
|
|
string[] descriptions;
|
|
|
|
string toString(size_t i)
|
|
{
|
|
const descs = descriptions;
|
|
const index = text(i + 1) ~ ") ";
|
|
|
|
enum fmt = "%%s%%s\n%%s";
|
|
|
|
if (descs.length < 2)
|
|
return format!fmt(index, descriptions.join(""), throwable);
|
|
|
|
auto trailing = descs[1 .. $]
|
|
.map!(e => ' '.repeat(index.length).array ~ e);
|
|
|
|
const description = descriptions[0]
|
|
.only
|
|
.chain(trailing)
|
|
.join("\n");
|
|
|
|
return format!fmt(index, description, throwable);
|
|
}
|
|
|
|
string fileInfo()
|
|
{
|
|
with (throwable)
|
|
return format!"%%s:%%s"(file, line);
|
|
}
|
|
}
|
|
|
|
Test[] failedTests;
|
|
size_t testCount;
|
|
|
|
void printReport()
|
|
{
|
|
if (!failedTests.empty)
|
|
{
|
|
alias formatTest = t => t.value.toString(t.index);
|
|
|
|
const failedTestsMessage = failedTests
|
|
.enumerate
|
|
.map!(formatTest)
|
|
.join("\n\n");
|
|
|
|
stderr.writefln!"Failures:\n\n%%s\n"(failedTestsMessage);
|
|
}
|
|
|
|
auto output = failedTests.empty ? stdout : stderr;
|
|
output.writefln!"%%s tests, %%s failures"(testCount, failedTests.length);
|
|
|
|
if (failedTests.empty)
|
|
return;
|
|
|
|
stderr.writefln!"\nFailed tests:\n%%s"(
|
|
failedTests.map!(t => t.fileInfo).join("\n"));
|
|
}
|
|
|
|
TestCallback[] getTestCallbacks(alias module_, alias uda)()
|
|
{
|
|
enum isMemberAccessible(string memberName) =
|
|
is(typeof(__traits(getMember, module_, memberName)));
|
|
|
|
TestCallback[] callbacks;
|
|
|
|
static foreach(mem ; __traits(allMembers, module_))
|
|
{
|
|
static if (isMemberAccessible!(mem))
|
|
{{
|
|
alias member = __traits(getMember, module_, mem);
|
|
|
|
static if (isCallable!member && hasUDA!(member, uda))
|
|
callbacks ~= &member;
|
|
}}
|
|
}
|
|
|
|
return callbacks;
|
|
}
|
|
|
|
void executeCallbacks(const TestCallback[] callbacks)
|
|
{
|
|
callbacks.each!(c => c());
|
|
}
|
|
|
|
static foreach (module_ ; modules)
|
|
{
|
|
foreach (unitTest ; __traits(getUnitTests, module_))
|
|
{
|
|
enum attributes = [__traits(getAttributes, unitTest)];
|
|
|
|
const beforeEachCallbacks = getTestCallbacks!(module_, support.beforeEach);
|
|
const afterEachCallbacks = getTestCallbacks!(module_, support.afterEach);
|
|
|
|
Test test;
|
|
|
|
try
|
|
{
|
|
static if (!attributes.empty)
|
|
{
|
|
test.descriptions = attributes;
|
|
|
|
if (attributes.any!(a => a.canFind(filter)))
|
|
{
|
|
testCount++;
|
|
executeCallbacks(beforeEachCallbacks);
|
|
unitTest();
|
|
}
|
|
}
|
|
|
|
else static if (filter.length == 0)
|
|
{
|
|
testCount++;
|
|
executeCallbacks(beforeEachCallbacks);
|
|
unitTest();
|
|
}
|
|
}
|
|
|
|
catch (Throwable t)
|
|
{
|
|
test.throwable = t;
|
|
failedTests ~= test;
|
|
}
|
|
|
|
finally
|
|
executeCallbacks(afterEachCallbacks);
|
|
}
|
|
}
|
|
|
|
printReport();
|
|
|
|
UnitTestResult result = {
|
|
runMain: false,
|
|
executed: testCount,
|
|
passed: testCount - failedTests.length
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
version (D_Coverage)
|
|
shared static this()
|
|
{
|
|
import core.runtime;
|
|
|
|
static immutable sourcePath = `%s`;
|
|
|
|
dmd_coverSourcePath(sourcePath);
|
|
dmd_coverDestPath(sourcePath);
|
|
dmd_coverSetMerge(true);
|
|
}
|
|
}.outdent;
|
|
|
|
const imports = moduleNames
|
|
.map!(e => format!"static import %s;"(e))
|
|
.joiner("\n")
|
|
.to!string;
|
|
|
|
const modules = moduleNames
|
|
.map!(e => format!"%s"(e))
|
|
.joiner(",\n")
|
|
.to!string;
|
|
|
|
const content = format!codeTemplate(imports, modules, format!`"%s"`(filter), compilerRootDir);
|
|
write(path, content);
|
|
}
|
|
|
|
/**
|
|
Returns the arguments for the compiler invocation.
|
|
|
|
Params:
|
|
runnerPath = the path of the unit test runner file outputted by `writeRunnerFile`
|
|
outputPath = the path where to place the compiled binary
|
|
testFiles = the test files to compile
|
|
*/
|
|
string[] buildCmdArgs(string runnerPath, string outputPath, const string[] testFiles)
|
|
{
|
|
auto flags = chain([
|
|
"-version=NoBackend",
|
|
"-version=GC",
|
|
"-version=NoMain",
|
|
"-version=MARS",
|
|
"-version=DMDLIB",
|
|
"-g",
|
|
"-unittest",
|
|
"-J" ~ buildOutputPath,
|
|
"-Jsrc/dmd/res",
|
|
"-Isrc",
|
|
"-I" ~ unitTestDir,
|
|
"-i",
|
|
"-main",
|
|
"-of" ~ outputPath,
|
|
"-m" ~ model
|
|
],
|
|
testFiles.map!(f => relativePath(f, compilerRootDir)),
|
|
[ runnerPath ]
|
|
).array;
|
|
|
|
// Generate coverage reports if requested
|
|
if (environment.get("DMD_TEST_COVERAGE", "0") == "1")
|
|
flags ~= "-cov";
|
|
|
|
return flags;
|
|
}
|
|
|
|
/**
|
|
Returns `true` if any of the given files don't exist.
|
|
|
|
Also prints an error message.
|
|
*/
|
|
bool missingTestFiles(Range)(Range givenFiles)
|
|
{
|
|
const nonExistingTestFiles = givenFiles
|
|
.filter!(file => !file.exists)
|
|
.join("\n");
|
|
|
|
if (!nonExistingTestFiles.empty)
|
|
{
|
|
stderr.writefln("The following test files don't exist:\n\n%s",
|
|
nonExistingTestFiles);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int main(string[] args)
|
|
{
|
|
string unitTestFilter;
|
|
getopt(args, "filter|f", &unitTestFilter);
|
|
|
|
auto givenFiles = args[1 .. $].map!absolutePath;
|
|
|
|
if (missingTestFiles(givenFiles))
|
|
return 1;
|
|
|
|
const absResultsDir = resultsDir.absolutePath();
|
|
const runnerPath = absResultsDir.buildPath("runner.d");
|
|
const testFiles = givenFiles.testFiles;
|
|
|
|
mkdirRecurse(resultsDir);
|
|
testFiles
|
|
.moduleNames
|
|
.writeRunnerFile(runnerPath, unitTestFilter);
|
|
|
|
const cmdfilePath = absResultsDir.buildPath("cmdfile");
|
|
const outputPath = absResultsDir.buildPath("runner" ~ exeExtension);
|
|
const flags = buildCmdArgs(runnerPath, outputPath, testFiles);
|
|
write(cmdfilePath, flags.join("\n"));
|
|
|
|
const dmd = execute([ dmdPath, "@" ~ cmdfilePath ], null, Config.none, size_t.max, compilerRootDir);
|
|
if (dmd.status)
|
|
{
|
|
enum msg = "Failed to compile the `unit` test executable! (exit code %d)
|
|
|
|
> %-(%s %)
|
|
%s";
|
|
// Build the string in advance to avoid cluttering
|
|
writeln(format(msg, dmd.status, dmdPath ~ flags, dmd.output));
|
|
return 1;
|
|
}
|
|
|
|
const test = execute(outputPath);
|
|
if (test.status)
|
|
{
|
|
enum msg = "Failed to execute the `unit` test executable! (exit code %d)
|
|
|
|
> %-(%s %)
|
|
%s
|
|
> %s
|
|
%s";
|
|
// Build the string in advance to avoid cluttering
|
|
writeln(format(msg, test.status, dmdPath ~ flags, dmd.output, outputPath, test.output));
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|