mirror of
https://github.com/dlang/dmd.git
synced 2025-04-27 05:30:13 +03:00

when linking through lld-link, add /SAFESEH:NO to linker command line add build on azure that uses lld and mingw import libraries use run.d instead of gmake to run tests d_do_test: remove legacy command line "-map nul.map" update to build with LDC 1.20.0
1321 lines
45 KiB
D
Executable file
1321 lines
45 KiB
D
Executable file
#!/usr/bin/env rdmd
|
|
module d_do_test;
|
|
|
|
import std.algorithm;
|
|
import std.array;
|
|
import std.conv;
|
|
import std.datetime.stopwatch;
|
|
import std.datetime.systime;
|
|
import std.exception;
|
|
import std.file;
|
|
import std.format;
|
|
import std.process;
|
|
import std.random;
|
|
import std.range : chain;
|
|
import std.regex;
|
|
import std.path;
|
|
import std.stdio;
|
|
import std.string;
|
|
import core.sys.posix.sys.wait;
|
|
|
|
const dmdTestDir = __FILE_FULL_PATH__.dirName.dirName;
|
|
|
|
version(Win32)
|
|
{
|
|
extern(C) int putenv(const char*);
|
|
}
|
|
|
|
void usage()
|
|
{
|
|
write("d_do_test <test_file>\n"
|
|
~ "\n"
|
|
~ " Note: this program is normally called through the Makefile, it"
|
|
~ " is not meant to be called directly by the user.\n"
|
|
~ "\n"
|
|
~ " example: d_do_test runnable/pi.d\n"
|
|
~ "\n"
|
|
~ " relevant environment variables:\n"
|
|
~ " ARGS: set to execute all combinations of\n"
|
|
~ " REQUIRED_ARGS: arguments always passed to the compiler\n"
|
|
~ " DMD: compiler to use, ex: ../src/dmd (required)\n"
|
|
~ " CC: C++ compiler to use, ex: dmc, g++\n"
|
|
~ " OS: windows, linux, freebsd, osx, netbsd, dragonflybsd\n"
|
|
~ " RESULTS_DIR: base directory for test results\n"
|
|
~ " MODEL: 32 or 64 (required)\n"
|
|
~ " AUTO_UPDATE: set to 1 to auto-update mismatching test output\n"
|
|
~ " PRINT_RUNTIME: set to 1 to print test runtime\n"
|
|
~ "\n"
|
|
~ " windows vs non-windows portability env vars:\n"
|
|
~ " DSEP: \\\\ or /\n"
|
|
~ " SEP: \\ or / (required)\n"
|
|
~ " OBJ: .obj or .o (required)\n"
|
|
~ " EXE: .exe or <null> (required)\n");
|
|
}
|
|
|
|
enum TestMode
|
|
{
|
|
COMPILE,
|
|
FAIL_COMPILE,
|
|
RUN,
|
|
DSHELL,
|
|
}
|
|
|
|
struct TestArgs
|
|
{
|
|
TestMode mode;
|
|
|
|
bool compileSeparately;
|
|
bool link;
|
|
string executeArgs;
|
|
string dflags;
|
|
string cxxflags;
|
|
string[] sources;
|
|
string[] compiledImports;
|
|
string[] cppSources;
|
|
string[] objcSources;
|
|
string permuteArgs;
|
|
string[] argSets;
|
|
string compileOutput;
|
|
string compileOutputFile; /// file containing the expected output
|
|
string gdbScript;
|
|
string gdbMatch;
|
|
string postScript;
|
|
string requiredArgs;
|
|
string requiredArgsForLink;
|
|
string disabledReason; // if empty, the test is not disabled
|
|
|
|
bool isDisabled() const { return disabledReason.length != 0; }
|
|
}
|
|
|
|
struct EnvData
|
|
{
|
|
string all_args;
|
|
string dmd;
|
|
string results_dir;
|
|
string sep;
|
|
string dsep;
|
|
string obj;
|
|
string exe;
|
|
string os;
|
|
string compiler;
|
|
string ccompiler;
|
|
string model;
|
|
string required_args;
|
|
bool dobjc;
|
|
bool coverage_build;
|
|
bool autoUpdate;
|
|
bool usingMicrosoftCompiler;
|
|
}
|
|
|
|
bool findTestParameter(const ref EnvData envData, string file, string token, ref string result, string multiLineDelimiter = " ")
|
|
{
|
|
auto tokenStart = std.string.indexOf(file, token);
|
|
if (tokenStart == -1) return false;
|
|
|
|
file = file[tokenStart + token.length .. $];
|
|
|
|
auto lineEndR = std.string.indexOf(file, "\r");
|
|
auto lineEndN = std.string.indexOf(file, "\n");
|
|
auto lineEnd = lineEndR == -1 ?
|
|
(lineEndN == -1 ? file.length : lineEndN) :
|
|
(lineEndN == -1 ? lineEndR : min(lineEndR, lineEndN));
|
|
|
|
//writeln("found ", token, " in line: ", file.length, ", ", tokenStart, ", ", tokenStart+lineEnd);
|
|
//writeln("found ", token, " in line: '", file[tokenStart .. tokenStart+lineEnd], "'");
|
|
|
|
result = file[0 .. lineEnd];
|
|
const commentStart = std.string.indexOf(result, "//");
|
|
if (commentStart != -1)
|
|
result = result[0 .. commentStart];
|
|
result = strip(result);
|
|
|
|
// filter by OS specific setting (os1 os2 ...)
|
|
if (result.startsWith("("))
|
|
{
|
|
auto close = std.string.indexOf(result, ")");
|
|
if (close >= 0)
|
|
{
|
|
string[] oss = split(result[1 .. close], " ");
|
|
if (oss.canFind(envData.os))
|
|
result = result[close + 1 .. $];
|
|
else
|
|
result = null;
|
|
}
|
|
}
|
|
// skips the :, if present
|
|
if (result.startsWith(":"))
|
|
result = strip(result[1 .. $]);
|
|
|
|
//writeln("arg: '", result, "'");
|
|
|
|
string result2;
|
|
if (findTestParameter(envData, file[lineEnd .. $], token, result2, multiLineDelimiter))
|
|
{
|
|
if (result2.length > 0)
|
|
{
|
|
if (result.length == 0)
|
|
result = result2;
|
|
else
|
|
result ~= multiLineDelimiter ~ result2;
|
|
}
|
|
}
|
|
|
|
// fix-up separators
|
|
result = result.unifyDirSep(envData.sep);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool findOutputParameter(string file, string token, out string result, string sep)
|
|
{
|
|
bool found = false;
|
|
|
|
while (true)
|
|
{
|
|
const istart = std.string.indexOf(file, token);
|
|
if (istart == -1)
|
|
break;
|
|
found = true;
|
|
|
|
file = file[istart + token.length .. $];
|
|
|
|
enum embed_sep = "---";
|
|
auto n = std.string.indexOf(file, embed_sep);
|
|
|
|
enforce(n != -1, "invalid "~token~" format");
|
|
n += embed_sep.length;
|
|
while (file[n] == '-') ++n;
|
|
if (file[n] == '\r') ++n;
|
|
if (file[n] == '\n') ++n;
|
|
|
|
file = file[n .. $];
|
|
auto iend = std.string.indexOf(file, embed_sep);
|
|
enforce(iend != -1, "invalid TEST_OUTPUT format");
|
|
|
|
result ~= file[0 .. iend];
|
|
|
|
while (file[iend] == '-') ++iend;
|
|
file = file[iend .. $];
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
result = std.string.strip(result);
|
|
result = result.unifyNewLine().unifyDirSep(sep);
|
|
result = result ? result : ""; // keep non-null
|
|
}
|
|
return found;
|
|
}
|
|
|
|
void replaceResultsDir(ref string arguments, const ref EnvData envData)
|
|
{
|
|
// Bash would expand this automatically on Posix, but we need to manually
|
|
// perform the replacement for Windows compatibility.
|
|
arguments = replace(arguments, "${RESULTS_DIR}", envData.results_dir);
|
|
}
|
|
|
|
string getDisabledReason(string[] disabledPlatforms, const ref EnvData envData)
|
|
{
|
|
if (disabledPlatforms.length == 0)
|
|
return null;
|
|
|
|
const target = ((envData.os == "windows") ? "win" : envData.os) ~ envData.model;
|
|
|
|
// allow partial matching, e.g. `win` to disable both win32 and win64
|
|
const i = disabledPlatforms.countUntil!(p => target.canFind(p));
|
|
if (i != -1)
|
|
return "on " ~ disabledPlatforms[i];
|
|
|
|
return null;
|
|
}
|
|
|
|
bool gatherTestParameters(ref TestArgs testArgs, string input_dir, string input_file, const ref EnvData envData)
|
|
{
|
|
string file = cast(string)std.file.read(input_file);
|
|
|
|
findTestParameter(envData, file, "DFLAGS", testArgs.dflags);
|
|
|
|
findTestParameter(envData, file, "REQUIRED_ARGS", testArgs.requiredArgs);
|
|
if (envData.required_args.length)
|
|
{
|
|
if (testArgs.requiredArgs.length)
|
|
testArgs.requiredArgs ~= " " ~ envData.required_args;
|
|
else
|
|
testArgs.requiredArgs = envData.required_args;
|
|
}
|
|
replaceResultsDir(testArgs.requiredArgs, envData);
|
|
|
|
if (! findTestParameter(envData, file, "PERMUTE_ARGS", testArgs.permuteArgs))
|
|
{
|
|
if (testArgs.mode == TestMode.RUN)
|
|
testArgs.permuteArgs = envData.all_args;
|
|
|
|
string unittestJunk;
|
|
if(!findTestParameter(envData, file, "unittest", unittestJunk))
|
|
testArgs.permuteArgs = replace(testArgs.permuteArgs, "-unittest", "");
|
|
}
|
|
replaceResultsDir(testArgs.permuteArgs, envData);
|
|
|
|
// remove permute args enforced as required anyway
|
|
if (testArgs.requiredArgs.length && testArgs.permuteArgs.length)
|
|
{
|
|
const required = split(testArgs.requiredArgs);
|
|
const newPermuteArgs = split(testArgs.permuteArgs)
|
|
.filter!(a => !required.canFind(a))
|
|
.join(" ");
|
|
testArgs.permuteArgs = newPermuteArgs;
|
|
}
|
|
|
|
{
|
|
string argSetsStr;
|
|
findTestParameter(envData, file, "ARG_SETS", argSetsStr, ";");
|
|
foreach(s; split(argSetsStr, ";"))
|
|
{
|
|
replaceResultsDir(s, envData);
|
|
testArgs.argSets ~= s;
|
|
}
|
|
}
|
|
|
|
// win(32|64) doesn't support pic
|
|
if (envData.os == "windows")
|
|
{
|
|
auto index = std.string.indexOf(testArgs.permuteArgs, "-fPIC");
|
|
if (index != -1)
|
|
testArgs.permuteArgs = testArgs.permuteArgs[0 .. index] ~ testArgs.permuteArgs[index+5 .. $];
|
|
}
|
|
|
|
// clean up extra spaces
|
|
testArgs.permuteArgs = strip(replace(testArgs.permuteArgs, " ", " "));
|
|
|
|
findTestParameter(envData, file, "EXECUTE_ARGS", testArgs.executeArgs);
|
|
replaceResultsDir(testArgs.executeArgs, envData);
|
|
|
|
string extraSourcesStr;
|
|
findTestParameter(envData, file, "EXTRA_SOURCES", extraSourcesStr);
|
|
testArgs.sources = [input_file];
|
|
// prepend input_dir to each extra source file
|
|
foreach(s; split(extraSourcesStr))
|
|
testArgs.sources ~= input_dir ~ "/" ~ s;
|
|
|
|
{
|
|
string compiledImports;
|
|
findTestParameter(envData, file, "COMPILED_IMPORTS", compiledImports);
|
|
foreach(s; split(compiledImports))
|
|
testArgs.compiledImports ~= input_dir ~ "/" ~ s;
|
|
}
|
|
|
|
findTestParameter(envData, file, "CXXFLAGS", testArgs.cxxflags);
|
|
string extraCppSourcesStr;
|
|
findTestParameter(envData, file, "EXTRA_CPP_SOURCES", extraCppSourcesStr);
|
|
testArgs.cppSources = [];
|
|
// prepend input_dir to each extra source file
|
|
foreach(s; split(extraCppSourcesStr))
|
|
testArgs.cppSources ~= s;
|
|
|
|
string extraObjcSourcesStr;
|
|
auto objc = findTestParameter(envData, file, "EXTRA_OBJC_SOURCES", extraObjcSourcesStr);
|
|
|
|
if (objc && !envData.dobjc)
|
|
return false;
|
|
|
|
testArgs.objcSources = [];
|
|
// prepend input_dir to each extra source file
|
|
foreach(s; split(extraObjcSourcesStr))
|
|
testArgs.objcSources ~= s;
|
|
|
|
// swap / with $SEP
|
|
if (envData.sep && envData.sep != "/")
|
|
foreach (ref s; testArgs.sources)
|
|
s = replace(s, "/", to!string(envData.sep));
|
|
//writeln ("sources: ", testArgs.sources);
|
|
|
|
{
|
|
string throwAway;
|
|
testArgs.link = findTestParameter(envData, file, "LINK", throwAway);
|
|
}
|
|
|
|
// COMPILE_SEPARATELY can take optional compiler switches when link .o files
|
|
testArgs.compileSeparately = findTestParameter(envData, file, "COMPILE_SEPARATELY", testArgs.requiredArgsForLink);
|
|
|
|
string disabledPlatformsStr;
|
|
findTestParameter(envData, file, "DISABLED", disabledPlatformsStr);
|
|
|
|
version (DragonFlyBSD)
|
|
{
|
|
// DragonFlyBSD is x86_64 only, instead of adding DISABLED to a lot of tests, just exclude them from running
|
|
if (testArgs.requiredArgs.canFind("-m32"))
|
|
testArgs.disabledReason = "on DragonFlyBSD (no -m32)";
|
|
}
|
|
|
|
version (ARM) enum supportsM64 = false;
|
|
else version (MIPS32) enum supportsM64 = false;
|
|
else version (PPC) enum supportsM64 = false;
|
|
else enum supportsM64 = true;
|
|
|
|
static if (!supportsM64)
|
|
{
|
|
if (testArgs.requiredArgs.canFind("-m64"))
|
|
testArgs.disabledReason = "because target doesn't support -m64";
|
|
}
|
|
|
|
if (!testArgs.isDisabled)
|
|
testArgs.disabledReason = getDisabledReason(split(disabledPlatformsStr), envData);
|
|
|
|
findTestParameter(envData, file, "TEST_OUTPUT_FILE", testArgs.compileOutputFile);
|
|
|
|
// Only check for TEST_OUTPUT is no file was given because it would
|
|
// partially match TEST_OUTPUT_FILE
|
|
if (testArgs.compileOutputFile)
|
|
{
|
|
// Don't require tests to specify the test directory
|
|
testArgs.compileOutputFile = input_dir.buildPath(testArgs.compileOutputFile);
|
|
testArgs.compileOutput = readText(testArgs.compileOutputFile);
|
|
}
|
|
else
|
|
findOutputParameter(file, "TEST_OUTPUT", testArgs.compileOutput, envData.sep);
|
|
|
|
findOutputParameter(file, "GDB_SCRIPT", testArgs.gdbScript, envData.sep);
|
|
findTestParameter(envData, file, "GDB_MATCH", testArgs.gdbMatch);
|
|
|
|
if (findTestParameter(envData, file, "POST_SCRIPT", testArgs.postScript))
|
|
testArgs.postScript = replace(testArgs.postScript, "/", to!string(envData.sep));
|
|
|
|
return true;
|
|
}
|
|
|
|
string[] combinations(string argstr)
|
|
{
|
|
string[] results;
|
|
string[] args = split(argstr);
|
|
long combinations = 1 << args.length;
|
|
for (size_t i = 0; i < combinations; i++)
|
|
{
|
|
string r;
|
|
bool printed = false;
|
|
|
|
for (size_t j = 0; j < args.length; j++)
|
|
{
|
|
if (i & 1 << j)
|
|
{
|
|
if (printed)
|
|
r ~= " ";
|
|
r ~= args[j];
|
|
printed = true;
|
|
}
|
|
}
|
|
|
|
results ~= r;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
string genTempFilename(string result_path)
|
|
{
|
|
auto a = appender!string();
|
|
a.put(result_path);
|
|
foreach (ref e; 0 .. 8)
|
|
{
|
|
formattedWrite(a, "%x", rndGen.front);
|
|
rndGen.popFront();
|
|
}
|
|
|
|
return a.data;
|
|
}
|
|
|
|
int system(string command)
|
|
{
|
|
static import core.stdc.stdlib;
|
|
if (!command) return core.stdc.stdlib.system(null);
|
|
const commandz = toStringz(command);
|
|
auto status = core.stdc.stdlib.system(commandz);
|
|
if (status == -1) return status;
|
|
version (Windows) status <<= 8;
|
|
return status;
|
|
}
|
|
|
|
version(Windows)
|
|
{
|
|
extern (D) bool WIFEXITED( int status ) { return ( status & 0x7F ) == 0; }
|
|
extern (D) int WEXITSTATUS( int status ) { return ( status & 0xFF00 ) >> 8; }
|
|
extern (D) int WTERMSIG( int status ) { return status & 0x7F; }
|
|
extern (D) bool WIFSIGNALED( int status )
|
|
{
|
|
return ( cast(byte) ( ( status & 0x7F ) + 1 ) >> 1 ) > 0;
|
|
}
|
|
}
|
|
|
|
void removeIfExists(in char[] filename)
|
|
{
|
|
if (std.file.exists(filename))
|
|
std.file.remove(filename);
|
|
}
|
|
|
|
string execute(ref File f, string command, bool expectpass, string result_path)
|
|
{
|
|
auto filename = genTempFilename(result_path);
|
|
scope(exit) removeIfExists(filename);
|
|
|
|
auto rc = system(command ~ " > " ~ filename ~ " 2>&1");
|
|
|
|
string output = readText(filename);
|
|
f.writeln(command);
|
|
f.write(output);
|
|
|
|
if (WIFSIGNALED(rc))
|
|
{
|
|
auto value = WTERMSIG(rc);
|
|
enforce(0 == value, "caught signal: " ~ to!string(value));
|
|
}
|
|
else if (WIFEXITED(rc))
|
|
{
|
|
auto value = WEXITSTATUS(rc);
|
|
if (expectpass)
|
|
enforce(0 == value, "expected rc == 0, exited with rc == " ~ to!string(value));
|
|
else
|
|
enforce(1 == value, "expected rc == 1, but exited with rc == " ~ to!string(value));
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/// add quotes around the whole string if it contains spaces that are not in quotes
|
|
string quoteSpaces(string str)
|
|
{
|
|
if (str.indexOf(' ') < 0)
|
|
return str;
|
|
bool inquote = false;
|
|
foreach(dchar c; str)
|
|
if (c == '"')
|
|
inquote = !inquote;
|
|
else if (c == ' ' && !inquote)
|
|
return "\"" ~ str ~ "\"";
|
|
return str;
|
|
}
|
|
|
|
string unifyNewLine(string str)
|
|
{
|
|
// On Windows, Outbuffer.writenl() puts `\r\n` into the buffer,
|
|
// then fprintf() adds another `\r` when formatting the message.
|
|
// This is why there's a match for `\r\r\n` in this regex.
|
|
static re = regex(`\r\r\n|\r\n|\r|\n`, "g");
|
|
return std.regex.replace(str, re, "\n");
|
|
}
|
|
|
|
string unifyDirSep(string str, string sep)
|
|
{
|
|
static re = regex(`(?<=[-\w{}][-\w{}]*)/(?=[-\w][-\w/]*\.(di?|mixin)\b)`, "g");
|
|
return std.regex.replace(str, re, sep);
|
|
}
|
|
unittest
|
|
{
|
|
assert(`fail_compilation/test.d(1) Error: dummy error message for 'test'`.unifyDirSep(`\`)
|
|
== `fail_compilation\test.d(1) Error: dummy error message for 'test'`);
|
|
assert(`fail_compilation/test.d(1) Error: at fail_compilation/test.d(2)`.unifyDirSep(`\`)
|
|
== `fail_compilation\test.d(1) Error: at fail_compilation\test.d(2)`);
|
|
|
|
assert(`fail_compilation/test.d(1) Error: at fail_compilation/imports/test.d(2)`.unifyDirSep(`\`)
|
|
== `fail_compilation\test.d(1) Error: at fail_compilation\imports\test.d(2)`);
|
|
assert(`fail_compilation/diag.d(2): Error: fail_compilation/imports/fail.d must be imported`.unifyDirSep(`\`)
|
|
== `fail_compilation\diag.d(2): Error: fail_compilation\imports\fail.d must be imported`);
|
|
|
|
assert(`{{RESULTS_DIR}}/fail_compilation/mixin_test.mixin(7): Error:`.unifyDirSep(`\`)
|
|
== `{{RESULTS_DIR}}\fail_compilation\mixin_test.mixin(7): Error:`);
|
|
}
|
|
|
|
bool collectExtraSources (in string input_dir, in string output_dir, in string[] extraSources,
|
|
ref string[] sources, in EnvData envData, in string compiler,
|
|
const(char)[] cxxflags)
|
|
{
|
|
foreach (cur; extraSources)
|
|
{
|
|
auto curSrc = input_dir ~ envData.sep ~"extra-files" ~ envData.sep ~ cur;
|
|
auto curObj = output_dir ~ envData.sep ~ cur ~ envData.obj;
|
|
string command = quoteSpaces(compiler);
|
|
if (envData.compiler == "dmd")
|
|
{
|
|
if (envData.usingMicrosoftCompiler)
|
|
{
|
|
command ~= ` /c /nologo `~curSrc~` /Fo`~curObj;
|
|
}
|
|
else if (envData.os == "windows" && envData.model == "32")
|
|
{
|
|
command ~= " -c "~curSrc~" -o"~curObj;
|
|
}
|
|
else
|
|
{
|
|
command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
command ~= " -m"~envData.model~" -c "~curSrc~" -o "~curObj;
|
|
}
|
|
if (cxxflags)
|
|
command ~= " " ~ cxxflags;
|
|
|
|
auto rc = system(command);
|
|
if(rc)
|
|
{
|
|
writeln("failed to execute '"~command~"'");
|
|
return false;
|
|
}
|
|
sources ~= curObj;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/++
|
|
Compares the output string to the reference string by character
|
|
except parts marked with one of the following special sequences:
|
|
|
|
$n$ = numbers (e.g. compiler generated unique identifiers)
|
|
$p:<path>$ = real paths ending with <path>
|
|
$?:<choices>$ = environment dependent content supplied as a list
|
|
choices (either <condition>=<content> or <default>),
|
|
separated by a '|'. Currently supported conditions are
|
|
OS and model as supplied from the environment
|
|
|
|
Params:
|
|
output = the real output
|
|
refoutput = the expected output
|
|
envData = test environment
|
|
|
|
Returns: whether output matches the expected refoutput
|
|
++/
|
|
bool compareOutput(string output, string refoutput, const ref EnvData envData)
|
|
{
|
|
// If no output is expected, only check that nothing was captured.
|
|
if (refoutput.length == 0)
|
|
return (output.length == 0) ? true : false;
|
|
|
|
for ( ; ; )
|
|
{
|
|
auto special = refoutput.find("$n$", "$p:", "$?:").rename!("remainder", "id");
|
|
|
|
// Simple equality check if no special tokens remain
|
|
if (special.id == 0)
|
|
return refoutput == output;
|
|
|
|
const expected = refoutput[0 .. $ - special.remainder.length];
|
|
|
|
// Check until the special token
|
|
if (!output.skipOver(expected))
|
|
return false;
|
|
|
|
// Discard the special token and progress output appropriately
|
|
refoutput = special.remainder[3 .. $];
|
|
|
|
if (special.id == 1) // $n$
|
|
{
|
|
import std.ascii : isDigit;
|
|
output.skipOver!isDigit();
|
|
continue;
|
|
}
|
|
|
|
// $<identifier>:<special content>$
|
|
/// ( special content, "$", remaining expected output )
|
|
auto refparts = refoutput.findSplit("$");
|
|
enforce(refparts, "Malformed special sequence!");
|
|
refoutput = refparts[2];
|
|
|
|
if (special.id == 2) // $p:<some path>$
|
|
{
|
|
// special content is the expected path tail
|
|
// Substitute / with the appropriate directory separator
|
|
auto pathEnd = refparts[0].replace("/", envData.sep);
|
|
|
|
/// ( whole path, remaining output )
|
|
auto parts = output.findSplitAfter(pathEnd);
|
|
|
|
if (!parts || !exists(parts[0]))
|
|
return false;
|
|
|
|
output = parts[1];
|
|
continue;
|
|
}
|
|
|
|
// $?:<predicate>=<content>(;<predicate>=<content>)*(;<default>)?$
|
|
string toSkip = null;
|
|
|
|
foreach (const chunk; refparts[0].splitter('|'))
|
|
{
|
|
// ( <predicate> , "=", <content> )
|
|
const conditional = chunk.findSplit("=");
|
|
|
|
if (!conditional) // <default>
|
|
{
|
|
toSkip = chunk;
|
|
break;
|
|
}
|
|
// Match against OS or model (accepts "32mscoff" as "32")
|
|
else if (conditional[0].among(envData.os, envData.model, envData.model[0 .. min(2, $)]))
|
|
{
|
|
toSkip = conditional[2];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (toSkip !is null && !output.skipOver(toSkip))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
EnvData ed;
|
|
version (Windows)
|
|
ed.sep = `\`;
|
|
else
|
|
ed.sep = `/`;
|
|
|
|
assert( compareOutput(`Grass is green`, `Grass is green`, ed));
|
|
assert(!compareOutput(`Grass is green`, `Grass was green`, ed));
|
|
|
|
assert( compareOutput(`Bob took 12 apples`, `Bob took $n$ apples`, ed));
|
|
assert(!compareOutput(`Bob took abc apples`, `Bob took $n$ apples`, ed));
|
|
assert(!compareOutput(`Bob took 12 berries`, `Bob took $n$ apples`, ed));
|
|
|
|
assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:d_do_test.d$ is important`, ed));
|
|
assert( compareOutput(`HINT: ` ~ __FILE_FULL_PATH__ ~ ` is important`, `HINT: $p:test/tools/d_do_test.d$ is important`, ed));
|
|
|
|
ed.sep = "/";
|
|
assert(!compareOutput(`See /path/to/druntime/import/object.d`, `See $p:druntime/import/object.d$`, ed));
|
|
|
|
assertThrown(compareOutput(`Path /a/b/c.d!`, `Path $p:c.d!`, ed)); // Missing closing $
|
|
|
|
const fmt = "This $?:windows=A|posix=B|C$ uses $?:64=1|32=2|3$ bytes";
|
|
|
|
assert( compareOutput("This C uses 3 bytes", fmt, ed));
|
|
|
|
ed.os = "posix";
|
|
ed.model = "64";
|
|
assert( compareOutput("This B uses 1 bytes", fmt, ed));
|
|
assert(!compareOutput("This C uses 3 bytes", fmt, ed));
|
|
|
|
const emptyFmt = "On <$?:windows=abc|$> use <$?:posix=$>!";
|
|
assert(compareOutput("On <> use <>!", emptyFmt, ed));
|
|
|
|
ed.model = "32mscoff";
|
|
assert(compareOutput("size_t is uint!", "size_t is $?:32=uint|64=ulong$!", ed));
|
|
}
|
|
|
|
/++
|
|
Creates a diff of the expected and actual test output.
|
|
|
|
Params:
|
|
testData = the test configuration
|
|
output = the actual output
|
|
name = the test files name
|
|
|
|
Returns: the comparison created by the `diff` utility
|
|
++/
|
|
string generateDiff(ref const TestArgs testData, const string output, const string name)
|
|
{
|
|
string actualFile = tempDir.buildPath("expected_" ~ name);
|
|
File(actualFile, "w").writeln(output); // Append \n
|
|
scope (exit) remove(actualFile);
|
|
|
|
const needTmp = !testData.compileOutputFile;
|
|
string expectedFile;
|
|
if (needTmp) // Create a temporary file
|
|
{
|
|
expectedFile = tempDir.buildPath("actual_" ~ name);
|
|
File(expectedFile, "w").writeln(testData.compileOutput); // Append \n
|
|
}
|
|
else // Reuse TEST_OUTPUT_FILE
|
|
expectedFile = testData.compileOutputFile;
|
|
|
|
// Remove temporary file
|
|
scope (exit) if (needTmp)
|
|
remove(expectedFile);
|
|
|
|
const cmd = ["diff", "-pu", "--strip-trailing-cr", expectedFile, actualFile];
|
|
try
|
|
{
|
|
string diff = std.process.execute(cmd).output;
|
|
// Skip diff's status lines listing the diffed files and line count
|
|
foreach (_; 0..3)
|
|
diff = diff.findSplitAfter("\n")[1];
|
|
return diff;
|
|
}
|
|
catch (Exception e)
|
|
return format(`%-(%s, %) failed: %s`, cmd, e.msg);
|
|
}
|
|
|
|
string envGetRequired(in char[] name)
|
|
{
|
|
auto value = environment.get(name);
|
|
if(value is null)
|
|
{
|
|
writefln("Error: missing environment variable '%s', was this called this through the Makefile?",
|
|
name);
|
|
throw new SilentQuit();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
class SilentQuit : Exception { this() { super(null); } }
|
|
|
|
class CompareException : Exception
|
|
{
|
|
string expected;
|
|
string actual;
|
|
|
|
this(string expected, string actual, string diff) {
|
|
string msg = "\nexpected:\n----\n" ~ expected ~
|
|
"\n----\nactual:\n----\n" ~ actual ~
|
|
"\n----\ndiff:\n----\n" ~ diff ~ "----\n";
|
|
super(msg);
|
|
this.expected = expected;
|
|
this.actual = actual;
|
|
}
|
|
}
|
|
|
|
version(unittest) void main(){} else
|
|
int main(string[] args)
|
|
{
|
|
try { return tryMain(args); }
|
|
catch(SilentQuit) { return 1; }
|
|
}
|
|
|
|
int tryMain(string[] args)
|
|
{
|
|
if (args.length != 2)
|
|
{
|
|
usage();
|
|
return 1;
|
|
}
|
|
|
|
const test_file = args[1];
|
|
string input_dir = test_file.dirName();
|
|
|
|
TestArgs testArgs;
|
|
switch (input_dir)
|
|
{
|
|
case "compilable": testArgs.mode = TestMode.COMPILE; break;
|
|
case "fail_compilation": testArgs.mode = TestMode.FAIL_COMPILE; break;
|
|
case "runnable": testArgs.mode = TestMode.RUN; break;
|
|
case "dshell": testArgs.mode = TestMode.DSHELL; break;
|
|
default:
|
|
writefln("Error: invalid test directory '%s', expected 'compilable', 'fail_compilation', 'runnable' or 'dshell'", input_dir);
|
|
return 1;
|
|
}
|
|
|
|
string test_base_name = test_file.baseName();
|
|
string test_name = test_base_name.stripExtension();
|
|
|
|
EnvData envData;
|
|
envData.all_args = environment.get("ARGS");
|
|
envData.results_dir = envGetRequired("RESULTS_DIR");
|
|
envData.sep = envGetRequired ("SEP");
|
|
envData.dsep = environment.get("DSEP");
|
|
envData.obj = envGetRequired ("OBJ");
|
|
envData.exe = envGetRequired ("EXE");
|
|
envData.os = environment.get("OS");
|
|
envData.dmd = replace(envGetRequired("DMD"), "/", envData.sep);
|
|
envData.compiler = "dmd"; //should be replaced for other compilers
|
|
envData.ccompiler = environment.get("CC");
|
|
envData.model = envGetRequired("MODEL");
|
|
envData.required_args = environment.get("REQUIRED_ARGS");
|
|
envData.dobjc = environment.get("D_OBJC") == "1";
|
|
envData.coverage_build = environment.get("DMD_TEST_COVERAGE") == "1";
|
|
envData.autoUpdate = environment.get("AUTO_UPDATE", "") == "1";
|
|
|
|
string result_path = envData.results_dir ~ envData.sep;
|
|
string input_file = input_dir ~ envData.sep ~ test_base_name;
|
|
string output_dir = result_path ~ input_dir;
|
|
string output_file = result_path ~ input_file ~ ".out";
|
|
string test_app_dmd_base = output_dir ~ envData.sep ~ test_name ~ "_";
|
|
|
|
if (test_base_name.extension() == ".sh")
|
|
{
|
|
string file = cast(string) std.file.read(input_file);
|
|
string disabledPlatforms;
|
|
if (findTestParameter(envData, file, "DISABLED", disabledPlatforms))
|
|
{
|
|
const reason = getDisabledReason(split(disabledPlatforms), envData);
|
|
if (reason.length != 0)
|
|
{
|
|
writefln(" ... %-30s [DISABLED %s]", input_file, reason);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return runBashTest(input_dir, test_name);
|
|
}
|
|
|
|
if (testArgs.mode == TestMode.DSHELL)
|
|
return runDShellTest(input_dir, test_name, envData, output_dir, output_file);
|
|
|
|
// envData.sep is required as the results_dir path can be `generated`
|
|
const absoluteResultDirPath = envData.results_dir.absolutePath ~ envData.sep;
|
|
const resultsDirReplacement = "{{RESULTS_DIR}}" ~ envData.sep;
|
|
|
|
// running & linking costs time - for coverage builds we can save this
|
|
if (envData.coverage_build && testArgs.mode == TestMode.RUN)
|
|
testArgs.mode = TestMode.COMPILE;
|
|
|
|
if (envData.ccompiler.empty)
|
|
{
|
|
if (envData.os != "windows")
|
|
envData.ccompiler = "c++";
|
|
else if (envData.model == "32")
|
|
envData.ccompiler = "dmc";
|
|
else if (envData.model == "64")
|
|
envData.ccompiler = `C:\"Program Files (x86)"\"Microsoft Visual Studio 10.0"\VC\bin\amd64\cl.exe`;
|
|
else
|
|
assert(0, "unknown $OS$MODEL combination: " ~ envData.os ~ envData.model);
|
|
}
|
|
|
|
envData.usingMicrosoftCompiler = envData.ccompiler.toLower.endsWith("cl.exe");
|
|
|
|
const printRuntime = environment.get("PRINT_RUNTIME", "") == "1";
|
|
auto stopWatch = StopWatch(AutoStart.no);
|
|
if (printRuntime)
|
|
stopWatch.start();
|
|
|
|
if (!gatherTestParameters(testArgs, input_dir, input_file, envData))
|
|
return 0;
|
|
|
|
// Clear the DFLAGS environment variable if it was specified in the test file
|
|
if (testArgs.dflags !is null)
|
|
{
|
|
if (testArgs.dflags != "")
|
|
throw new Exception("The DFLAGS test argument must be empty: It is '" ~ testArgs.dflags ~ "'");
|
|
|
|
// `environment["DFLAGS"] = "";` doesn't seem to work on Win32 (might be a bug
|
|
// in std.process). So, resorting to `putenv` in snn.lib
|
|
version(Win32)
|
|
{
|
|
putenv("DFLAGS=");
|
|
}
|
|
else
|
|
{
|
|
environment["DFLAGS"] = "";
|
|
}
|
|
}
|
|
|
|
//prepare cpp extra sources
|
|
if (!testArgs.isDisabled && testArgs.cppSources.length)
|
|
{
|
|
switch (envData.compiler)
|
|
{
|
|
case "dmd":
|
|
case "ldc":
|
|
if(envData.os != "windows")
|
|
testArgs.requiredArgs ~= " -L-lstdc++ -L--no-demangle";
|
|
break;
|
|
case "gdc":
|
|
testArgs.requiredArgs ~= "-Xlinker -lstdc++ -Xlinker --no-demangle";
|
|
break;
|
|
default:
|
|
writeln("unknown compiler: "~envData.compiler);
|
|
return 1;
|
|
}
|
|
if (!collectExtraSources(input_dir, output_dir, testArgs.cppSources, testArgs.sources, envData, envData.ccompiler, testArgs.cxxflags))
|
|
return 1;
|
|
}
|
|
//prepare objc extra sources
|
|
if (!testArgs.isDisabled && !collectExtraSources(input_dir, output_dir, testArgs.objcSources, testArgs.sources, envData, "clang", null))
|
|
return 1;
|
|
|
|
writef(" ... %-30s %s%s(%s)",
|
|
input_file,
|
|
testArgs.requiredArgs,
|
|
(!testArgs.requiredArgs.empty ? " " : ""),
|
|
testArgs.permuteArgs);
|
|
|
|
if (testArgs.isDisabled)
|
|
writef("!!! [DISABLED %s]", testArgs.disabledReason);
|
|
|
|
removeIfExists(output_file);
|
|
|
|
auto f = File(output_file, "a");
|
|
|
|
enum Result { continue_, return0, return1 }
|
|
|
|
// Runs the test with a specific combination of arguments
|
|
Result testCombination(bool autoCompileImports, string argSet, size_t permuteIndex, string permutedArgs)
|
|
{
|
|
string test_app_dmd = test_app_dmd_base ~ to!string(permuteIndex) ~ envData.exe;
|
|
string command; // copy of the last executed command so that it can be re-invoked on failures
|
|
try
|
|
{
|
|
string[] toCleanup;
|
|
|
|
auto thisRunName = genTempFilename(result_path);
|
|
auto fThisRun = File(thisRunName, "w");
|
|
scope(exit)
|
|
{
|
|
fThisRun.close();
|
|
f.write(readText(thisRunName));
|
|
f.writeln();
|
|
removeIfExists(thisRunName);
|
|
}
|
|
|
|
// can override -verrors by using REQUIRED_ARGS
|
|
auto reqArgs =
|
|
(testArgs.mode == TestMode.FAIL_COMPILE ? "-verrors=0 " : null) ~
|
|
testArgs.requiredArgs;
|
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=10664: exceptions don't work reliably with COMDAT folding
|
|
// it also slows down some tests drastically, e.g. runnable/test17338.d
|
|
if (envData.usingMicrosoftCompiler)
|
|
reqArgs ~= " -L/OPT:NOICF";
|
|
|
|
string compile_output;
|
|
if (!testArgs.compileSeparately)
|
|
{
|
|
string objfile = output_dir ~ envData.sep ~ test_name ~ "_" ~ to!string(permuteIndex) ~ envData.obj;
|
|
toCleanup ~= objfile;
|
|
|
|
command = format("%s -conf= -m%s -I%s %s %s -od%s -of%s %s %s%s %s", envData.dmd, envData.model, input_dir,
|
|
reqArgs, permutedArgs, output_dir,
|
|
(testArgs.mode == TestMode.RUN || testArgs.link ? test_app_dmd : objfile),
|
|
argSet,
|
|
(testArgs.mode == TestMode.RUN || testArgs.link ? "" : "-c "),
|
|
join(testArgs.sources, " "),
|
|
(autoCompileImports ? "-i" : join(testArgs.compiledImports, " ")));
|
|
|
|
compile_output = execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path);
|
|
}
|
|
else
|
|
{
|
|
foreach (filename; testArgs.sources ~ (autoCompileImports ? null : testArgs.compiledImports))
|
|
{
|
|
string newo= result_path ~ replace(replace(filename, ".d", envData.obj), envData.sep~"imports"~envData.sep, envData.sep);
|
|
toCleanup ~= newo;
|
|
|
|
command = format("%s -conf= -m%s -I%s %s %s -od%s -c %s %s", envData.dmd, envData.model, input_dir,
|
|
reqArgs, permutedArgs, output_dir, argSet, filename);
|
|
compile_output ~= execute(fThisRun, command, testArgs.mode != TestMode.FAIL_COMPILE, result_path);
|
|
}
|
|
|
|
if (testArgs.mode == TestMode.RUN || testArgs.link)
|
|
{
|
|
// link .o's into an executable
|
|
command = format("%s -conf= -m%s%s%s %s %s -od%s -of%s %s", envData.dmd, envData.model,
|
|
autoCompileImports ? " -i" : "",
|
|
autoCompileImports ? "extraSourceIncludePaths" : "",
|
|
envData.required_args, testArgs.requiredArgsForLink, output_dir, test_app_dmd, join(toCleanup, " "));
|
|
|
|
execute(fThisRun, command, true, result_path);
|
|
}
|
|
}
|
|
|
|
compile_output = compile_output.unifyNewLine();
|
|
compile_output = std.regex.replaceAll(compile_output, regex(`^DMD v2\.[0-9]+.*\n? DEBUG$`, "m"), "");
|
|
compile_output = std.string.strip(compile_output);
|
|
// replace test_result path with fixed ones
|
|
compile_output = compile_output.replace(result_path, resultsDirReplacement);
|
|
compile_output = compile_output.replace(absoluteResultDirPath, resultsDirReplacement);
|
|
|
|
auto m = std.regex.match(compile_output, `Internal error: .*$`);
|
|
enforce(!m, m.hit);
|
|
m = std.regex.match(compile_output, `core.exception.AssertError@dmd.*`);
|
|
enforce(!m, m.hit);
|
|
|
|
if (!compareOutput(compile_output, testArgs.compileOutput, envData))
|
|
{
|
|
// Allow any messages to come from tests if TEST_OUTPUT wasn't given.
|
|
// This will be removed in future once all tests have been updated.
|
|
if (testArgs.compileOutput !is null ||
|
|
(testArgs.mode != TestMode.COMPILE &&
|
|
testArgs.mode != TestMode.FAIL_COMPILE &&
|
|
testArgs.mode != TestMode.RUN))
|
|
{
|
|
const diff = generateDiff(testArgs, compile_output, test_base_name);
|
|
throw new CompareException(testArgs.compileOutput, compile_output, diff);
|
|
}
|
|
}
|
|
|
|
if (testArgs.mode == TestMode.RUN)
|
|
{
|
|
toCleanup ~= test_app_dmd;
|
|
version(Windows)
|
|
if (envData.usingMicrosoftCompiler)
|
|
{
|
|
toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".ilk";
|
|
toCleanup ~= test_app_dmd_base ~ to!string(permuteIndex) ~ ".pdb";
|
|
}
|
|
|
|
if (testArgs.gdbScript is null)
|
|
{
|
|
command = test_app_dmd;
|
|
if (testArgs.executeArgs) command ~= " " ~ testArgs.executeArgs;
|
|
|
|
execute(fThisRun, command, true, result_path);
|
|
}
|
|
else version (linux)
|
|
{
|
|
auto script = test_app_dmd_base ~ to!string(permuteIndex) ~ ".gdb";
|
|
toCleanup ~= script;
|
|
with (File(script, "w"))
|
|
{
|
|
writeln("set disable-randomization off");
|
|
write(testArgs.gdbScript);
|
|
}
|
|
string gdbCommand = "gdb "~test_app_dmd~" --batch -x "~script;
|
|
auto gdb_output = execute(fThisRun, gdbCommand, true, result_path);
|
|
if (testArgs.gdbMatch !is null)
|
|
{
|
|
enforce(match(gdb_output, regex(testArgs.gdbMatch)),
|
|
"\nGDB regex: '"~testArgs.gdbMatch~"' didn't match output:\n----\n"~gdb_output~"\n----\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
fThisRun.close();
|
|
|
|
if (testArgs.postScript && !envData.coverage_build)
|
|
{
|
|
f.write("Executing post-test script: ");
|
|
string prefix = "";
|
|
version (Windows) prefix = "bash ";
|
|
assert(testArgs.sources[0].length, "Internal error: the tested file has no sources.");
|
|
import std.path : baseName, dirName, stripExtension;
|
|
auto testDir = testArgs.sources[0].dirName.baseName;
|
|
auto testName = testArgs.sources[0].baseName.stripExtension;
|
|
execute(f, prefix ~ "tools/postscript.sh " ~ testArgs.postScript ~ " " ~ testDir ~ " " ~ testName ~ " " ~ thisRunName, true, result_path);
|
|
}
|
|
|
|
foreach (file; toCleanup) collectException(std.file.remove(file));
|
|
return Result.continue_;
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
// it failed but it was disabled, exit as if it was successful
|
|
if (testArgs.isDisabled)
|
|
{
|
|
writeln();
|
|
return Result.return0;
|
|
}
|
|
|
|
if (envData.autoUpdate)
|
|
if (auto ce = cast(CompareException) e)
|
|
{
|
|
// remove the output file in test_results as its outdated
|
|
output_file.remove();
|
|
|
|
if (testArgs.compileOutputFile)
|
|
{
|
|
std.file.write(testArgs.compileOutputFile, ce.actual);
|
|
writefln("\n==> `TEST_OUTPUT_FILE` `%s` has been updated", testArgs.compileOutputFile);
|
|
return Result.return0;
|
|
}
|
|
|
|
auto existingText = input_file.readText;
|
|
auto updatedText = existingText.replace(ce.expected, ce.actual);
|
|
if (existingText != updatedText)
|
|
{
|
|
std.file.write(input_file, updatedText);
|
|
writefln("\n==> `TEST_OUTPUT` of %s has been updated", input_file);
|
|
}
|
|
else
|
|
{
|
|
writefln("\nWARNING: %s has multiple `TEST_OUTPUT` blocks and can't be auto-updated", input_file);
|
|
}
|
|
return Result.return0;
|
|
}
|
|
f.writeln();
|
|
f.writeln("==============================");
|
|
f.writef("Test %s failed: ", input_file);
|
|
f.writeln(e.msg);
|
|
f.close();
|
|
|
|
writefln("\nTest %s failed. The logged output:", input_file);
|
|
auto outputText = output_file.readText;
|
|
writeln(outputText);
|
|
output_file.remove();
|
|
|
|
// auto-update if a diff is found and can be updated
|
|
if (envData.autoUpdate &&
|
|
outputText.canFind("diff ") && outputText.canFind("--- ") && outputText.canFind("+++ "))
|
|
{
|
|
import std.range : dropOne;
|
|
auto newFile = outputText.findSplitAfter("+++ ")[1].until("\t");
|
|
auto baseFile = outputText.findSplitAfter("--- ")[1].until("\t");
|
|
writefln("===> Updating %s with %s", baseFile, newFile);
|
|
newFile.copy(baseFile);
|
|
return Result.return0;
|
|
}
|
|
|
|
// automatically rerun a segfaulting test and print its stack trace
|
|
version(linux)
|
|
if (e.msg.canFind("exited with rc == 139"))
|
|
{
|
|
auto gdbCommand = "gdb -q -n -ex 'set backtrace limit 100' -ex run -ex bt -batch -args " ~ command;
|
|
import std.process : spawnShell;
|
|
spawnShell(gdbCommand).wait;
|
|
}
|
|
|
|
return Result.return1;
|
|
}
|
|
}
|
|
|
|
size_t index = 0; // index over all tests to avoid identical output names in consecutive tests
|
|
auto argSets = (testArgs.argSets.length == 0) ? [""] : testArgs.argSets;
|
|
for(auto autoCompileImports = false;; autoCompileImports = true)
|
|
{
|
|
foreach(argSet; argSets)
|
|
{
|
|
foreach (c; combinations(testArgs.permuteArgs))
|
|
{
|
|
final switch(testCombination(autoCompileImports, argSet, index, c))
|
|
{
|
|
case Result.continue_: break;
|
|
case Result.return0: return 0;
|
|
case Result.return1: return 1;
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
if(autoCompileImports || testArgs.compiledImports.length == 0)
|
|
break;
|
|
}
|
|
|
|
if (printRuntime)
|
|
{
|
|
const long ms = stopWatch.peek.total!"msecs";
|
|
writefln(" [%.3f secs]", ms / 1000.0);
|
|
}
|
|
else
|
|
writeln();
|
|
|
|
// it was disabled but it passed! print an informational message
|
|
if (testArgs.isDisabled)
|
|
writefln(" !!! %-30s DISABLED but PASSES!", input_file);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int runBashTest(string input_dir, string test_name)
|
|
{
|
|
const scriptPath = dmdTestDir.buildPath("tools", "sh_do_test.sh");
|
|
version(Windows)
|
|
{
|
|
auto process = spawnShell(format("bash %s %s %s",
|
|
scriptPath, input_dir, test_name));
|
|
}
|
|
else
|
|
{
|
|
auto process = spawnProcess([scriptPath, input_dir, test_name]);
|
|
}
|
|
return process.wait();
|
|
}
|
|
|
|
/// Return the correct pic flags
|
|
string[] getPicFlags()
|
|
{
|
|
version (Windows) { } else
|
|
{
|
|
version(X86_64)
|
|
return ["-fPIC"];
|
|
if (environment.get("PIC", null) == "1")
|
|
return ["-fPIC"];
|
|
}
|
|
return cast(string[])[];
|
|
}
|
|
|
|
/// Run a dshell test
|
|
int runDShellTest(string input_dir, string test_name, const ref EnvData envData,
|
|
string output_dir, string output_file)
|
|
{
|
|
const testScriptDir = buildPath(dmdTestDir, input_dir);
|
|
const testScriptPath = buildPath(testScriptDir, test_name ~ ".d");
|
|
const testOutDir = buildPath(output_dir, test_name);
|
|
const testLogName = format("%s/%s.d", input_dir, test_name);
|
|
|
|
writefln(" ... %s", testLogName);
|
|
|
|
removeIfExists(output_file);
|
|
if (exists(testOutDir))
|
|
rmdirRecurse(testOutDir);
|
|
mkdirRecurse(testOutDir);
|
|
|
|
// create the "dshell" module for the tests
|
|
{
|
|
auto dshellFile = File(buildPath(testOutDir, "dshell.d"), "w");
|
|
dshellFile.writeln(`module dshell;
|
|
public import dshell_prebuilt;
|
|
static this()
|
|
{
|
|
dshellPrebuiltInit("` ~ input_dir ~ `", "`, test_name , `");
|
|
}
|
|
`);
|
|
}
|
|
|
|
const testScriptExe = buildPath(testOutDir, "run" ~ envData.exe);
|
|
const output_file_temp = output_file ~ ".tmp";
|
|
|
|
//
|
|
// compile the test
|
|
//
|
|
{
|
|
auto outfile = File(output_file_temp, "w");
|
|
const compile = [envData.dmd, "-conf=", "-m"~envData.model] ~
|
|
getPicFlags ~ [
|
|
"-od" ~ testOutDir,
|
|
"-of" ~ testScriptExe,
|
|
"-I=" ~ testScriptDir,
|
|
"-I=" ~ testOutDir,
|
|
"-I=" ~ buildPath(dmdTestDir, "tools", "dshell_prebuilt"),
|
|
"-i",
|
|
// Causing linker errors for some reason?
|
|
"-i=-dshell_prebuilt", buildPath(envData.results_dir, "dshell_prebuilt" ~ envData.obj),
|
|
testScriptPath,
|
|
];
|
|
outfile.writeln("[COMPILE_TEST] ", escapeShellCommand(compile));
|
|
// Note that spawnprocess closes the file, so it will need to be re-opened
|
|
// below when we run the test
|
|
auto compileProc = std.process.spawnProcess(compile, stdin, outfile, outfile);
|
|
const exitCode = wait(compileProc);
|
|
if (exitCode != 0)
|
|
{
|
|
printTestFailure(testLogName, output_file_temp);
|
|
return exitCode;
|
|
}
|
|
}
|
|
|
|
//
|
|
// run the test
|
|
//
|
|
{
|
|
auto outfile = File(output_file_temp, "a");
|
|
const runTest = [testScriptExe];
|
|
outfile.writeln("[RUN_TEST] ", escapeShellCommand(runTest));
|
|
auto runTestProc = std.process.spawnProcess(runTest, stdin, outfile, outfile);
|
|
const exitCode = wait(runTestProc);
|
|
if (exitCode != 0)
|
|
{
|
|
printTestFailure(testLogName, output_file_temp);
|
|
return exitCode;
|
|
}
|
|
}
|
|
|
|
rename(output_file_temp, output_file);
|
|
// TODO: should we remove all the test artifacts if the test passes? rmdirRecurse(testOutDir)?
|
|
return 0;
|
|
}
|
|
|
|
void printTestFailure(string testLogName, string output_file_temp)
|
|
{
|
|
writeln("==============================");
|
|
writefln("Test '%s' failed. The logged output:", testLogName);
|
|
const output = readText(output_file_temp);
|
|
write(output);
|
|
if (!output.endsWith("\n"))
|
|
writeln();
|
|
writeln("==============================");
|
|
remove(output_file_temp);
|
|
}
|
|
|
|
/// Make any parent diretories needed for the given `filename`
|
|
void mkdirsFor(string filename)
|
|
{
|
|
auto dir = dirName(filename);
|
|
if (!exists(dir))
|
|
mkdirRecurse(dir);
|
|
}
|