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 }