1 /**
2 Copyright: Copyright (c) 2015-2017, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module dextool_test.utils;
7 
8 import scriptlike;
9 
10 import std.range : isInputRange;
11 import std.typecons : Flag;
12 public import std.typecons : Yes, No;
13 
14 import logger = std.experimental.logger;
15 
16 enum dextoolExePath = "path_to_dextool/dextool_debug";
17 
18 import dextool_test.builders : BuildCommandRun;
19 
20 auto buildArtifacts() {
21     return Path("build");
22 }
23 
24 auto gmockLib() {
25     return buildArtifacts ~ "libgmock_gtest.a";
26 }
27 
28 private void delegate(string) oldYap = null;
29 private string[] yapLog;
30 
31 static this() {
32     scriptlikeCustomEcho = (string s) => dextoolYap(s);
33     echoOn;
34 }
35 
36 void dextoolYap(string msg) nothrow {
37     yapLog ~= msg;
38 }
39 
40 void dextoolYap(T...)(T args) {
41     import std.format : format;
42 
43     yapLog ~= format(args);
44 }
45 
46 string[] getYapLog() {
47     return yapLog.dup;
48 }
49 
50 void resetYapLog() {
51     yapLog.length = 0;
52 }
53 
54 void echoOn() {
55     .scriptlikeEcho = true;
56 }
57 
58 void echoOff() {
59     .scriptlikeEcho = false;
60 }
61 
62 string escapePath(in Path p) {
63     import scriptlike : escapeShellArg;
64 
65     return p.raw.dup.escapeShellArg;
66 }
67 
68 deprecated("to be removed") auto runAndLog(T)(T args_) {
69     import std.traits : Unqual;
70 
71     static if (is(Unqual!T == Path)) {
72         string args = args_.escapePath;
73     } else static if (is(Unqual!T == Args)) {
74         string args = args_.data;
75     } else {
76         string args = args_;
77     }
78 
79     auto status = tryRunCollect(args);
80 
81     yap("Exit status: ", status.status);
82     yap(status.output);
83     return status;
84 }
85 
86 void syncMkdirRecurse(string p) nothrow {
87     synchronized {
88         try {
89             mkdirRecurse(p);
90         }
91         catch (Exception e) {
92         }
93     }
94 }
95 
96 struct TestEnv {
97     import std.ascii : newline;
98 
99     private Path outdir_;
100     private string outdir_suffix;
101     private Path dextool_;
102 
103     this(Path dextool) {
104         this.dextool_ = dextool.absolutePath;
105     }
106 
107     Path outdir() const nothrow {
108         try {
109             return ((buildArtifacts ~ outdir_).stripExtension ~ outdir_suffix).absolutePath;
110         }
111         catch (Exception e) {
112             return ((buildArtifacts ~ outdir_).stripExtension ~ outdir_suffix);
113         }
114     }
115 
116     Path dextool() const {
117         return dextool_;
118     }
119 
120     string toString() {
121         // dfmt off
122         return only(
123                     ["dextool:", dextool.toString],
124                     ["outdir:", outdir.toString],
125                     )
126             .map!(a => leftJustifier(a[0], 10).text ~ a[1])
127             .joiner(newline)
128             .text;
129         // dfmt on
130     }
131 
132     void setOutput(Path outdir__) {
133         this.outdir_ = outdir__;
134     }
135 
136     /** Setup the test environment
137      *
138      * Example of using the outputSuffix.
139      * ---
140      * mixin(envSetup(globalTestdir, No.setupEnv));
141      * testEnv.outputSuffix("foo");
142      * testEnv.setupEnv;
143      * ---
144      */
145     void outputSuffix(string suffix) {
146         this.outdir_suffix = suffix;
147     }
148 
149     void setupEnv() {
150         yap("Test environment:", newline, toString);
151         syncMkdirRecurse(outdir.toString);
152         cleanOutdir;
153     }
154 
155     void cleanOutdir() nothrow {
156         // ensure logs are empty
157         const auto d = outdir();
158 
159         string[] files;
160 
161         try {
162             files = dirEntries(d, SpanMode.depth).filter!(a => a.isFile).map!(a => a.name).array();
163         }
164         catch (Exception e) {
165         }
166 
167         foreach (a; files) {
168             // tryRemove can fail, usually duo to I/O when tests are ran in
169             // parallel.
170             try {
171                 tryRemove(Path(a));
172             }
173             catch (Exception e) {
174             }
175         }
176     }
177 
178     void setup(Path outdir__) {
179         setOutput(outdir__);
180         setupEnv;
181     }
182 
183     void teardown() {
184         auto stdout_path = outdir ~ "console.log";
185         File logfile;
186         try {
187             logfile = File(stdout_path.toString, "w");
188         }
189         catch (Exception e) {
190             logger.trace(e.msg);
191             return;
192         }
193 
194         // Use when saving error data for later analyze
195         foreach (l; getYapLog) {
196             logfile.writeln(l);
197         }
198         resetYapLog();
199     }
200 }
201 
202 //TODO deprecated, use envSetup instead.
203 string EnvSetup(string logdir) {
204     return envSetup(logdir, Yes.setupEnv);
205 }
206 
207 string envSetup(string logdir, Flag!"setupEnv" setupEnv = Yes.setupEnv) {
208     import std.format : format;
209 
210     auto txt = `
211     import scriptlike;
212 
213     auto testEnv = TestEnv(Path("%s"));
214 
215     // Setup and cleanup
216     scope (exit) {
217         testEnv.teardown();
218     }
219     chdir(thisExePath.dirName);
220 
221     {
222         import std.traits : fullyQualifiedName;
223         int _ = 0;
224         testEnv.setOutput(Path("%s/" ~ fullyQualifiedName!_));
225     }
226 `;
227 
228     txt = format(txt, dextoolExePath, logdir);
229 
230     if (setupEnv) {
231         txt ~= "\ntestEnv.setupEnv();\n";
232     }
233 
234     return txt;
235 }
236 
237 struct GR {
238     Path gold;
239     Path result;
240 }
241 
242 auto removeJunk(R)(R r, Flag!"skipComments" skipComments) {
243     import std.algorithm : filter;
244     import std.range : tee;
245 
246     // dfmt off
247     return r
248         // remove comments
249         .filter!(a => !skipComments || !(a.value.strip.length > 2 && a.value.strip[0 .. 2] == "//"))
250         // remove the line with the version
251         .filter!(a => !(a.value.length > 39 && a.value[0 .. 39] == "/// @brief Generated by dextool version"))
252         .filter!(a => !(a.value.length > 32 && a.value[0 .. 32] == "/// Generated by dextool version"))
253         // remove empty lines
254         .filter!(a => a.value.strip.length != 0);
255     // dfmt on
256 }
257 
258 /** Sorted compare of gold and result.
259  *
260  * TODO remove this function when all tests are converted to using BuildCompare.
261  *
262  * max_diff is arbitrarily chosen to 5.
263  * The purpose is to limit the amount of text that is dumped.
264  * The reasoning is that it is better to give more than one line as feedback.
265  */
266 deprecated("to be removed") void compare(in Path gold, in Path result,
267         Flag!"sortLines" sortLines, Flag!"skipComments" skipComments = Yes.skipComments) {
268     import std.stdio : File;
269 
270     yap("Comparing gold:", gold.raw);
271     yap("        result:", result.raw);
272 
273     File goldf;
274     File resultf;
275 
276     try {
277         goldf = File(gold.escapePath);
278         resultf = File(result.escapePath);
279     }
280     catch (ErrnoException ex) {
281         throw new ErrorLevelException(-1, ex.msg);
282     }
283 
284     auto maybeSort(T)(T lines) {
285         import std.array : array;
286         import std.algorithm : sort;
287 
288         if (sortLines) {
289             return sort!((a, b) => a[1] < b[1])(lines.array()).array();
290         }
291 
292         return lines.array();
293     }
294 
295     bool diff_detected = false;
296     immutable max_diff = 5;
297     int accumulated_diff;
298     // dfmt off
299     foreach (g, r;
300              lockstep(maybeSort(goldf
301                                 .byLineCopy()
302                                 .enumerate
303                                 .removeJunk(skipComments)),
304                       maybeSort(resultf
305                                 .byLineCopy()
306                                 .enumerate
307                                 .removeJunk(skipComments))
308                       )) {
309         if (g[1] != r[1] && accumulated_diff < max_diff) {
310             // +1 of index because editors start counting lines from 1
311             yap("Line ", g[0] + 1, " gold:", g[1]);
312             yap("Line ", r[0] + 1, "  out:", r[1], "\n");
313             diff_detected = true;
314             ++accumulated_diff;
315         }
316     }
317     // dfmt on
318 
319     //TODO replace with enforce
320     if (diff_detected) {
321         yap("Output is different from reference file (gold): " ~ gold.escapePath);
322         throw new ErrorLevelException(-1,
323                 "Output is different from reference file (gold): " ~ gold.escapePath);
324     }
325 }
326 
327 deprecated("to be removed") bool stdoutContains(const string txt) {
328     import std..string : indexOf;
329 
330     return getYapLog().joiner().array().indexOf(txt) != -1;
331 }
332 
333 /// Check if a log contains the fragment txt.
334 bool sliceContains(const string[] log, const string txt) {
335     import std..string : indexOf;
336 
337     return log.dup.joiner().array().indexOf(txt) != -1;
338 }
339 
340 /// Check if the logged stdout data contains the input range.
341 bool stdoutContains(T)(const T gold_lines) if (isInputRange!T) {
342     auto result_lines = getYapLog().map!(a => a.splitLines).joiner().array();
343     return sliceContains(result_lines, gold_lines);
344 }
345 
346 /// Check if the log contains the input range.
347 bool sliceContains(T)(const string[] log, const T gold_lines) if (isInputRange!T) {
348     import std.array : array;
349     import std.range : enumerate;
350     import std..string : indexOf;
351     import std.traits : isArray;
352 
353     enum ContainState {
354         NotFoundFirstLine,
355         Comparing,
356         BlockFound,
357         BlockNotFound
358     }
359 
360     ContainState state;
361 
362     auto result_lines = log;
363     size_t gold_idx, result_idx;
364 
365     while (!state.among(ContainState.BlockFound, ContainState.BlockNotFound)) {
366         string result_line;
367         // ensure it doesn't do an out-of-range indexing
368         if (result_idx < result_lines.length) {
369             result_line = result_lines[result_idx];
370         }
371 
372         switch (state) with (ContainState) {
373         case NotFoundFirstLine:
374             if (result_line.indexOf(gold_lines[0].strip) != -1) {
375                 state = Comparing;
376                 ++gold_idx;
377             } else if (result_lines.length == result_idx) {
378                 state = BlockNotFound;
379             }
380             break;
381         case Comparing:
382             if (gold_lines.length == gold_idx) {
383                 state = BlockFound;
384             } else if (result_lines.length == result_idx) {
385                 state = BlockNotFound;
386             } else if (result_line.indexOf(gold_lines[gold_idx].strip) == -1) {
387                 state = BlockNotFound;
388             } else {
389                 ++gold_idx;
390             }
391             break;
392         default:
393         }
394 
395         if (state == ContainState.BlockNotFound && result_lines.length == result_idx) {
396             yap("Error: log do not contain the reference lines");
397             yap(" Expected: " ~ gold_lines[0]);
398         } else if (state == ContainState.BlockNotFound) {
399             yap("Error: Difference from reference. Line ", gold_idx);
400             yap(" Expected: " ~ gold_lines[gold_idx]);
401             yap("   Actual: " ~ result_line);
402         }
403 
404         if (state.among(ContainState.BlockFound, ContainState.BlockNotFound)) {
405             break;
406         }
407 
408         ++result_idx;
409     }
410 
411     return state == ContainState.BlockFound;
412 }
413 
414 /// Check if the logged stdout contains the golden block.
415 ///TODO refactor function. It is unnecessarily complex.
416 bool stdoutContains(in Path gold) {
417     import std.array : array;
418     import std.range : enumerate;
419     import std.stdio : File;
420 
421     yap("Contains gold:", gold.raw);
422 
423     File goldf;
424 
425     try {
426         goldf = File(gold.escapePath);
427     }
428     catch (ErrnoException ex) {
429         yap(ex.msg);
430         return false;
431     }
432 
433     bool status = stdoutContains(goldf.byLine.array());
434 
435     if (!status) {
436         yap("Output do not contain the reference file (gold): " ~ gold.escapePath);
437         return false;
438     }
439 
440     return true;
441 }
442 
443 /** Run dextool.
444  *
445  * Return: The runtime in ms.
446  */
447 deprecated("to be removed") auto runDextool(T)(in T input,
448         const ref TestEnv testEnv, in string[] pre_args, in string[] flags) {
449     import std.traits : isArray;
450     import std.algorithm : min;
451 
452     Args args;
453     args ~= testEnv.dextool;
454     args ~= pre_args.dup;
455     args ~= "--out=" ~ testEnv.outdir.escapePath;
456 
457     static if (isArray!T) {
458         foreach (f; input) {
459             args ~= "--in=" ~ f.escapePath;
460         }
461     } else {
462         if (input.escapePath.length > 0) {
463             args ~= "--in=" ~ input.escapePath;
464         }
465     }
466 
467     if (flags.length > 0) {
468         args ~= "--";
469         args ~= flags.dup;
470     }
471 
472     import std.datetime;
473 
474     StopWatch sw;
475     sw.start;
476     auto output = runAndLog(args.data);
477     sw.stop;
478     yap("Dextool execution time was ms: " ~ sw.peek().msecs.text);
479 
480     if (output.status != 0) {
481         auto l = min(100, output.output.length);
482 
483         throw new ErrorLevelException(output.status, output.output[0 .. l].dup);
484     }
485 
486     return sw.peek.msecs;
487 }
488 
489 deprecated("to be removed") auto filesToDextoolInFlags(T)(const T in_files) {
490     Args args;
491 
492     static if (isArray!T) {
493         foreach (f; input) {
494             args ~= "--in=" ~ f.escapePath;
495         }
496     } else {
497         if (input.escapePath.length > 0) {
498             args ~= "--in=" ~ input.escapePath;
499         }
500     }
501 
502     return args;
503 }
504 
505 /** Construct an execution of dextool with needed arguments.
506  */
507 auto makeDextool(const ref TestEnv testEnv) {
508     import dextool_test.builders : BuildDextoolRun;
509 
510     return BuildDextoolRun(testEnv.dextool.escapePath, testEnv.outdir.escapePath);
511 }
512 
513 /** Construct an execution of a command.
514  */
515 auto makeCommand(string command) {
516     return BuildCommandRun(command);
517 }
518 
519 /** Construct an execution of a command.
520  */
521 auto makeCommand(const ref TestEnv testEnv, string command) {
522     return BuildCommandRun(command, testEnv.outdir.escapePath);
523 }
524 
525 auto makeCompare(const ref TestEnv env) {
526     import dextool_test.golden : BuildCompare;
527 
528     return BuildCompare(env.outdir.escapePath);
529 }
530 
531 deprecated("to be removed") void compareResult(T...)(Flag!"sortLines" sortLines,
532         Flag!"skipComments" skipComments, in T args) {
533     static assert(args.length >= 1);
534 
535     foreach (a; args) {
536         if (existsAsFile(a.gold)) {
537             compare(a.gold, a.result, sortLines, skipComments);
538         }
539     }
540 }
541 
542 string testId(uint line = __LINE__) {
543     import std.conv : to;
544 
545     // assuming it is always the UDA for a test and thus +1 to get the correct line
546     return "id:" ~ (line + 1).to!string() ~ " ";
547 }
548 
549 /**
550  * Params:
551  *  dir = directory to perform the recursive search in
552  *  ext = extension of the files to match (including dot)
553  *
554  * Returns: a list of all files with the extension
555  */
556 auto recursiveFilesWithExtension(Path dir, string ext) {
557     // dfmt off
558     return std.file.dirEntries(dir.toString, SpanMode.depth)
559         .filter!(a => a.isFile)
560         .filter!(a => extension(a.name) == ext)
561         .map!(a => Path(a));
562     // dfmt on
563 }