1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool_test.builders;
11 
12 import scriptlike;
13 
14 import std.datetime.stopwatch : StopWatch;
15 import std.range : isInputRange;
16 import std.typecons : Yes, No, Flag;
17 import std.traits : ReturnType;
18 
19 static import core.thread;
20 
21 import dextool_test.utils : escapePath;
22 
23 /** Build the command line arguments and working directory to use when invoking
24  * dextool.
25  */
26 struct BuildDextoolRun {
27     import std.ascii : newline;
28 
29     private {
30         string dextool;
31         string workdir_;
32         string test_outputdir;
33         string[] args_;
34         string[] post_args;
35         string[] flags_;
36 
37         /// if the output from running the command should be yapped via scriptlike
38         bool yap_output = true;
39 
40         /// if --debug is added to the arguments
41         bool arg_debug = true;
42 
43         /// Throw an exception if the exit status is NOT zero
44         bool throw_on_exit_status = true;
45     }
46 
47     /**
48      * Params:
49      *  command = the executable to run
50      *  workdir = directory to run the executable from
51      */
52     this(string dextool, string workdir) {
53         this.dextool = dextool;
54         this.workdir_ = workdir;
55         this.test_outputdir = workdir;
56     }
57 
58     Path workdir() {
59         return Path(workdir_);
60     }
61 
62     auto setWorkdir(T)(T v) {
63         static if (is(T == string))
64             workdir_ = v;
65         else
66             workdir_ = v.toString;
67         return this;
68     }
69 
70     auto throwOnExitStatus(bool v) {
71         this.throw_on_exit_status = v;
72         return this;
73     }
74 
75     auto flags(string[] v) {
76         this.flags_ = v;
77         return this;
78     }
79 
80     auto addFlag(T)(T v) {
81         this.flags_ ~= v;
82         return this;
83     }
84 
85     auto addDefineFlag(string v) {
86         this.flags_ ~= ["-D", v];
87         return this;
88     }
89 
90     auto addIncludeFlag(string v) {
91         this.flags_ ~= ["-I", v];
92         return this;
93     }
94 
95     auto addIncludeFlag(Path v) {
96         this.flags_ ~= ["-I", v.toString];
97         return this;
98     }
99 
100     auto args(string[] v) {
101         this.args_ = v;
102         return this;
103     }
104 
105     auto addArg(T)(T v) {
106         this.args_ ~= v;
107         return this;
108     }
109 
110     auto addArg(Path v) {
111         this.args_ ~= v.escapePath;
112         return this;
113     }
114 
115     auto addInputArg(string v) {
116         post_args ~= "--in=" ~ Path(v).escapePath;
117         return this;
118     }
119 
120     auto addInputArg(string[] v) {
121         post_args ~= v.map!(a => Path(a)).map!(a => "--in=" ~ a.escapePath).array();
122         return this;
123     }
124 
125     auto addInputArg(Path v) {
126         post_args ~= "--in=" ~ v.escapePath;
127         return this;
128     }
129 
130     auto addInputArg(Path[] v) {
131         post_args ~= v.map!(a => "--in=" ~ a.escapePath).array();
132         return this;
133     }
134 
135     auto postArg(string[] v) {
136         this.post_args = v;
137         return this;
138     }
139 
140     auto addPostArg(T)(T v) {
141         this.post_args ~= v;
142         return this;
143     }
144 
145     auto addPostArg(Path v) {
146         this.post_args ~= v.escapePath;
147         return this;
148     }
149 
150     /// Activate debugging mode of the dextool binary
151     auto argDebug(bool v) {
152         arg_debug = v;
153         return this;
154     }
155 
156     auto yapOutput(bool v) {
157         yap_output = v;
158         return this;
159     }
160 
161     auto run() {
162         import std.array : join;
163         import std.algorithm : min;
164 
165         string[] cmd;
166         cmd ~= dextool;
167         cmd ~= args_.dup;
168         cmd ~= post_args;
169         cmd ~= "--out=" ~ workdir_;
170 
171         if (arg_debug) {
172             cmd ~= "--debug";
173         }
174 
175         if (flags_.length > 0) {
176             cmd ~= "--";
177             cmd ~= flags_.dup;
178         }
179 
180         StopWatch sw;
181         ReturnType!(std.process.tryWait) exit_;
182         exit_.status = -1;
183         Appender!(string[]) stdout_;
184         Appender!(string[]) stderr_;
185 
186         sw.start;
187         try {
188             auto p = std.process.pipeProcess(cmd,
189                     std.process.Redirect.stdout | std.process.Redirect.stderr);
190 
191             for (;;) {
192                 exit_ = std.process.tryWait(p.pid);
193 
194                 foreach (l; p.stdout.byLineCopy)
195                     stdout_.put(l);
196                 foreach (l; p.stderr.byLineCopy)
197                     stderr_.put(l);
198 
199                 if (exit_.terminated)
200                     break;
201                 core.thread.Thread.sleep(20.msecs);
202             }
203             sw.stop;
204         }
205         catch (Exception e) {
206             stderr_ ~= [e.msg];
207             sw.stop;
208         }
209 
210         auto rval = BuildCommandRunResult(exit_.status == 0, exit_.status,
211                 stdout_.data, stderr_.data, sw.peek.total!"msecs", cmd);
212         if (yap_output) {
213             auto f = File(nextFreeLogfile(test_outputdir), "w");
214             f.writef("%s", rval);
215         }
216 
217         if (throw_on_exit_status && exit_.status != 0) {
218             auto l = min(10, stderr_.data.length);
219             throw new ErrorLevelException(exit_.status, stderr_.data[0 .. l].join(newline));
220         } else {
221             return rval;
222         }
223     }
224 }
225 
226 /** Build the command line arguments and working directory to use when invoking
227  * a command.
228  */
229 struct BuildCommandRun {
230     import std.ascii : newline;
231 
232     private {
233         string command;
234         string workdir_;
235         string[] args_;
236 
237         bool run_in_outdir;
238 
239         /// if the output from running the command should be yapped via scriptlike
240         bool yap_output = true;
241 
242         /// Throw an exception if the exit status is NOT zero
243         bool throw_on_exit_status = true;
244     }
245 
246     this(string command) {
247         this.command = command;
248         run_in_outdir = false;
249     }
250 
251     /**
252      * Params:
253      *  command = the executable to run
254      *  workdir = directory to run the executable from
255      */
256     this(string command, string workdir) {
257         this.command = command;
258         this.workdir_ = workdir;
259         run_in_outdir = true;
260     }
261 
262     Path workdir() {
263         return Path(workdir_);
264     }
265 
266     auto setWorkdir(Path v) {
267         workdir_ = v.toString;
268         return this;
269     }
270 
271     /// If the command to run is in workdir.
272     auto commandInOutdir(bool v) {
273         run_in_outdir = v;
274         return this;
275     }
276 
277     auto throwOnExitStatus(bool v) {
278         this.throw_on_exit_status = v;
279         return this;
280     }
281 
282     auto args(string[] v) {
283         this.args_ = v;
284         return this;
285     }
286 
287     auto addArg(string v) {
288         this.args_ ~= v;
289         return this;
290     }
291 
292     auto addArg(Path v) {
293         this.args_ ~= v.escapePath;
294         return this;
295     }
296 
297     auto addArg(string[] v) {
298         this.args_ ~= v;
299         return this;
300     }
301 
302     auto addFileFromOutdir(string v) {
303         this.args_ ~= buildPath(workdir_, v);
304         return this;
305     }
306 
307     auto yapOutput(bool v) {
308         yap_output = v;
309         return this;
310     }
311 
312     auto run() {
313         import std.path : buildPath;
314 
315         string[] cmd;
316         if (run_in_outdir)
317             cmd ~= buildPath(workdir.toString, command);
318         else
319             cmd ~= command;
320         cmd ~= args_.dup;
321 
322         StopWatch sw;
323         ReturnType!(std.process.tryWait) exit_;
324         exit_.status = -1;
325         Appender!(string[]) stdout_;
326         Appender!(string[]) stderr_;
327 
328         sw.start;
329         try {
330             auto p = std.process.pipeProcess(cmd,
331                     std.process.Redirect.stdout | std.process.Redirect.stderr);
332 
333             for (;;) {
334                 exit_ = std.process.tryWait(p.pid);
335 
336                 foreach (l; p.stdout.byLineCopy)
337                     stdout_.put(l);
338                 foreach (l; p.stderr.byLineCopy)
339                     stderr_.put(l);
340 
341                 if (exit_.terminated)
342                     break;
343                 core.thread.Thread.sleep(10.msecs);
344             }
345 
346             sw.stop;
347         }
348         catch (Exception e) {
349             stderr_ ~= [e.msg];
350             sw.stop;
351         }
352 
353         auto rval = BuildCommandRunResult(exit_.status == 0, exit_.status,
354                 stdout_.data, stderr_.data, sw.peek.total!"msecs", cmd);
355         if (yap_output) {
356             auto f = File(nextFreeLogfile(workdir_), "w");
357             f.writef("%s", rval);
358         }
359 
360         if (throw_on_exit_status && exit_.status != 0) {
361             auto l = min(10, stderr_.data.length);
362             throw new ErrorLevelException(exit_.status, stderr_.data[0 .. l].join(newline));
363         } else {
364             return rval;
365         }
366     }
367 }
368 
369 private auto nextFreeLogfile(string workdir) {
370     import std.file : exists;
371     import std.path : baseName, buildPath;
372     import std..string : format;
373 
374     int idx;
375     string f;
376     do {
377         f = buildPath(workdir, format("run_command%s.log", idx));
378         ++idx;
379     }
380     while (exists(f));
381 
382     return f;
383 }
384 
385 struct BuildCommandRunResult {
386     import std.ascii : newline;
387     import std.format : FormatSpec;
388 
389     /// convenient value which is true when exit status is zero.
390     const bool success;
391     /// actual exit status
392     const int status;
393     /// captured output
394     string[] stdout;
395     string[] stderr;
396     /// time to execute the command. TODO: change to Duration after DMD v2.076
397     const long executionMsecs;
398 
399     private string[] cmd;
400 
401     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
402         import std.algorithm : joiner;
403         import std.format : formattedWrite;
404         import std.range.primitives : put;
405 
406         formattedWrite(w, "run: %s", cmd.dup.joiner(" "));
407         put(w, newline);
408 
409         formattedWrite(w, "exit status: %s", status);
410         put(w, newline);
411         formattedWrite(w, "execution time ms: %s", executionMsecs);
412         put(w, newline);
413 
414         put(w, "stdout:");
415         put(w, newline);
416         this.stdout.each!((a) { put(w, a); put(w, newline); });
417 
418         put(w, "stderr:");
419         put(w, newline);
420         this.stderr.each!((a) { put(w, a); put(w, newline); });
421     }
422 
423     string toString() @safe pure const {
424         import std.exception : assumeUnique;
425         import std.format : FormatSpec;
426 
427         char[] buf;
428         buf.reserve(100);
429         auto fmt = FormatSpec!char("%s");
430         toString((const(char)[] s) { buf ~= s; }, fmt);
431         auto trustedUnique(T)(T t) @trusted {
432             return assumeUnique(t);
433         }
434 
435         return trustedUnique(buf);
436     }
437 }