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