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 blob_model : Blob;
23 import my.named_type;
24 import proc : DrainElement;
25 import sumtype;
26 
27 import dextool.plugin.mutate.backend.database : MutationId;
28 import dextool.plugin.mutate.backend.interface_;
29 import dextool.plugin.mutate.backend.test_mutant.test_case_analyze : GatherTestCase;
30 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner;
31 import dextool.plugin.mutate.config;
32 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand;
33 import dextool.type : AbsolutePath, Path;
34 
35 public import dextool.plugin.mutate.backend.type : Mutation, TestCase,
36     ExitStatus, MutantTimeProfile;
37 
38 version (unittest) {
39     import unit_threaded.assertions;
40 }
41 
42 @safe:
43 
44 /// The result of running the test suite on one mutant.
45 struct MutationTestResult {
46     import std.datetime : Duration;
47     import dextool.plugin.mutate.backend.database : MutationStatusId, MutationId;
48     import dextool.plugin.mutate.backend.type : Mutation, TestCase, ExitStatus;
49 
50     MutationId mutId;
51     MutationStatusId id;
52     Mutation.Status status;
53     MutantTimeProfile profile;
54     TestCase[] testCases;
55     ExitStatus exitStatus;
56 }
57 
58 /** Analyze stdout/stderr output from a test suite for test cases that failed
59  * (killed) a mutant, which test cases that exists and if any of them are
60  * unstable.
61  */
62 struct TestCaseAnalyzer {
63     private {
64         ShellCommand[] externalAnalysers;
65         TestCaseAnalyzeBuiltin[] builtins;
66         AutoCleanup cleanup;
67     }
68 
69     static struct Success {
70         TestCase[] failed;
71         TestCase[] found;
72     }
73 
74     static struct Unstable {
75         TestCase[] unstable;
76         TestCase[] found;
77     }
78 
79     static struct Failed {
80     }
81 
82     alias Result = SumType!(Success, Unstable, Failed);
83 
84     this(TestCaseAnalyzeBuiltin[] builtins, ShellCommand[] externalAnalyzers, AutoCleanup cleanup) {
85         this.externalAnalysers = externalAnalyzers;
86         this.builtins = builtins;
87         this.cleanup = cleanup;
88     }
89 
90     Result analyze(DrainElement[] data, Flag!"allFound" allFound = No.allFound) {
91         import dextool.plugin.mutate.backend.test_mutant.test_case_analyze : GatherTestCase;
92 
93         GatherTestCase gather;
94 
95         // the post processer must succeeed for the data to be stored. It is
96         // considered a major error that may corrupt existing data if it fails.
97         bool success = true;
98 
99         if (!externalAnalysers.empty) {
100             foreach (cmd; externalAnalysers) {
101                 success = success && externalProgram(cmd, data, gather, cleanup);
102             }
103         }
104         if (!builtins.empty) {
105             builtin(data, builtins, gather);
106         }
107 
108         if (!gather.unstable.empty) {
109             return Result(Unstable(gather.unstableAsArray, allFound ? gather.foundAsArray : null));
110         }
111 
112         if (success) {
113             return Result(Success(gather.failedAsArray, allFound ? gather.foundAsArray : null));
114         }
115 
116         return Result(Failed.init);
117     }
118 
119     /// Returns: true if there are no analyzers setup.
120     bool empty() @safe pure nothrow const @nogc {
121         return externalAnalysers.empty && builtins.empty;
122     }
123 }
124 
125 /** Analyze the output from the test suite with one of the builtin analyzers.
126  */
127 void builtin(DrainElement[] output,
128         const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, ref GatherTestCase app) @safe nothrow {
129     import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze;
130     import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze;
131     import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze;
132 
133     GtestParser gtest;
134     CtestParser ctest;
135     MakefileParser makefile;
136 
137     void analyzeLine(const(char)[] line) {
138         // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this?
139         immutable magic_nr = 2048;
140         if (line.length > magic_nr) {
141             // The byLine split may fail and thus result in one huge line.
142             // The result of this is that regex's that use backtracking become really slow.
143             // By skipping these lines dextool at list doesn't hang.
144             logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...",
145                     line.length, magic_nr);
146             return;
147         }
148 
149         foreach (const p; tc_analyze_builtin) {
150             final switch (p) {
151             case TestCaseAnalyzeBuiltin.gtest:
152                 gtest.process(line, app);
153                 break;
154             case TestCaseAnalyzeBuiltin.ctest:
155                 ctest.process(line, app);
156                 break;
157             case TestCaseAnalyzeBuiltin.makefile:
158                 makefile.process(line, app);
159                 break;
160             }
161         }
162     }
163 
164     foreach (l; LineRange(output)) {
165         try {
166             analyzeLine(l);
167         } catch (Exception e) {
168             logger.warning("A error encountered when trying to analyze the output from the test suite. Ignoring the offending line.")
169                 .collectException;
170             logger.warning(e.msg).collectException;
171         }
172     }
173 
174     foreach (const p; tc_analyze_builtin) {
175         final switch (p) {
176         case TestCaseAnalyzeBuiltin.gtest:
177             gtest.finalize(app);
178             break;
179         case TestCaseAnalyzeBuiltin.ctest:
180             break;
181         case TestCaseAnalyzeBuiltin.makefile:
182             break;
183         }
184     }
185 }
186 
187 struct LineRange {
188     DrainElement[] elems;
189     const(char)[] buf;
190     const(char)[] line;
191 
192     const(char)[] front() @safe pure nothrow {
193         assert(!empty, "Can't get front of an empty range");
194         return line;
195     }
196 
197     void popFront() @safe nothrow {
198         assert(!empty, "Can't pop front of an empty range");
199         import std.algorithm : countUntil;
200 
201         static auto nextLine(ref const(char)[] buf) @safe nothrow {
202             const(char)[] line;
203 
204             try {
205                 const idx = buf.countUntil('\n');
206                 if (idx != -1) {
207                     line = buf[0 .. idx];
208                     if (idx < buf.length) {
209                         buf = buf[idx + 1 .. $];
210                     } else {
211                         buf = null;
212                     }
213                 }
214             } catch (Exception e) {
215                 logger.warning(e.msg).collectException;
216                 logger.warning("Unable to parse the buffered data for a newline. Ignoring the rest.")
217                     .collectException;
218                 buf = null;
219             }
220 
221             return line;
222         }
223 
224         line = null;
225         while (!elems.empty && line.empty) {
226             try {
227                 auto tmp = elems[0].byUTF8.array;
228                 buf ~= tmp;
229             } catch (Exception e) {
230                 logger.warning(e.msg).collectException;
231                 logger.warning(
232                         "A error encountered when trying to parse the output as UTF-8. Ignoring the offending data.")
233                     .collectException;
234             }
235             elems = elems[1 .. $];
236             line = nextLine(buf);
237         }
238 
239         const s = buf.length;
240         // there are data in the buffer that may contain lines
241         if (elems.empty && !buf.empty && line.empty) {
242             line = nextLine(buf);
243         }
244 
245         // the last data in the buffer. This is a special case if an
246         // application write data but do not end the last block of data with a
247         // newline.
248         // `s == buf.length` handles the case wherein there is an empty line.
249         if (elems.empty && !buf.empty && line.empty && (s == buf.length)) {
250             line = buf;
251             buf = null;
252         }
253     }
254 
255     bool empty() @safe pure nothrow const @nogc {
256         return elems.empty && buf.empty && line.empty;
257     }
258 }
259 
260 @("shall end the parsing of DrainElements even if the last is missing a newline")
261 unittest {
262     import std.algorithm : copy;
263     import std.array : appender;
264 
265     auto app = appender!(DrainElement[])();
266     ["foo", "bar\n", "smurf"].map!(a => DrainElement(DrainElement.Type.stdout,
267             cast(const(ubyte)[]) a)).copy(app);
268 
269     auto r = LineRange(app.data);
270 
271     r.empty.shouldBeFalse;
272     r.popFront;
273     r.front.shouldEqual("foobar");
274 
275     r.empty.shouldBeFalse;
276     r.popFront;
277     r.front.shouldEqual("smurf");
278 
279     r.empty.shouldBeFalse;
280     r.popFront;
281     r.empty.shouldBeTrue;
282 }
283 
284 /** Run an external program that analyze the output from the test suite for
285  * test cases that failed.
286  *
287  * Params:
288  * cmd = user analyze command to execute on the output
289  * output = output from the test command to be passed on to the analyze command
290  * report = the result is stored in the report
291  *
292  * Returns: True if it successfully analyzed the output
293  */
294 bool externalProgram(ShellCommand cmd, DrainElement[] output,
295         ref GatherTestCase report, AutoCleanup cleanup) @safe nothrow {
296     import std.datetime : dur;
297     import std.algorithm : copy;
298     import std.ascii : newline;
299     import std.string : strip, startsWith;
300     import proc;
301 
302     immutable passed = "passed:";
303     immutable failed = "failed:";
304     immutable unstable = "unstable:";
305 
306     auto tmpdir = createTmpDir();
307     if (tmpdir.empty) {
308         return false;
309     }
310 
311     ShellCommand writeOutput(ShellCommand cmd) @safe {
312         import std.stdio : File;
313 
314         const stdoutPath = buildPath(tmpdir, "stdout.log");
315         const stderrPath = buildPath(tmpdir, "stderr.log");
316         auto stdout = File(stdoutPath, "w");
317         auto stderr = File(stderrPath, "w");
318 
319         foreach (a; output) {
320             final switch (a.type) {
321             case DrainElement.Type.stdout:
322                 stdout.rawWrite(a.data);
323                 break;
324             case DrainElement.Type.stderr:
325                 stderr.rawWrite(a.data);
326                 break;
327             }
328         }
329 
330         cmd.value ~= [stdoutPath, stderrPath];
331         return cmd;
332     }
333 
334     try {
335         cleanup.add(tmpdir.Path.AbsolutePath);
336         cmd = writeOutput(cmd);
337         auto p = pipeProcess(cmd.value).sandbox.rcKill;
338         foreach (l; p.process.drainByLineCopy().map!(a => a.strip)
339                 .filter!(a => !a.empty)) {
340             if (l.startsWith(passed))
341                 report.reportFound(TestCase(l[passed.length .. $].strip.idup));
342             else if (l.startsWith(failed))
343                 report.reportFailed(TestCase(l[failed.length .. $].strip.idup));
344             else if (l.startsWith(unstable))
345                 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup));
346         }
347 
348         if (p.wait == 0) {
349             return true;
350         }
351 
352         logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd);
353     } catch (Exception e) {
354         logger.warning(e.msg).collectException;
355     }
356 
357     return false;
358 }
359 
360 /// Returns: path to a tmp directory or null on failure.
361 string createTmpDir() @safe nothrow {
362     import std.random : uniform;
363     import std.format : format;
364     import std.file : mkdir;
365 
366     string test_tmp_output;
367 
368     // try 5 times or bailout
369     foreach (const _; 0 .. 5) {
370         try {
371             auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong);
372             mkdir(tmp);
373             test_tmp_output = AbsolutePath(Path(tmp));
374             break;
375         } catch (Exception e) {
376             logger.warning(e.msg).collectException;
377         }
378     }
379 
380     if (test_tmp_output.length == 0) {
381         logger.warning("Unable to create a temporary directory to store stdout/stderr in")
382             .collectException;
383     }
384 
385     return test_tmp_output;
386 }
387 
388 /** Paths stored will be removed automatically either when manually called or
389  * goes out of scope.
390  */
391 class AutoCleanup {
392     private string[] remove_dirs;
393 
394     void add(AbsolutePath p) @safe nothrow {
395         remove_dirs ~= cast(string) p;
396     }
397 
398     // trusted: the paths are forced to be valid paths.
399     void cleanup() @trusted nothrow {
400         import std.file : rmdirRecurse, exists;
401 
402         foreach (ref p; remove_dirs.filter!(a => !a.empty)) {
403             try {
404                 if (exists(p))
405                     rmdirRecurse(p);
406                 if (!exists(p))
407                     p = null;
408             } catch (Exception e) {
409                 logger.info(e.msg).collectException;
410             }
411         }
412 
413         remove_dirs = remove_dirs.filter!(a => !a.empty).array;
414     }
415 }
416 
417 alias CompileResult = SumType!(Mutation.Status, bool);
418 
419 CompileResult compile(ShellCommand cmd, Duration timeout, bool printToStdout = false) @trusted nothrow {
420     import proc;
421     import std.stdio : write;
422 
423     try {
424         auto p = () {
425             if (cmd.value.length == 1) {
426                 return pipeShell(cmd.value[0]).sandbox.timeout(timeout).rcKill;
427             }
428             return pipeProcess(cmd.value).sandbox.timeout(timeout).rcKill;
429         }();
430         foreach (a; p.process.drain) {
431             if (!a.empty && printToStdout) {
432                 write(a.byUTF8);
433             }
434         }
435         if (p.wait != 0) {
436             return CompileResult(Mutation.Status.killedByCompiler);
437         }
438     } catch (Exception e) {
439         logger.warning("Unknown error when executing the build command").collectException;
440         logger.warning(cmd.value).collectException;
441         logger.warning(e.msg).collectException;
442         return CompileResult(Mutation.Status.unknown);
443     }
444 
445     return CompileResult(true);
446 }
447 
448 struct TestResult {
449     Mutation.Status status;
450     DrainElement[] output;
451     ExitStatus exitStatus;
452     ShellCommand[] testCmds;
453 }
454 
455 /** Run the test suite to verify a mutation.
456  *
457  * Params:
458  *  compile_p = compile command
459  *  tester_p = test command
460  *  timeout = kill the test command and mark mutant as timeout if the runtime exceed this value.
461  *  fio = i/o
462  *
463  * Returns: the result of testing the mutant.
464  */
465 TestResult runTester(ref TestRunner runner) nothrow {
466     import proc;
467 
468     TestResult rval;
469     try {
470         auto res = runner.run;
471         rval.output = res.output;
472         rval.exitStatus = res.exitStatus;
473         rval.testCmds = res.testCmds;
474 
475         final switch (res.status) with (
476             dextool.plugin.mutate.backend.test_mutant.test_cmd_runner.TestResult.Status) {
477         case passed:
478             rval.status = Mutation.Status.alive;
479             break;
480         case failed:
481             rval.status = Mutation.Status.killed;
482             break;
483         case timeout:
484             rval.status = Mutation.Status.timeout;
485             break;
486         case error:
487             rval.status = Mutation.Status.unknown;
488             break;
489         }
490     } catch (Exception e) {
491         // unable to for example execute the test suite
492         logger.warning(e.msg).collectException;
493         rval.status = Mutation.Status.unknown;
494     }
495 
496     return rval;
497 }
498 
499 void restoreFiles(AbsolutePath[] files, FilesysIO fio) {
500     foreach (a; files) {
501         fio.makeOutput(a).write(fio.makeInput(a));
502     }
503 }
504 
505 /// The conditions for when to stop mutation testing.
506 /// Intended to be re-used by both the main FSM and the sub-FSMs.
507 struct TestStopCheck {
508     import std.format : format;
509     import std.datetime.systime : Clock, SysTime;
510     import my.optional;
511     import dextool.plugin.mutate.config : ConfigMutationTest;
512 
513     enum HaltReason {
514         none,
515         maxRuntime,
516         aliveTested,
517         overloaded,
518     }
519 
520     private {
521         typeof(ConfigMutationTest.loadBehavior) loadBehavior;
522         typeof(ConfigMutationTest.loadThreshold) baseLoadThreshold;
523         typeof(ConfigMutationTest.loadThreshold) loadThreshold;
524 
525         Optional!int maxAlive;
526 
527         /// Max time to run the mutation testing for.
528         SysTime stopAt;
529         Duration maxRuntime;
530 
531         long aliveMutants_;
532     }
533 
534     this(ConfigMutationTest conf) {
535         loadBehavior = conf.loadBehavior;
536         loadThreshold = conf.loadThreshold;
537         baseLoadThreshold = conf.loadThreshold;
538         if (!conf.maxAlive.isNull)
539             maxAlive = some(conf.maxAlive.get);
540         stopAt = Clock.currTime + conf.maxRuntime;
541         maxRuntime = conf.maxRuntime;
542     }
543 
544     void incrAliveMutants() @safe pure nothrow @nogc {
545         ++aliveMutants_;
546     }
547 
548     long aliveMutants() @safe pure nothrow const @nogc {
549         return aliveMutants_;
550     }
551 
552     /// A halt conditions has occured. Mutation testing should stop.
553     HaltReason isHalt() @safe nothrow {
554         if (isMaxRuntime)
555             return HaltReason.maxRuntime;
556 
557         if (isAliveTested)
558             return HaltReason.aliveTested;
559 
560         if (loadBehavior == ConfigMutationTest.LoadBehavior.halt && load15 > baseLoadThreshold.get)
561             return HaltReason.overloaded;
562 
563         return HaltReason.none;
564     }
565 
566     /// The system is overloaded and the user has configured the tool to slowdown.
567     bool isOverloaded() @safe nothrow const @nogc {
568         if (loadBehavior == ConfigMutationTest.LoadBehavior.slowdown && load15 > loadThreshold.get)
569             return true;
570 
571         return false;
572     }
573 
574     bool isAliveTested() @safe pure nothrow @nogc {
575         return maxAlive.hasValue && aliveMutants_ >= maxAlive.orElse(0);
576     }
577 
578     bool isMaxRuntime() @safe nothrow const {
579         return Clock.currTime > stopAt;
580     }
581 
582     double load15() nothrow const @nogc @trusted {
583         import my.libc : getloadavg;
584 
585         double[3] load;
586         const nr = getloadavg(&load[0], 3);
587         if (nr <= 0 || nr > load.length) {
588             return 0.0;
589         }
590         return load[nr - 1];
591     };
592 
593     /// Pause the current thread by sleeping.
594     void pause() @trusted nothrow {
595         import core.thread : Thread;
596         import std.algorithm : max;
597 
598         const sleepFor = 30.dur!"seconds";
599         logger.infof("Sleeping %s", sleepFor).collectException;
600         Thread.sleep(sleepFor);
601 
602         // make it more sensitive if the system is still overloaded.
603         if (load15 > loadThreshold.get)
604             loadThreshold.get = max(1, baseLoadThreshold.get - 1);
605         else
606             loadThreshold = baseLoadThreshold;
607     }
608 
609     string overloadToString() @safe const {
610         return format!"Detected overload (%s > %s)"(load15, loadThreshold.get);
611     }
612 
613     string maxRuntimeToString() @safe const {
614         return format!"Max runtime of %s reached at %s"(maxRuntime, Clock.currTime);
615     }
616 }