/** A small library to help write D shell-like test scripts. */ module dshell_prebuilt; public import core.stdc.stdlib : exit; public import core.time; public import core.thread; public import std.meta; public import std.exception; public import std.array; public import std.string; public import std.format; public import std.path; public import std.file; public import std.regex; public import std.stdio; public import std.process; /** Emulates bash environment variables. Variables set here will be availble for BASH-like expansion. */ struct Vars { private static __gshared string[string] map; static void set(string name, string value) in { assert(value !is null); } do { const expanded = shellExpand(value); assert(expanded !is null, "codebug"); map[name] = expanded; } static string get(const(char)[] name) { auto result = map.get(cast(string)name, null); if (result is null) assert(0, "Unknown variable '" ~ name ~ "'"); return result; } static string opDispatch(string name)() { return get(name); } static void opDispatch(string name)(string value) { set(name, value); } } private alias requiredEnvVars = AliasSeq!( "MODEL", "RESULTS_DIR", "EXE", "OBJ", "DMD", "DFLAGS", "OS", "SEP", "DSEP", "BUILD" ); private alias optionalEnvVars = AliasSeq!( "CC", "CXX", "PIC_FLAG" ); private alias allVars = AliasSeq!( requiredEnvVars, optionalEnvVars, "TEST_DIR", "TEST_NAME", "RESULTS_TEST_DIR", "OUTPUT_BASE", "EXTRA_FILES", "LIBEXT", "SOEXT" ); static foreach (var; allVars) { mixin(`string ` ~ var ~ `() { return Vars.` ~ var ~ `; }`); } /// called from the dshell module to initialize environment void dshellPrebuiltInit(string testDir, string testName) { foreach (var; requiredEnvVars) { Vars.set(var, requireEnv(var)); } foreach (var; optionalEnvVars) { Vars.set(var, environment.get(var, "")); } Vars.set("TEST_DIR", testDir); Vars.set("TEST_NAME", testName); // reference to the resulting test_dir folder, e.g .test_results/runnable Vars.set("RESULTS_TEST_DIR", buildPath(RESULTS_DIR, TEST_DIR)); // reference to the resulting files without a suffix, e.g. test_results/runnable/test123import test); Vars.set("OUTPUT_BASE", buildPath(RESULTS_TEST_DIR, TEST_NAME)); // reference to the extra files directory Vars.set("EXTRA_FILES", buildPath(TEST_DIR, "extra-files")); // reference to the imports directory Vars.set("IMPORT_FILES", buildPath(TEST_DIR, "imports")); version (Windows) { Vars.set("LIBEXT", ".lib"); Vars.set("SOEXT", ".dll"); } else version (OSX) { Vars.set("LIBEXT", ".a"); Vars.set(`SOEXT`, `.dylib`); } else { Vars.set("LIBEXT", ".a"); Vars.set("SOEXT", ".so"); } } private string requireEnv(string name) { const result = environment.get(name, null); if (result is null) { writefln("Error: missing required environment variable '%s'", name); exit(1); } return result; } /// Exit code to return if the test is disabled for the current platform enum DISABLED = 125; /// Remove one or more files void rm(scope const(char[])[] args...) { foreach (arg; args) { auto expanded = shellExpand(arg); if (exists(expanded)) { writeln("rm '", expanded, "'"); // Use loop to workaround issue in windows with removing // executables after running then for (int sleepMsecs = 10; ; sleepMsecs *= 2) { try { std.file.remove(expanded); break; } catch (Exception e) { if (sleepMsecs >= 3000) throw e; Thread.sleep(dur!"msecs"(sleepMsecs)); } } } } } /// Make all parent directories needed to create the given `filename` void mkdirFor(string filename) { auto dir = dirName(filename); if (!exists(dir)) { writefln("[INFO] mkdir -p '%s'", dir); mkdirRecurse(dir); } } /** Run the given command. The `tryRun` variants return the exit code, whereas the `run` variants will assert on a non-zero exit code. */ int tryRun(scope const(char[])[] args, File stdout = std.stdio.stdout, File stderr = std.stdio.stderr, string[string] env = null) { std.stdio.stdout.write("[RUN]"); if (env) { foreach (pair; env.byKeyValue) { std.stdio.stdout.write(" ", pair.key, "=", pair.value); } } std.stdio.write(" ", escapeShellCommand(args)); if (stdout != std.stdio.stdout) { std.stdio.stdout.write(" > ", stdout.name); } // "Commit" all output from the tester std.stdio.stdout.writeln(); std.stdio.stdout.flush(); std.stdio.stderr.writeln(); std.stdio.stderr.flush(); auto proc = spawnProcess(args, stdin, stdout, stderr, env); return wait(proc); } /// ditto int tryRun(string cmd, File stdout = std.stdio.stdout, File stderr = std.stdio.stderr, string[string] env = null) { return tryRun(parseCommand(cmd), stdout, stderr, env); } /// ditto void run(scope const(char[])[] args, File stdout = std.stdio.stdout, File stderr = std.stdio.stderr, string[string] env = null) { const exitCode = tryRun(args, stdout, stderr, env); if (exitCode != 0) { writefln("Error: last command exited with code %s", exitCode); assert(0, "last command failed"); } } /// ditto void run(string cmd, File stdout = std.stdio.stdout, File stderr = std.stdio.stderr, string[string] env = null) { // TODO: option to disable this? if (SEP != "/") cmd = cmd.replace("/", SEP); run(parseCommand(cmd), stdout, stderr, env); } /** Parse the given string `s` as a command. Performs BASH-like variable expansion. */ string[] parseCommand(string s) { auto rawArgs = s.split(); auto args = appender!(string[])(); foreach (rawArg; rawArgs) { const exp = shellExpand(rawArg); if (exp.length) args.put(exp); } return args.data; } /// Expand the given string using BASH-like variable expansion. string shellExpand(const(char)[] s) { auto expanded = appender!(char[])(); for (size_t i = 0; i < s.length;) { if (s[i] != '$') { expanded.put(s[i]); i++; } else { i++; assert(i < s.length, "lone '$' at end of string"); auto start = i; if (s[i] == '{') { start++; for (;;) { i++; assert(i < s.length, "unterminated ${..."); if (s[i] == '}') break; } expanded.put(Vars.get(s[start .. i])); i++; } else { assert(validVarChar(s[i]), "invalid sequence $'" ~ s[i]); for (;;) { i++; if (i >= s.length || !validVarChar(s[i])) break; } expanded.put(Vars.get(s[start .. i])); } } } auto result = expanded.data; return (result is null) ? "" : result.assumeUnique; } // [a-zA-Z0-9_] private bool validVarChar(const char c) { import std.ascii : isAlphaNum; return c.isAlphaNum || c == '_'; } struct GrepResult { string[] matches; void enforceMatches(string message) { if (matches.length == 0) { assert(0, message); } } } /** grep the given `file` for the given `pattern`. */ GrepResult grep(string file, string pattern) { const patternExpanded = shellExpand(pattern); const fileExpanded = shellExpand(file); writefln("[GREP] file='%s' pattern='%s'", fileExpanded, patternExpanded); return grepLines(File(fileExpanded, "r").byLine, patternExpanded); } /// ditto GrepResult grep(GrepResult lastResult, string pattern) { auto patternExpanded = shellExpand(pattern); writefln("[GREP] (%s lines from last grep) pattern='%s'", lastResult.matches.length, patternExpanded); return grepLines(lastResult.matches, patternExpanded); } private GrepResult grepLines(T)(T lineRange, string finalPattern) { auto matches = appender!(string[])(); foreach(line; lineRange) { if (matchFirst(line, finalPattern)) { static if (is(typeof(lineRange.front()) == string)) matches.put(line); else matches.put(line.idup); } } writefln("[GREP] matched %s lines", matches.data.length); return GrepResult(matches.data); } /** remove \r and the compiler debug header from the given string. */ string filterCompilerOutput(string output) { output = std.string.replace(output, "\r", ""); output = std.regex.replaceAll(output, regex(`^DMD v2\.[0-9]+.*\n? DEBUG\n`, "m"), ""); return output; }