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 process : 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 
162 struct LineRange {
163     DrainElement[] elems;
164     const(char)[] buf;
165     const(char)[] line;
166 
167     const(char)[] front() @safe pure nothrow {
168         assert(!empty, "Can't get front of an empty range");
169         return line;
170     }
171 
172     void popFront() @safe nothrow {
173         assert(!empty, "Can't pop front of an empty range");
174         import std.algorithm : countUntil;
175 
176         static auto nextLine(ref const(char)[] buf) @safe nothrow {
177             const(char)[] line;
178 
179             try {
180                 const idx = buf.countUntil('\n');
181                 if (idx != -1) {
182                     line = buf[0 .. idx];
183                     if (idx < buf.length) {
184                         buf = buf[idx + 1 .. $];
185                     } else {
186                         buf = null;
187                     }
188                 }
189             } catch (Exception e) {
190                 logger.warning(e.msg).collectException;
191                 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.")
192                     .collectException;
193                 buf = null;
194             }
195 
196             return line;
197         }
198 
199         line = null;
200         while (!elems.empty && line.empty) {
201             try {
202                 auto tmp = elems[0].byUTF8.array;
203                 buf ~= tmp;
204             } catch (Exception e) {
205                 logger.warning(e.msg).collectException;
206                 logger.warning(
207                         "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.")
208                     .collectException;
209             }
210             elems = elems[1 .. $];
211             line = nextLine(buf);
212         }
213 
214         const s = buf.length;
215         // there are data in the buffer that may contain lines
216         if (elems.empty && !buf.empty && line.empty) {
217             line = nextLine(buf);
218         }
219 
220         // the last data in the buffer. This is a special case if an
221         // application write data but do not end the last block of data with a
222         // newline.
223         // `s == buf.length` handles the case wherein there is an empty line.
224         if (elems.empty && !buf.empty && line.empty && (s == buf.length)) {
225             line = buf;
226             buf = null;
227         }
228     }
229 
230     bool empty() @safe pure nothrow const @nogc {
231         return elems.empty && buf.empty && line.empty;
232     }
233 }
234 
235 @("shall end the parsing of DrainElements even if the last is missing a newline")
236 unittest {
237     import std.algorithm : copy;
238     import std.array : appender;
239 
240     auto app = appender!(DrainElement[])();
241     ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout,
242             cast(const(ubyte)[]) a)).copy(app);
243 
244     auto r = LineRange(app.data);
245 
246     r.empty.shouldBeFalse;
247     r.popFront;
248     r.front.shouldEqual("foobar");
249 
250     r.empty.shouldBeFalse;
251     r.popFront;
252     r.front.shouldEqual("smurf");
253 
254     r.empty.shouldBeFalse;
255     r.popFront;
256     r.empty.shouldBeTrue;
257 }
258 
259 /** Run an external program that analyze the output from the test suite for
260  * test cases that failed.
261  *
262  * Params:
263  * cmd = user analyze command to execute on the output
264  * output = output from the test command to be passed on to the analyze command
265  * report = the result is stored in the report
266  *
267  * Returns: True if it successfully analyzed the output
268  */
269 bool externalProgram(ShellCommand cmd, DrainElement[] output,
270         TestCaseReport report, AutoCleanup cleanup) @safe nothrow {
271     import std.datetime : dur;
272     import std.algorithm : copy;
273     import std.ascii : newline;
274     import std.string : strip, startsWith;
275     import process;
276 
277     immutable passed = "passed:";
278     immutable failed = "failed:";
279     immutable unstable = "unstable:";
280 
281     auto tmpdir = createTmpDir();
282     if (tmpdir.empty) {
283         return false;
284     }
285 
286     ShellCommand writeOutput(ShellCommand cmd) @safe {
287         import std.stdio : File;
288 
289         const stdoutPath = buildPath(tmpdir, "stdout.log");
290         const stderrPath = buildPath(tmpdir, "stderr.log");
291         auto stdout = File(stdoutPath, "w");
292         auto stderr = File(stderrPath, "w");
293 
294         foreach (a; output) {
295             final switch (a.type) {
296             case DrainElement.Type.stdout:
297                 stdout.write(a.data);
298                 break;
299             case DrainElement.Type.stderr:
300                 stderr.write(a.data);
301                 break;
302             }
303         }
304 
305         cmd.value ~= [stdoutPath, stderrPath];
306         return cmd;
307     }
308 
309     try {
310         cleanup.add(tmpdir.Path.AbsolutePath);
311         cmd = writeOutput(cmd);
312         auto p = pipeProcess(cmd.value).sandbox.scopeKill;
313         foreach (l; p.process.drainByLineCopy(200.dur!"msecs").map!(a => a.strip)
314                 .filter!(a => !a.empty)) {
315             if (l.startsWith(passed))
316                 report.reportFound(TestCase(l[passed.length .. $].strip.idup));
317             else if (l.startsWith(failed))
318                 report.reportFailed(TestCase(l[failed.length .. $].strip.idup));
319             else if (l.startsWith(unstable))
320                 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup));
321         }
322 
323         if (p.wait == 0) {
324             return true;
325         }
326 
327         logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd);
328     } catch (Exception e) {
329         logger.warning(e.msg).collectException;
330     }
331 
332     return false;
333 }
334 
335 /// Returns: path to a tmp directory or null on failure.
336 string createTmpDir() @safe nothrow {
337     import std.random : uniform;
338     import std.format : format;
339     import std.file : mkdir;
340 
341     string test_tmp_output;
342 
343     // try 5 times or bailout
344     foreach (const _; 0 .. 5) {
345         try {
346             auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong);
347             mkdir(tmp);
348             test_tmp_output = AbsolutePath(Path(tmp));
349             break;
350         } catch (Exception e) {
351             logger.warning(e.msg).collectException;
352         }
353     }
354 
355     if (test_tmp_output.length == 0) {
356         logger.warning("Unable to create a temporary directory to store stdout/stderr in")
357             .collectException;
358     }
359 
360     return test_tmp_output;
361 }
362 
363 /** Paths stored will be removed automatically either when manually called or
364  * goes out of scope.
365  */
366 class AutoCleanup {
367     private string[] remove_dirs;
368 
369     void add(AbsolutePath p) @safe nothrow {
370         remove_dirs ~= cast(string) p;
371     }
372 
373     // trusted: the paths are forced to be valid paths.
374     void cleanup() @trusted nothrow {
375         import std.file : rmdirRecurse, exists;
376 
377         foreach (ref p; remove_dirs.filter!(a => !a.empty)) {
378             try {
379                 if (exists(p))
380                     rmdirRecurse(p);
381                 if (!exists(p))
382                     p = null;
383             } catch (Exception e) {
384                 logger.info(e.msg).collectException;
385             }
386         }
387 
388         remove_dirs = remove_dirs.filter!(a => !a.empty).array;
389     }
390 }
391 
392 alias CompileResult = SumType!(Mutation.Status, bool);
393 
394 CompileResult compile(ShellCommand cmd) nothrow {
395     import process;
396 
397     try {
398         auto p = pipeProcess(cmd.value).sandbox.drainToNull(200.dur!"msecs").scopeKill;
399         if (p.wait != 0) {
400             return CompileResult(Mutation.Status.killedByCompiler);
401         }
402     } catch (Exception e) {
403         logger.warning("Unknown error when executing the build command").collectException;
404         logger.warning(e.msg).collectException;
405         return CompileResult(Mutation.Status.unknown);
406     }
407 
408     return CompileResult(true);
409 }
410 
411 /** Run the test suite to verify a mutation.
412  *
413  * Params:
414  *  compile_p = compile command
415  *  tester_p = test command
416  *  timeout = kill the test command and mark mutant as timeout if the runtime exceed this value.
417  *  fio = i/o
418  *
419  * Returns: the result of testing the mutant.
420  */
421 auto runTester(ref TestRunner runner) nothrow {
422     import process;
423 
424     struct Rval {
425         Mutation.Status status;
426         DrainElement[] output;
427     }
428 
429     Rval rval;
430     try {
431         auto res = runner.run;
432         rval.output = res.output;
433 
434         final switch (res.status) with (TestResult.Status) {
435         case passed:
436             rval.status = Mutation.Status.alive;
437             break;
438         case failed:
439             rval.status = Mutation.Status.killed;
440             break;
441         case timeout:
442             rval.status = Mutation.Status.timeout;
443             break;
444         case error:
445             rval.status = Mutation.Status.unknown;
446             break;
447         }
448     } catch (Exception e) {
449         // unable to for example execute the test suite
450         logger.warning(e.msg).collectException;
451         rval.status = Mutation.Status.unknown;
452     }
453 
454     return rval;
455 }