1 /** 2 A small library to help write D shell-like test scripts. 3 */ 4 module dshell_prebuilt; 5 6 public import core.stdc.stdlib : exit; 7 8 public import core.time; 9 public import core.thread; 10 public import std.meta; 11 public import std.exception; 12 public import std.array; 13 public import std..string; 14 public import std.format; 15 public import std.path; 16 public import std.file; 17 public import std.regex; 18 public import std.stdio; 19 public import std.process; 20 21 /** 22 Emulates bash environment variables. Variables set here will be availble for BASH-like expansion. 23 */ 24 struct Vars 25 { 26 private static __gshared string[string] map; 27 static void set(string name, string value) 28 in { assert(value !is null); } do 29 { 30 const expanded = shellExpand(value); 31 assert(expanded !is null, "codebug"); 32 map[name] = expanded; 33 } 34 static string get(const(char)[] name) 35 { 36 auto result = map.get(cast(string)name, null); 37 if (result is null) 38 assert(0, "Unknown variable '" ~ name ~ "'"); 39 return result; 40 } 41 static string opDispatch(string name)() { return get(name); } 42 } 43 44 private alias requiredEnvVars = AliasSeq!( 45 "MODEL", "RESULTS_DIR", 46 "EXE", "OBJ", 47 "DMD", "DFLAGS", 48 "OS", "SEP", "DSEP", 49 "BUILD" 50 ); 51 private alias optionalEnvVars = AliasSeq!( 52 "CC", 53 ); 54 private alias allVars = AliasSeq!( 55 requiredEnvVars, 56 optionalEnvVars, 57 "TEST_DIR", "TEST_NAME", 58 "RESULTS_TEST_DIR", 59 "OUTPUT_BASE", "EXTRA_FILES", 60 "LIBEXT" 61 ); 62 63 static foreach (var; allVars) 64 { 65 mixin(`string ` ~ var ~ `() { return Vars.` ~ var ~ `; }`); 66 } 67 68 /// called from the dshell module to initialize environment 69 void dshellPrebuiltInit(string testDir, string testName) 70 { 71 foreach (var; requiredEnvVars) 72 { 73 Vars.set(var, requireEnv(var)); 74 } 75 76 foreach (var; optionalEnvVars) 77 { 78 Vars.set(var, environment.get(var, "")); 79 } 80 81 Vars.set("TEST_DIR", testDir); 82 Vars.set("TEST_NAME", testName); 83 // reference to the resulting test_dir folder, e.g .test_results/runnable 84 Vars.set("RESULTS_TEST_DIR", buildPath(RESULTS_DIR, TEST_DIR)); 85 // reference to the resulting files without a suffix, e.g. test_results/runnable/test123import test); 86 Vars.set("OUTPUT_BASE", buildPath(RESULTS_TEST_DIR, TEST_NAME)); 87 // reference to the extra files directory 88 Vars.set("EXTRA_FILES", buildPath(TEST_DIR, "extra-files")); 89 version (Windows) 90 { 91 Vars.set("LIBEXT", ".lib"); 92 } 93 else 94 { 95 Vars.set("LIBEXT", ".a"); 96 } 97 } 98 99 private string requireEnv(string name) 100 { 101 const result = environment.get(name, null); 102 if (result is null) 103 { 104 writefln("Error: missing required environment variable '%s'", name); 105 exit(1); 106 } 107 return result; 108 } 109 110 /// Exit code to return if the test is disabled for the current platform 111 enum DISABLED = 125; 112 113 /// Remove one or more files 114 void rm(scope const(char[])[] args...) 115 { 116 foreach (arg; args) 117 { 118 auto expanded = shellExpand(arg); 119 if (exists(expanded)) 120 { 121 writeln("rm '", expanded, "'"); 122 // Use loop to workaround issue in windows with removing 123 // executables after running then 124 for (int sleepMsecs = 10; ; sleepMsecs *= 2) 125 { 126 try { 127 std.file.remove(expanded); 128 break; 129 } catch (Exception e) { 130 if (sleepMsecs >= 3000) 131 throw e; 132 Thread.sleep(dur!"msecs"(sleepMsecs)); 133 } 134 } 135 } 136 } 137 } 138 139 /// Make all parent directories needed to create the given `filename` 140 void mkdirFor(string filename) 141 { 142 auto dir = dirName(filename); 143 if (!exists(dir)) 144 { 145 writefln("[INFO] mkdir -p '%s'", dir); 146 mkdirRecurse(dir); 147 } 148 } 149 150 /** 151 Run the given command. The `tryRun` variants return the exit code, whereas the `run` variants 152 will assert on a non-zero exit code. 153 */ 154 auto tryRun(scope const(char[])[] args, File stdout = std.stdio.stdout, 155 File stderr = std.stdio.stderr, string[string] env = null) 156 { 157 std.stdio.stdout.write("[RUN]"); 158 if (env) 159 { 160 foreach (pair; env.byKeyValue) 161 { 162 std.stdio.stdout.write(" ", pair.key, "=", pair.value); 163 } 164 } 165 std.stdio.write(" ", escapeShellCommand(args)); 166 if (stdout != std.stdio.stdout) 167 { 168 std.stdio.stdout.write(" > ", stdout.name); 169 } 170 std.stdio.stdout.writeln(); 171 std.stdio.stdout.flush(); 172 auto proc = spawnProcess(args, stdin, stdout, stderr, env); 173 return wait(proc); 174 } 175 /// ditto 176 void run(scope const(char[])[] args, File stdout = std.stdio.stdout, 177 File stderr = std.stdio.stderr, string[string] env = null) 178 { 179 const exitCode = tryRun(args, stdout, stderr, env); 180 if (exitCode != 0) 181 { 182 writefln("Error: last command exited with code %s", exitCode); 183 assert(0, "last command failed"); 184 } 185 } 186 /// ditto 187 void run(string cmd, File stdout = std.stdio.stdout, 188 File stderr = std.stdio.stderr, string[string] env = null) 189 { 190 // TODO: option to disable this? 191 if (SEP != "/") 192 cmd = cmd.replace("/", SEP); 193 run(parseCommand(cmd), stdout, stderr, env); 194 } 195 196 /** 197 Parse the given string `s` as a command. Performs BASH-like variable expansion. 198 */ 199 string[] parseCommand(string s) 200 { 201 auto rawArgs = s.split(); 202 auto args = appender!(string[])(); 203 foreach (rawArg; rawArgs) 204 { 205 args.put(shellExpand(rawArg)); 206 } 207 return args.data; 208 } 209 210 /// Expand the given string using BASH-like variable expansion. 211 string shellExpand(const(char)[] s) 212 { 213 auto expanded = appender!(char[])(); 214 for (size_t i = 0; i < s.length;) 215 { 216 if (s[i] != '$') 217 { 218 expanded.put(s[i]); 219 i++; 220 } 221 else 222 { 223 i++; 224 assert(i < s.length, "lone '$' at end of string"); 225 auto start = i; 226 if (s[i] == '{') 227 { 228 start++; 229 for (;;) 230 { 231 i++; 232 assert(i < s.length, "unterminated ${..."); 233 if (s[i] == '}') break; 234 } 235 expanded.put(Vars.get(s[start .. i])); 236 i++; 237 } 238 else 239 { 240 assert(validVarChar(s[i]), "invalid sequence $'" ~ s[i]); 241 for (;;) 242 { 243 i++; 244 if (i >= s.length || !validVarChar(s[i])) 245 break; 246 } 247 expanded.put(Vars.get(s[start .. i])); 248 } 249 } 250 } 251 auto result = expanded.data; 252 return (result is null) ? "" : result.assumeUnique; 253 } 254 255 // [a-zA-Z0-9_] 256 private bool validVarChar(const char c) 257 { 258 import std.ascii : isAlphaNum; 259 return c.isAlphaNum || c == '_'; 260 } 261 262 struct GrepResult 263 { 264 string[] matches; 265 void enforceMatches(string message) 266 { 267 if (matches.length == 0) 268 { 269 assert(0, message); 270 } 271 } 272 } 273 274 /** 275 grep the given `file` for the given `pattern`. 276 */ 277 GrepResult grep(string file, string pattern) 278 { 279 const patternExpanded = shellExpand(pattern); 280 const fileExpanded = shellExpand(file); 281 writefln("[GREP] file='%s' pattern='%s'", fileExpanded, patternExpanded); 282 return grepLines(File(fileExpanded, "r").byLine, patternExpanded); 283 } 284 /// ditto 285 GrepResult grep(GrepResult lastResult, string pattern) 286 { 287 auto patternExpanded = shellExpand(pattern); 288 writefln("[GREP] (%s lines from last grep) pattern='%s'", lastResult.matches.length, patternExpanded); 289 return grepLines(lastResult.matches, patternExpanded); 290 } 291 292 private GrepResult grepLines(T)(T lineRange, string finalPattern) 293 { 294 auto matches = appender!(string[])(); 295 foreach(line; lineRange) 296 { 297 if (matchFirst(line, finalPattern)) 298 { 299 static if (is(typeof(lineRange.front()) == string)) 300 matches.put(line); 301 else 302 matches.put(line.idup); 303 } 304 } 305 writefln("[GREP] matched %s lines", matches.data.length); 306 return GrepResult(matches.data); 307 } 308 309 /** 310 remove \r and the compiler debug header from the given string. 311 */ 312 string filterCompilerOutput(string output) 313 { 314 output = std..string.replace(output, "\r", ""); 315 output = std.regex.replaceAll(output, regex(`^DMD v2\.[0-9]+.*\n? DEBUG\n`, "m"), ""); 316 return output; 317 }