1 /**
2 Copyright: Copyright (c) 2020, 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 Common functionality used both by source and schemata testing of a mutant.
11 */
12 module dextool.plugin.mutate.backend.test_mutant.common;
13 
14 import logger = std.experimental.logger;
15 import std.algorithm : map, filter;
16 import std.array : empty, array;
17 import std.datetime : Duration, dur;
18 import std.exception : collectException;
19 import std.path : buildPath;
20 import std.typecons : Flag, No;
21 
22 import sumtype;
23 import proc : DrainElement;
24 
25 import dextool.plugin.mutate.backend.database : MutationId;
26 import dextool.plugin.mutate.backend.interface_;
27 import dextool.plugin.mutate.backend.test_mutant.common;
28 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport;
29 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner;
30 import dextool.plugin.mutate.backend.type : Mutation, TestCase;
31 import dextool.plugin.mutate.config;
32 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand;
33 import dextool.set;
34 import dextool.type : AbsolutePath, Path;
35 
36 version (unittest) {
37     import unit_threaded.assertions;
38 }
39 
40 @safe:
41 
42 /** Analyze stdout/stderr output from a test suite for test cases that failed
43  * (killed) a mutant, which test cases that exists and if any of them are
44  * unstable.
45  */
46 struct TestCaseAnalyzer {
47     private {
48         ShellCommand[] externalAnalysers;
49         TestCaseAnalyzeBuiltin[] builtins;
50         AutoCleanup cleanup;
51     }
52 
53     static struct Success {
54         TestCase[] failed;
55         TestCase[] found;
56     }
57 
58     static struct Unstable {
59         TestCase[] unstable;
60         TestCase[] found;
61     }
62 
63     static struct Failed {
64     }
65 
66     alias Result = SumType!(Success, Unstable, Failed);
67 
68     this(TestCaseAnalyzeBuiltin[] builtins, ShellCommand[] externalAnalyzers, AutoCleanup cleanup) {
69         this.externalAnalysers = externalAnalyzers;
70         this.builtins = builtins;
71         this.cleanup = cleanup;
72     }
73 
74     Result analyze(DrainElement[] data, Flag!"allFound" allFound = No.allFound) {
75         import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
76 
77         auto gather = new GatherTestCase;
78         // TODO: maybe destroy it too, to cleanup memory earlier? But it isn't
79         // @safe
80         //scope(exit) .destroy(gather);
81 
82         // the post processer must succeeed for the data to be stored. It is
83         // considered a major error that may corrupt existing data if it fails.
84         bool success = true;
85 
86         if (!externalAnalysers.empty) {
87             foreach (cmd; externalAnalysers) {
88                 success = success && externalProgram(cmd, data, gather, cleanup);
89             }
90         }
91         if (!builtins.empty) {
92             builtin(data, builtins, gather);
93         }
94 
95         if (!gather.unstable.empty) {
96             return Result(Unstable(gather.unstableAsArray, allFound ? gather.foundAsArray : null));
97         }
98 
99         if (success) {
100             return Result(Success(gather.failedAsArray, allFound ? gather.foundAsArray : null));
101         }
102 
103         return Result(Failed.init);
104     }
105 
106     /// Returns: true if there are no analyzers setup.
107     bool empty() @safe pure nothrow const @nogc {
108         return externalAnalysers.empty && builtins.empty;
109     }
110 }
111 
112 /** Analyze the output from the test suite with one of the builtin analyzers.
113  */
114 void builtin(DrainElement[] output,
115         const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @safe nothrow {
116     import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze;
117     import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze;
118     import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze;
119 
120     GtestParser gtest;
121     CtestParser ctest;
122     MakefileParser makefile;
123 
124     void analyzeLine(const(char)[] line) {
125         // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this?
126         immutable magic_nr = 2048;
127         if (line.length > magic_nr) {
128             // The byLine split may fail and thus result in one huge line.
129             // The result of this is that regex's that use backtracking become really slow.
130             // By skipping these lines dextool at list doesn't hang.
131             logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...",
132                     line.length, magic_nr);
133             return;
134         }
135 
136         foreach (const p; tc_analyze_builtin) {
137             final switch (p) {
138             case TestCaseAnalyzeBuiltin.gtest:
139                 gtest.process(line, app);
140                 break;
141             case TestCaseAnalyzeBuiltin.ctest:
142                 ctest.process(line, app);
143                 break;
144             case TestCaseAnalyzeBuiltin.makefile:
145                 makefile.process(line, app);
146                 break;
147             }
148         }
149     }
150 
151     foreach (l; LineRange(output)) {
152         try {
153             analyzeLine(l);
154         } catch (Exception e) {
155             logger.warning("A error encountered when trying to analyze the output from the test suite. Ignoring the offending line.")
156                 .collectException;
157             logger.warning(e.msg).collectException;
158         }
159     }
160 
161     foreach (const p; tc_analyze_builtin) {
162         final switch (p) {
163         case TestCaseAnalyzeBuiltin.gtest:
164             gtest.finalize(app);
165             break;
166         case TestCaseAnalyzeBuiltin.ctest:
167             break;
168         case TestCaseAnalyzeBuiltin.makefile:
169             break;
170         }
171     }
172 }
173 
174 struct LineRange {
175     DrainElement[] elems;
176     const(char)[] buf;
177     const(char)[] line;
178 
179     const(char)[] front() @safe pure nothrow {
180         assert(!empty, "Can't get front of an empty range");
181         return line;
182     }
183 
184     void popFront() @safe nothrow {
185         assert(!empty, "Can't pop front of an empty range");
186         import std.algorithm : countUntil;
187 
188         static auto nextLine(ref const(char)[] buf) @safe nothrow {
189             const(char)[] line;
190 
191             try {
192                 const idx = buf.countUntil('\n');
193                 if (idx != -1) {
194                     line = buf[0 .. idx];
195                     if (idx < buf.length) {
196                         buf = buf[idx + 1 .. $];
197                     } else {
198                         buf = null;
199                     }
200                 }
201             } catch (Exception e) {
202                 logger.warning(e.msg).collectException;
203                 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.")
204                     .collectException;
205                 buf = null;
206             }
207 
208             return line;
209         }
210 
211         line = null;
212         while (!elems.empty && line.empty) {
213             try {
214                 auto tmp = elems[0].byUTF8.array;
215                 buf ~= tmp;
216             } catch (Exception e) {
217                 logger.warning(e.msg).collectException;
218                 logger.warning(
219                         "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.")
220                     .collectException;
221             }
222             elems = elems[1 .. $];
223             line = nextLine(buf);
224         }
225 
226         const s = buf.length;
227         // there are data in the buffer that may contain lines
228         if (elems.empty && !buf.empty && line.empty) {
229             line = nextLine(buf);
230         }
231 
232         // the last data in the buffer. This is a special case if an
233         // application write data but do not end the last block of data with a
234         // newline.
235         // `s == buf.length` handles the case wherein there is an empty line.
236         if (elems.empty && !buf.empty && line.empty && (s == buf.length)) {
237             line = buf;
238             buf = null;
239         }
240     }
241 
242     bool empty() @safe pure nothrow const @nogc {
243         return elems.empty && buf.empty && line.empty;
244     }
245 }
246 
247 @("shall end the parsing of DrainElements even if the last is missing a newline")
248 unittest {
249     import std.algorithm : copy;
250     import std.array : appender;
251 
252     auto app = appender!(DrainElement[])();
253     ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout,
254             cast(const(ubyte)[]) a)).copy(app);
255 
256     auto r = LineRange(app.data);
257 
258     r.empty.shouldBeFalse;
259     r.popFront;
260     r.front.shouldEqual("foobar");
261 
262     r.empty.shouldBeFalse;
263     r.popFront;
264     r.front.shouldEqual("smurf");
265 
266     r.empty.shouldBeFalse;
267     r.popFront;
268     r.empty.shouldBeTrue;
269 }
270 
271 /** Run an external program that analyze the output from the test suite for
272  * test cases that failed.
273  *
274  * Params:
275  * cmd = user analyze command to execute on the output
276  * output = output from the test command to be passed on to the analyze command
277  * report = the result is stored in the report
278  *
279  * Returns: True if it successfully analyzed the output
280  */
281 bool externalProgram(ShellCommand cmd, DrainElement[] output,
282         TestCaseReport report, AutoCleanup cleanup) @safe nothrow {
283     import std.datetime : dur;
284     import std.algorithm : copy;
285     import std.ascii : newline;
286     import std.string : strip, startsWith;
287     import proc;
288 
289     immutable passed = "passed:";
290     immutable failed = "failed:";
291     immutable unstable = "unstable:";
292 
293     auto tmpdir = createTmpDir();
294     if (tmpdir.empty) {
295         return false;
296     }
297 
298     ShellCommand writeOutput(ShellCommand cmd) @safe {
299         import std.stdio : File;
300 
301         const stdoutPath = buildPath(tmpdir, "stdout.log");
302         const stderrPath = buildPath(tmpdir, "stderr.log");
303         auto stdout = File(stdoutPath, "w");
304         auto stderr = File(stderrPath, "w");
305 
306         foreach (a; output) {
307             final switch (a.type) {
308             case DrainElement.Type.stdout:
309                 stdout.rawWrite(a.data);
310                 break;
311             case DrainElement.Type.stderr:
312                 stderr.rawWrite(a.data);
313                 break;
314             }
315         }
316 
317         cmd.value ~= [stdoutPath, stderrPath];
318         return cmd;
319     }
320 
321     try {
322         cleanup.add(tmpdir.Path.AbsolutePath);
323         cmd = writeOutput(cmd);
324         auto p = pipeProcess(cmd.value).sandbox.scopeKill;
325         foreach (l; p.process.drainByLineCopy(200.dur!"msecs").map!(a => a.strip)
326                 .filter!(a => !a.empty)) {
327             if (l.startsWith(passed))
328                 report.reportFound(TestCase(l[passed.length .. $].strip.idup));
329             else if (l.startsWith(failed))
330                 report.reportFailed(TestCase(l[failed.length .. $].strip.idup));
331             else if (l.startsWith(unstable))
332                 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup));
333         }
334 
335         if (p.wait == 0) {
336             return true;
337         }
338 
339         logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd);
340     } catch (Exception e) {
341         logger.warning(e.msg).collectException;
342     }
343 
344     return false;
345 }
346 
347 /// Returns: path to a tmp directory or null on failure.
348 string createTmpDir() @safe nothrow {
349     import std.random : uniform;
350     import std.format : format;
351     import std.file : mkdir;
352 
353     string test_tmp_output;
354 
355     // try 5 times or bailout
356     foreach (const _; 0 .. 5) {
357         try {
358             auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong);
359             mkdir(tmp);
360             test_tmp_output = AbsolutePath(Path(tmp));
361             break;
362         } catch (Exception e) {
363             logger.warning(e.msg).collectException;
364         }
365     }
366 
367     if (test_tmp_output.length == 0) {
368         logger.warning("Unable to create a temporary directory to store stdout/stderr in")
369             .collectException;
370     }
371 
372     return test_tmp_output;
373 }
374 
375 /** Paths stored will be removed automatically either when manually called or
376  * goes out of scope.
377  */
378 class AutoCleanup {
379     private string[] remove_dirs;
380 
381     void add(AbsolutePath p) @safe nothrow {
382         remove_dirs ~= cast(string) p;
383     }
384 
385     // trusted: the paths are forced to be valid paths.
386     void cleanup() @trusted nothrow {
387         import std.file : rmdirRecurse, exists;
388 
389         foreach (ref p; remove_dirs.filter!(a => !a.empty)) {
390             try {
391                 if (exists(p))
392                     rmdirRecurse(p);
393                 if (!exists(p))
394                     p = null;
395             } catch (Exception e) {
396                 logger.info(e.msg).collectException;
397             }
398         }
399 
400         remove_dirs = remove_dirs.filter!(a => !a.empty).array;
401     }
402 }
403 
404 alias CompileResult = SumType!(Mutation.Status, bool);
405 
406 CompileResult compile(ShellCommand cmd, bool printToStdout = false) nothrow {
407     import proc;
408     import std.stdio : write;
409 
410     try {
411         auto p = pipeProcess(cmd.value).sandbox.scopeKill;
412         foreach (a; p.process.drain(200.dur!"msecs")) {
413             if (!a.empty && printToStdout) {
414                 write(a.byUTF8);
415             }
416         }
417         if (p.wait != 0) {
418             return CompileResult(Mutation.Status.killedByCompiler);
419         }
420     } catch (Exception e) {
421         logger.warning("Unknown error when executing the build command").collectException;
422         logger.warning(e.msg).collectException;
423         return CompileResult(Mutation.Status.unknown);
424     }
425 
426     return CompileResult(true);
427 }
428 
429 /** Run the test suite to verify a mutation.
430  *
431  * Params:
432  *  compile_p = compile command
433  *  tester_p = test command
434  *  timeout = kill the test command and mark mutant as timeout if the runtime exceed this value.
435  *  fio = i/o
436  *
437  * Returns: the result of testing the mutant.
438  */
439 auto runTester(ref TestRunner runner) nothrow {
440     import proc;
441 
442     struct Rval {
443         Mutation.Status status;
444         DrainElement[] output;
445     }
446 
447     Rval rval;
448     try {
449         auto res = runner.run;
450         rval.output = res.output;
451 
452         final switch (res.status) with (TestResult.Status) {
453         case passed:
454             rval.status = Mutation.Status.alive;
455             break;
456         case failed:
457             rval.status = Mutation.Status.killed;
458             break;
459         case timeout:
460             rval.status = Mutation.Status.timeout;
461             break;
462         case error:
463             rval.status = Mutation.Status.unknown;
464             break;
465         }
466     } catch (Exception e) {
467         // unable to for example execute the test suite
468         logger.warning(e.msg).collectException;
469         rval.status = Mutation.Status.unknown;
470     }
471 
472     return rval;
473 }