1 /**
2 Copyright: Copyright (c) 2017, 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 module dextool.plugin.mutate.backend.test_mutant;
11 
12 import core.thread : Thread;
13 import core.time : Duration, dur;
14 import logger = std.experimental.logger;
15 import std.algorithm : sort, map, splitter, filter;
16 import std.array : empty, array, appender;
17 import std.datetime : SysTime, Clock;
18 import std.exception : collectException;
19 import std.path : buildPath;
20 import std.typecons : Nullable, NullableRef, nullableRef, Tuple;
21 
22 import blob_model : Blob, Uri;
23 import process : DrainElement;
24 import sumtype;
25 
26 import dextool.fsm : Fsm, next, act, get, TypeDataMap;
27 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
28     NextMutationEntry, spinSql, MutantTimeoutCtx, MutationId;
29 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
30 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport;
31 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner;
32 import dextool.plugin.mutate.backend.type : Mutation, TestCase;
33 import dextool.plugin.mutate.config;
34 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand;
35 import dextool.set;
36 import dextool.type : AbsolutePath, ExitStatusType, FileName, DirName, Path;
37 
38 @safe:
39 
40 auto makeTestMutant() {
41     return BuildTestMutant();
42 }
43 
44 private:
45 
46 struct BuildTestMutant {
47 @safe:
48 nothrow:
49 
50     import dextool.plugin.mutate.type : MutationKind;
51 
52     private struct InternalData {
53         Mutation.Kind[] mut_kinds;
54         FilesysIO filesys_io;
55         ConfigMutationTest config;
56     }
57 
58     private InternalData data;
59 
60     auto config(ConfigMutationTest c) {
61         data.config = c;
62         return this;
63     }
64 
65     auto mutations(MutationKind[] v) {
66         import dextool.plugin.mutate.backend.utility : toInternal;
67 
68         data.mut_kinds = toInternal(v);
69         return this;
70     }
71 
72     ExitStatusType run(ref Database db, FilesysIO fio) nothrow {
73         // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope
74         auto db_ref = () @trusted { return nullableRef(&db); }();
75 
76         auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config);
77 
78         try {
79             auto test_driver = TestDriver(driver_data);
80 
81             while (test_driver.isRunning) {
82                 test_driver.execute;
83             }
84 
85             return test_driver.status;
86         } catch (Exception e) {
87             logger.error(e.msg).collectException;
88         }
89 
90         return ExitStatusType.Errors;
91     }
92 }
93 
94 struct DriverData {
95     NullableRef!Database db;
96     FilesysIO filesysIO;
97     Mutation.Kind[] mutKind;
98     AutoCleanup autoCleanup;
99     ConfigMutationTest conf;
100 }
101 
102 /** Run the test suite to verify a mutation.
103  *
104  * Params:
105  *  compile_p = compile command
106  *  tester_p = test command
107  *  timeout = kill the test command and mark mutant as timeout if the runtime exceed this value.
108  *  fio = i/o
109  *
110  * Returns: the result of testing the mutant.
111  */
112 auto runTester(ShellCommand compile_p, ref TestRunner runner) nothrow {
113     import process;
114 
115     struct Rval {
116         Mutation.Status status;
117         DrainElement[] output;
118     }
119 
120     try {
121         auto p = pipeProcess(compile_p.value).sandbox.drainToNull.scopeKill;
122         if (p.wait != 0) {
123             return Rval(Mutation.Status.killedByCompiler);
124         }
125     } catch (Exception e) {
126         logger.warning("Unknown error when executing the build command").collectException;
127         logger.warning(e.msg).collectException;
128         return Rval(Mutation.Status.unknown);
129     }
130 
131     Rval rval;
132     try {
133         auto res = runner.run;
134         rval.output = res.output;
135 
136         final switch (res.status) with (TestResult.Status) {
137         case passed:
138             rval.status = Mutation.Status.alive;
139             break;
140         case failed:
141             rval.status = Mutation.Status.killed;
142             break;
143         case timeout:
144             rval.status = Mutation.Status.timeout;
145             break;
146         case error:
147             rval.status = Mutation.Status.unknown;
148             break;
149         }
150     } catch (Exception e) {
151         // unable to for example execute the test suite
152         logger.warning(e.msg).collectException;
153         rval.status = Mutation.Status.unknown;
154     }
155 
156     return rval;
157 }
158 
159 struct MeasureTestDurationResult {
160     bool ok;
161     Duration runtime;
162 }
163 
164 /** Measure the time it takes to run the test command.
165  *
166  * The runtime is the lowest of three executions. Anything else is assumed to
167  * be variations in the system.
168  *
169  * If the tests fail (exit code isn't 0) any time then they are too unreliable
170  * to use for mutation testing.
171  *
172  * Params:
173  *  cmd = test command to measure
174  */
175 MeasureTestDurationResult measureTestCommand(ref TestRunner runner) @safe nothrow {
176     import std.algorithm : min;
177     import std.datetime.stopwatch : StopWatch, AutoStart;
178     import process;
179 
180     if (runner.empty) {
181         collectException(logger.error("No test command(s) specified (--test-cmd)"));
182         return MeasureTestDurationResult(false);
183     }
184 
185     static struct Rval {
186         TestResult result;
187         Duration runtime;
188     }
189 
190     auto runTest() @safe {
191         auto sw = StopWatch(AutoStart.yes);
192         auto res = runner.run;
193         return Rval(res, sw.peek);
194     }
195 
196     static void print(DrainElement[] data) @trusted {
197         import std.stdio : stdout, write;
198 
199         foreach (l; data) {
200             write(l.byUTF8);
201         }
202         stdout.flush;
203     }
204 
205     auto runtime = Duration.max;
206     bool failed;
207     for (int i; i < 3 && !failed; ++i) {
208         try {
209             auto res = runTest;
210             final switch (res.result.status) with (TestResult) {
211             case Status.passed:
212                 runtime = min(runtime, res.runtime);
213                 break;
214             case Status.failed:
215                 goto case;
216             case Status.timeout:
217                 goto case;
218             case Status.error:
219                 failed = true;
220                 print(res.result.output);
221                 break;
222             }
223         } catch (Exception e) {
224             logger.error(e.msg).collectException;
225             failed = true;
226         }
227     }
228 
229     return MeasureTestDurationResult(!failed, runtime);
230 }
231 
232 /** Drive the control flow when testing **a** mutant.
233  */
234 struct MutationTestDriver {
235     import std.datetime.stopwatch : StopWatch;
236     import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
237 
238     static struct Global {
239         FilesysIO fio;
240         NullableRef!Database db;
241 
242         /// Files that should be automatically removed after the testing is done is added here.
243         AutoCleanup auto_cleanup;
244 
245         /// The mutant to apply.
246         MutationEntry mutp;
247 
248         /// Runs the test commands.
249         TestRunner* runner;
250 
251         /// File to mutate.
252         AbsolutePath mut_file;
253         /// The original file.
254         Blob original;
255 
256         /// The result of running the test cases.
257         Mutation.Status mut_status;
258 
259         /// Test cases that killed the mutant.
260         GatherTestCase test_cases;
261 
262         /// How long it took to do the mutation testing.
263         StopWatch sw;
264     }
265 
266     static struct TestMutantData {
267         /// If the user has configured that the test cases should be analyzed.
268         bool hasTestCaseOutputAnalyzer;
269         ShellCommand compile_cmd;
270     }
271 
272     static struct TestCaseAnalyzeData {
273         //TODO: change to a ShellCommand
274         ShellCommand test_case_cmd;
275         const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin;
276         DrainElement[] output;
277     }
278 
279     static struct None {
280     }
281 
282     static struct Initialize {
283     }
284 
285     static struct MutateCode {
286         bool next;
287         bool filesysError;
288         bool mutationError;
289     }
290 
291     static struct TestMutant {
292         bool mutationError;
293     }
294 
295     static struct RestoreCode {
296         bool next;
297         bool filesysError;
298     }
299 
300     static struct TestCaseAnalyze {
301         bool mutationError;
302         bool unstableTests;
303     }
304 
305     static struct StoreResult {
306     }
307 
308     static struct Done {
309     }
310 
311     static struct FilesysError {
312     }
313 
314     // happens when an error occurs during mutations testing but that do not
315     // prohibit testing of other mutants
316     static struct NoResultRestoreCode {
317     }
318 
319     static struct NoResult {
320     }
321 
322     alias Fsm = dextool.fsm.Fsm!(None, Initialize, MutateCode, TestMutant, RestoreCode,
323             TestCaseAnalyze, StoreResult, Done, FilesysError, NoResultRestoreCode, NoResult);
324     Fsm fsm;
325 
326     Global global;
327     MutationTestResult result;
328 
329     alias LocalStateDataT = Tuple!(TestMutantData, TestCaseAnalyzeData);
330     TypeDataMap!(LocalStateDataT, TestMutant, TestCaseAnalyze) local;
331 
332     this(Global global, TestMutantData l1, TestCaseAnalyzeData l2) {
333         this.global = global;
334         this.local = LocalStateDataT(l1, l2);
335     }
336 
337     static void execute_(ref MutationTestDriver self) @trusted {
338         self.fsm.next!((None a) => fsm(Initialize.init),
339                 (Initialize a) => fsm(MutateCode.init), (MutateCode a) {
340             if (a.next)
341                 return fsm(TestMutant.init);
342             else if (a.filesysError)
343                 return fsm(FilesysError.init);
344             else if (a.mutationError)
345                 return fsm(NoResultRestoreCode.init);
346             return fsm(a);
347         }, (TestMutant a) {
348             if (a.mutationError)
349                 return fsm(NoResultRestoreCode.init);
350             else if (self.global.mut_status == Mutation.Status.killed
351                 && self.local.get!TestMutant.hasTestCaseOutputAnalyzer
352                 && !self.local.get!TestCaseAnalyze.output.empty)
353                 return fsm(TestCaseAnalyze.init);
354             return fsm(RestoreCode.init);
355         }, (TestCaseAnalyze a) {
356             if (a.mutationError || a.unstableTests)
357                 return fsm(NoResultRestoreCode.init);
358             return fsm(RestoreCode.init);
359         }, (RestoreCode a) {
360             if (a.next)
361                 return fsm(StoreResult.init);
362             else if (a.filesysError)
363                 return fsm(FilesysError.init);
364             return fsm(a);
365         }, (StoreResult a) { return fsm(Done.init); }, (Done a) => fsm(a),
366                 (FilesysError a) => fsm(a),
367                 (NoResultRestoreCode a) => fsm(NoResult.init), (NoResult a) => fsm(a),);
368 
369         self.fsm.act!(self);
370     }
371 
372 nothrow:
373 
374     void execute() {
375         try {
376             execute_(this);
377         } catch (Exception e) {
378             logger.warning(e.msg).collectException;
379         }
380     }
381 
382     /// Returns: true as long as the driver is processing a mutant.
383     bool isRunning() {
384         return !fsm.isState!(Done, NoResult, FilesysError);
385     }
386 
387     bool stopBecauseError() {
388         return fsm.isState!(FilesysError);
389     }
390 
391     void opCall(None data) {
392     }
393 
394     void opCall(Initialize data) {
395         global.sw.start;
396     }
397 
398     void opCall(Done data) {
399     }
400 
401     void opCall(FilesysError data) {
402         logger.warning("Filesystem error").collectException;
403     }
404 
405     void opCall(NoResultRestoreCode data) {
406         RestoreCode tmp;
407         this.opCall(tmp);
408     }
409 
410     void opCall(NoResult data) {
411     }
412 
413     void opCall(ref MutateCode data) {
414         import dextool.plugin.mutate.backend.generate_mutant : generateMutant,
415             GenerateMutantResult, GenerateMutantStatus;
416 
417         try {
418             global.mut_file = AbsolutePath(FileName(global.mutp.file),
419                     DirName(global.fio.getOutputDir));
420             global.original = global.fio.makeInput(global.mut_file);
421         } catch (Exception e) {
422             logger.error(e.msg).collectException;
423             logger.warning("Unable to read ", global.mut_file).collectException;
424             data.filesysError = true;
425             return;
426         }
427 
428         // mutate
429         try {
430             auto fout = global.fio.makeOutput(global.mut_file);
431             auto mut_res = generateMutant(global.db.get, global.mutp, global.original, fout);
432 
433             final switch (mut_res.status) with (GenerateMutantStatus) {
434             case error:
435                 data.mutationError = true;
436                 break;
437             case filesysError:
438                 data.filesysError = true;
439                 break;
440             case databaseError:
441                 // such as when the database is locked
442                 data.mutationError = true;
443                 break;
444             case checksumError:
445                 data.filesysError = true;
446                 break;
447             case noMutation:
448                 data.mutationError = true;
449                 break;
450             case ok:
451                 data.next = true;
452                 try {
453                     logger.infof("%s from '%s' to '%s' in %s:%s:%s", global.mutp.id,
454                             cast(const(char)[]) mut_res.from, cast(const(char)[]) mut_res.to,
455                             global.mut_file, global.mutp.sloc.line, global.mutp.sloc.column);
456 
457                 } catch (Exception e) {
458                     logger.warning("Mutation ID", e.msg);
459                 }
460                 break;
461             }
462         } catch (Exception e) {
463             logger.warning(e.msg).collectException;
464             data.mutationError = true;
465         }
466     }
467 
468     void opCall(ref TestMutant data) {
469         try {
470             auto res = runTester(local.get!TestMutant.compile_cmd, *global.runner);
471             global.mut_status = res.status;
472             local.get!TestCaseAnalyze.output = res.output;
473         } catch (Exception e) {
474             logger.warning(e.msg).collectException;
475             data.mutationError = true;
476         }
477     }
478 
479     void opCall(ref TestCaseAnalyze data) {
480         try {
481             auto gather_tc = new GatherTestCase;
482 
483             // the post processer must succeeed for the data to be stored. if
484             // is considered a major error that may corrupt existing data if it
485             // fails.
486             bool success = true;
487 
488             if (!local.get!TestCaseAnalyze.test_case_cmd.empty) {
489                 success = success && externalProgram(local.get!TestCaseAnalyze.test_case_cmd,
490                         local.get!TestCaseAnalyze.output, gather_tc, global.auto_cleanup);
491             }
492             if (!local.get!TestCaseAnalyze.tc_analyze_builtin.empty) {
493                 success = success && builtin(local.get!TestCaseAnalyze.output,
494                         local.get!TestCaseAnalyze.tc_analyze_builtin, gather_tc);
495             }
496 
497             if (!gather_tc.unstable.empty) {
498                 logger.warningf("Unstable test cases found: [%-(%s, %)]",
499                         gather_tc.unstableAsArray);
500                 logger.info(
501                         "As configured the result is ignored which will force the mutant to be re-tested");
502                 data.unstableTests = true;
503             } else if (success) {
504                 global.test_cases = gather_tc;
505             }
506         } catch (Exception e) {
507             logger.warning(e.msg).collectException;
508         }
509     }
510 
511     void opCall(StoreResult data) {
512         global.sw.stop;
513         auto failedTestCases = () {
514             if (global.test_cases is null) {
515                 return null;
516             }
517             return global.test_cases.failedAsArray;
518         }();
519         result = MutationTestResult.StatusUpdate(global.mutp.id,
520                 global.mut_status, global.sw.peek, failedTestCases);
521     }
522 
523     void opCall(ref RestoreCode data) {
524         // restore the original file.
525         try {
526             global.fio.makeOutput(global.mut_file).write(global.original.content);
527         } catch (Exception e) {
528             logger.error(e.msg).collectException;
529             // fatal error because being unable to restore a file prohibit
530             // future mutations.
531             data.filesysError = true;
532             return;
533         }
534 
535         data.next = true;
536     }
537 }
538 
539 struct TestDriver {
540     import std.datetime : SysTime;
541     import std.typecons : Unique;
542     import dextool.plugin.mutate.backend.test_mutant.timeout : calculateTimeout, TimeoutFsm;
543 
544     /// Runs the test commands.
545     TestRunner runner;
546 
547     static struct Global {
548         DriverData data;
549         Unique!MutationTestDriver mut_driver;
550 
551         TimeoutFsm timeoutFsm;
552         /// The time it takes to execute the test suite when no mutant is injected.
553         Duration testSuiteRuntime;
554 
555         /// the next mutant to test, if there are any.
556         MutationEntry nextMutant;
557 
558         // when the user manually configure the timeout it means that the
559         // timeout algorithm should not be used.
560         bool hardcodedTimeout;
561 
562         /// Max time to run the mutation testing for.
563         SysTime maxRuntime;
564 
565         /// Test commands to execute.
566         ShellCommand[] testCmds;
567     }
568 
569     static struct UpdateTimeoutData {
570         long lastTimeoutIter;
571     }
572 
573     static struct None {
574     }
575 
576     static struct Initialize {
577     }
578 
579     static struct PullRequest {
580     }
581 
582     static struct PullRequestData {
583         import dextool.plugin.mutate.type : TestConstraint;
584 
585         TestConstraint constraint;
586         long seed;
587     }
588 
589     static struct SanityCheck {
590         bool sanityCheckFailed;
591     }
592 
593     static struct AnalyzeTestCmdForTestCase {
594         TestCase[] foundTestCases;
595     }
596 
597     static struct UpdateAndResetAliveMutants {
598         TestCase[] foundTestCases;
599     }
600 
601     static struct ResetOldMutant {
602         bool doneTestingOldMutants;
603     }
604 
605     static struct ResetOldMutantData {
606         /// Number of mutants that where reset.
607         long resetCount;
608         long maxReset;
609     }
610 
611     static struct CleanupTempDirs {
612     }
613 
614     static struct CheckMutantsLeft {
615         bool allMutantsTested;
616     }
617 
618     static struct ParseStdin {
619     }
620 
621     static struct PreCompileSut {
622         bool compilationError;
623     }
624 
625     static struct FindTestCmds {
626     }
627 
628     static struct ChooseMode {
629     }
630 
631     static struct MeasureTestSuite {
632         bool unreliableTestSuite;
633     }
634 
635     static struct PreMutationTest {
636     }
637 
638     static struct MutationTest {
639         bool next;
640         bool mutationError;
641         MutationTestResult result;
642     }
643 
644     static struct CheckTimeout {
645         bool timeoutUnchanged;
646     }
647 
648     static struct Done {
649     }
650 
651     static struct Error {
652     }
653 
654     static struct UpdateTimeout {
655     }
656 
657     static struct NextPullRequestMutant {
658         bool noUnknownMutantsLeft;
659     }
660 
661     static struct NextPullRequestMutantData {
662         import dextool.plugin.mutate.backend.database : MutationStatusId;
663 
664         MutationStatusId[] mutants;
665 
666         /// If set then stop after this many alive are found.
667         Nullable!int maxAlive;
668         /// number of alive mutants that has been found.
669         int alive;
670     }
671 
672     static struct NextMutant {
673         bool noUnknownMutantsLeft;
674     }
675 
676     static struct HandleTestResult {
677         MutationTestResult result;
678     }
679 
680     static struct CheckRuntime {
681         bool reachedMax;
682     }
683 
684     static struct SetMaxRuntime {
685     }
686 
687     alias Fsm = dextool.fsm.Fsm!(None, Initialize, SanityCheck,
688             AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, ResetOldMutant,
689             CleanupTempDirs, CheckMutantsLeft, PreCompileSut, MeasureTestSuite,
690             PreMutationTest, NextMutant, MutationTest, HandleTestResult,
691             CheckTimeout, Done, Error, UpdateTimeout, CheckRuntime,
692             SetMaxRuntime, PullRequest, NextPullRequestMutant, ParseStdin,
693             FindTestCmds, ChooseMode);
694 
695     Fsm fsm;
696 
697     Global global;
698 
699     alias LocalStateDataT = Tuple!(UpdateTimeoutData,
700             NextPullRequestMutantData, PullRequestData, ResetOldMutantData);
701     TypeDataMap!(LocalStateDataT, UpdateTimeout, NextPullRequestMutant,
702             PullRequest, ResetOldMutant) local;
703 
704     this(DriverData data) {
705         this.global = Global(data);
706         this.global.timeoutFsm = TimeoutFsm(data.mutKind);
707         this.global.hardcodedTimeout = !global.data.conf.mutationTesterRuntime.isNull;
708         local.get!PullRequest.constraint = global.data.conf.constraint;
709         local.get!PullRequest.seed = global.data.conf.pullRequestSeed;
710         local.get!NextPullRequestMutant.maxAlive = global.data.conf.maxAlive;
711         local.get!ResetOldMutant.maxReset = global.data.conf.oldMutantsNr;
712         this.global.testCmds = global.data.conf.mutationTester;
713 
714         this.runner = TestRunner.make(global.data.conf.testPoolSize);
715         // using an unreasonable timeout to make it possible to analyze for
716         // test cases and measure the test suite.
717         this.runner.timeout = 999.dur!"hours";
718         this.runner.put(data.conf.mutationTester);
719     }
720 
721     static void execute_(ref TestDriver self) @trusted {
722         // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a
723         // graphical view of the state machine.
724 
725         self.fsm.next!((None a) => fsm(Initialize.init),
726                 (Initialize a) => fsm(SanityCheck.init), (SanityCheck a) {
727             if (a.sanityCheckFailed)
728                 return fsm(Error.init);
729             if (self.global.data.conf.unifiedDiffFromStdin)
730                 return fsm(ParseStdin.init);
731             return fsm(PreCompileSut.init);
732         }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) => fsm(
733                 UpdateAndResetAliveMutants(a.foundTestCases)),
734                 (UpdateAndResetAliveMutants a) => fsm(CheckMutantsLeft.init), (ResetOldMutant a) {
735             if (a.doneTestingOldMutants)
736                 return fsm(Done.init);
737             return fsm(UpdateTimeout.init);
738         }, (CleanupTempDirs a) {
739             if (self.local.get!PullRequest.constraint.empty)
740                 return fsm(NextMutant.init);
741             return fsm(NextPullRequestMutant.init);
742         }, (CheckMutantsLeft a) {
743             if (a.allMutantsTested
744                 && self.global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing)
745                 return fsm(Done.init);
746             return fsm(MeasureTestSuite.init);
747         }, (PreCompileSut a) {
748             if (a.compilationError)
749                 return fsm(Error.init);
750             if (self.global.data.conf.testCommandDir.empty)
751                 return fsm(ChooseMode.init);
752             return fsm(FindTestCmds.init);
753         }, (FindTestCmds a) { return fsm(ChooseMode.init); }, (ChooseMode a) {
754             if (!self.local.get!PullRequest.constraint.empty)
755                 return fsm(PullRequest.init);
756             if (!self.global.data.conf.mutationTestCaseAnalyze.empty
757                 || !self.global.data.conf.mutationTestCaseBuiltin.empty)
758                 return fsm(AnalyzeTestCmdForTestCase.init);
759             return fsm(CheckMutantsLeft.init);
760         }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) {
761             if (a.unreliableTestSuite)
762                 return fsm(Error.init);
763             return fsm(SetMaxRuntime.init);
764         }, (SetMaxRuntime a) => fsm(UpdateTimeout.init), (NextPullRequestMutant a) {
765             if (a.noUnknownMutantsLeft)
766                 return fsm(Done.init);
767             return fsm(PreMutationTest.init);
768         }, (NextMutant a) {
769             if (a.noUnknownMutantsLeft)
770                 return fsm(CheckTimeout.init);
771             return fsm(PreMutationTest.init);
772         }, (PreMutationTest a) => fsm(MutationTest.init),
773                 (UpdateTimeout a) => fsm(CleanupTempDirs.init), (MutationTest a) {
774             if (a.next)
775                 return fsm(HandleTestResult(a.result));
776             else if (a.mutationError)
777                 return fsm(Error.init);
778             return fsm(a);
779         }, (HandleTestResult a) => fsm(CheckRuntime.init), (CheckRuntime a) {
780             if (a.reachedMax)
781                 return fsm(Done.init);
782             return fsm(UpdateTimeout.init);
783         }, (CheckTimeout a) {
784             if (a.timeoutUnchanged)
785                 return fsm(ResetOldMutant.init);
786             return fsm(UpdateTimeout.init);
787         }, (Done a) => fsm(a), (Error a) => fsm(a),);
788 
789         self.fsm.act!(self);
790     }
791 
792 nothrow:
793     void execute() {
794         try {
795             execute_(this);
796         } catch (Exception e) {
797             logger.warning(e.msg).collectException;
798         }
799     }
800 
801     bool isRunning() {
802         return !fsm.isState!(Done, Error);
803     }
804 
805     ExitStatusType status() {
806         if (fsm.isState!Done)
807             return ExitStatusType.Ok;
808         return ExitStatusType.Errors;
809     }
810 
811     void opCall(None data) {
812     }
813 
814     void opCall(Initialize data) {
815     }
816 
817     void opCall(Done data) {
818         global.data.autoCleanup.cleanup;
819 
820         logger.info("Done!").collectException;
821     }
822 
823     void opCall(Error data) {
824         global.data.autoCleanup.cleanup;
825     }
826 
827     void opCall(ref SanityCheck data) {
828         // #SPC-sanity_check_db_vs_filesys
829         import colorlog : color, Color;
830         import dextool.plugin.mutate.backend.utility : checksum, Checksum;
831 
832         logger.info("Checking that the file(s) on the filesystem match the database")
833             .collectException;
834 
835         auto failed = appender!(string[])();
836         foreach (file; spinSql!(() { return global.data.db.getFiles; })) {
837             auto db_checksum = spinSql!(() {
838                 return global.data.db.getFileChecksum(file);
839             });
840 
841             try {
842                 auto abs_f = AbsolutePath(FileName(file),
843                         DirName(cast(string) global.data.filesysIO.getOutputDir));
844                 auto f_checksum = checksum(global.data.filesysIO.makeInput(abs_f).content[]);
845                 if (db_checksum != f_checksum) {
846                     failed.put(abs_f);
847                 }
848             } catch (Exception e) {
849                 // assume it is a problem reading the file or something like that.
850                 failed.put(file);
851                 logger.warningf("%s: %s", file, e.msg).collectException;
852             }
853         }
854 
855         data.sanityCheckFailed = failed.data.length != 0;
856 
857         if (data.sanityCheckFailed) {
858             logger.error("Detected that file(s) has changed since last analyze where done")
859                 .collectException;
860             logger.error("Either restore the file(s) or rerun the analyze").collectException;
861             foreach (f; failed.data) {
862                 logger.info(f).collectException;
863             }
864         } else {
865             logger.info("Ok".color(Color.green)).collectException;
866         }
867     }
868 
869     void opCall(ParseStdin data) {
870         import dextool.plugin.mutate.backend.diff_parser : diffFromStdin;
871         import dextool.plugin.mutate.type : Line;
872 
873         try {
874             auto constraint = local.get!PullRequest.constraint;
875             foreach (pkv; diffFromStdin.toRange(global.data.filesysIO.getOutputDir)) {
876                 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array;
877             }
878             local.get!PullRequest.constraint = constraint;
879         } catch (Exception e) {
880             logger.warning(e.msg).collectException;
881         }
882     }
883 
884     void opCall(ref AnalyzeTestCmdForTestCase data) {
885         import std.datetime.stopwatch : StopWatch;
886         import dextool.plugin.mutate.backend.type : TestCase;
887 
888         TestCase[] all_found_tc;
889 
890         try {
891             import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
892 
893             auto res = runTester(global.data.conf.mutationCompile, runner);
894 
895             auto gather_tc = new GatherTestCase;
896 
897             if (!global.data.conf.mutationTestCaseAnalyze.empty) {
898                 externalProgram(global.data.conf.mutationTestCaseAnalyze,
899                         res.output, gather_tc, global.data.autoCleanup);
900                 logger.warningf(gather_tc.unstable.length != 0,
901                         "Unstable test cases found: [%-(%s, %)]", gather_tc.unstableAsArray);
902             }
903             if (!global.data.conf.mutationTestCaseBuiltin.empty) {
904                 builtin(res.output, global.data.conf.mutationTestCaseBuiltin, gather_tc);
905             }
906 
907             all_found_tc = gather_tc.foundAsArray;
908         } catch (Exception e) {
909             logger.warning(e.msg).collectException;
910         }
911 
912         warnIfConflictingTestCaseIdentifiers(all_found_tc);
913 
914         data.foundTestCases = all_found_tc;
915     }
916 
917     void opCall(UpdateAndResetAliveMutants data) {
918         import std.traits : EnumMembers;
919 
920         // the test cases before anything has potentially changed.
921         auto old_tcs = spinSql!(() {
922             Set!string old_tcs;
923             foreach (tc; global.data.db.getDetectedTestCases) {
924                 old_tcs.add(tc.name);
925             }
926             return old_tcs;
927         });
928 
929         void transaction() @safe {
930             final switch (global.data.conf.onRemovedTestCases) with (
931                 ConfigMutationTest.RemovedTestCases) {
932             case doNothing:
933                 global.data.db.addDetectedTestCases(data.foundTestCases);
934                 break;
935             case remove:
936                 foreach (id; global.data.db.setDetectedTestCases(data.foundTestCases)) {
937                     global.data.db.updateMutationStatus(id, Mutation.Status.unknown);
938                 }
939                 break;
940             }
941         }
942 
943         auto found_tcs = spinSql!(() @trusted {
944             auto tr = global.data.db.transaction;
945             transaction();
946 
947             Set!string found_tcs;
948             foreach (tc; global.data.db.getDetectedTestCases) {
949                 found_tcs.add(tc.name);
950             }
951 
952             tr.commit;
953             return found_tcs;
954         });
955 
956         printDroppedTestCases(old_tcs, found_tcs);
957 
958         if (hasNewTestCases(old_tcs, found_tcs)
959                 && global.data.conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) {
960             logger.info("Resetting alive mutants").collectException;
961             // there is no use in trying to limit the mutants to reset to those
962             // that are part of "this" execution because new test cases can
963             // only mean one thing: re-test all alive mutants.
964             spinSql!(() {
965                 global.data.db.resetMutant([EnumMembers!(Mutation.Kind)],
966                     Mutation.Status.alive, Mutation.Status.unknown);
967             });
968         }
969     }
970 
971     void opCall(ref ResetOldMutant data) {
972         import dextool.plugin.mutate.backend.database.type;
973 
974         if (global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) {
975             data.doneTestingOldMutants = true;
976             return;
977         }
978         if (Clock.currTime > global.maxRuntime) {
979             data.doneTestingOldMutants = true;
980             return;
981         }
982         if (local.get!ResetOldMutant.resetCount >= local.get!ResetOldMutant.maxReset) {
983             data.doneTestingOldMutants = true;
984             return;
985         }
986 
987         local.get!ResetOldMutant.resetCount++;
988 
989         logger.infof("Resetting an old mutant (%s/%s)", local.get!ResetOldMutant.resetCount,
990                 local.get!ResetOldMutant.maxReset).collectException;
991         auto oldest = spinSql!(() {
992             return global.data.db.getOldestMutants(global.data.mutKind, 1);
993         });
994 
995         foreach (const old; oldest) {
996             logger.info("Last updated ", old.updated).collectException;
997             spinSql!(() {
998                 global.data.db.updateMutationStatus(old.id, Mutation.Status.unknown);
999             });
1000         }
1001     }
1002 
1003     void opCall(CleanupTempDirs data) {
1004         global.data.autoCleanup.cleanup;
1005     }
1006 
1007     void opCall(ref CheckMutantsLeft data) {
1008         spinSql!(() { global.timeoutFsm.execute(global.data.db); });
1009 
1010         data.allMutantsTested = global.timeoutFsm.output.done;
1011 
1012         if (global.timeoutFsm.output.done) {
1013             logger.info("All mutants are tested").collectException;
1014         }
1015     }
1016 
1017     void opCall(ref PreCompileSut data) {
1018         import std.stdio : write;
1019         import colorlog : color, Color;
1020         import process;
1021 
1022         logger.info("Checking the build command").collectException;
1023         try {
1024             auto output = appender!(DrainElement[])();
1025             auto p = pipeProcess(global.data.conf.mutationCompile.value).sandbox.drain(output)
1026                 .scopeKill;
1027             if (p.wait == 0) {
1028                 logger.info("Ok".color(Color.green));
1029                 return;
1030             }
1031 
1032             logger.error("Build commman failed");
1033             foreach (l; output.data) {
1034                 write(l.byUTF8);
1035             }
1036         } catch (Exception e) {
1037             // unable to for example execute the compiler
1038             logger.error(e.msg).collectException;
1039         }
1040 
1041         data.compilationError = true;
1042     }
1043 
1044     void opCall(FindTestCmds data) {
1045         auto cmds = appender!(ShellCommand[])();
1046         foreach (root; global.data.conf.testCommandDir) {
1047             try {
1048                 cmds.put(findExecutables(root.AbsolutePath)
1049                         .map!(a => ShellCommand([a] ~ global.data.conf.testCommandDirFlag)));
1050             } catch (Exception e) {
1051                 logger.warning(e.msg).collectException;
1052             }
1053         }
1054 
1055         if (!cmds.data.empty) {
1056             this.runner.put(cmds.data);
1057             this.global.testCmds ~= cmds.data;
1058             logger.infof("Found test commands in %s:",
1059                     global.data.conf.testCommandDir).collectException;
1060             foreach (c; cmds.data) {
1061                 logger.info(c).collectException;
1062             }
1063         }
1064     }
1065 
1066     void opCall(ChooseMode data) {
1067     }
1068 
1069     void opCall(PullRequest data) {
1070         import std.array : appender;
1071         import std.random : randomCover, Mt19937_64;
1072         import dextool.plugin.mutate.backend.database : MutationStatusId;
1073         import dextool.plugin.mutate.backend.type : SourceLoc;
1074         import dextool.set;
1075 
1076         Set!MutationStatusId mut_ids;
1077 
1078         foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) {
1079             const file_id = spinSql!(() => global.data.db.getFileId(kv.key));
1080             if (file_id.isNull) {
1081                 logger.infof("The file %s do not exist in the database. Skipping...",
1082                         kv.key).collectException;
1083                 continue;
1084             }
1085 
1086             foreach (l; kv.value) {
1087                 auto mutants = spinSql!(() {
1088                     return global.data.db.getMutationsOnLine(global.data.mutKind,
1089                         file_id.get, SourceLoc(l.value, 0));
1090                 });
1091 
1092                 const pre_cnt = mut_ids.length;
1093                 foreach (v; mutants)
1094                     mut_ids.add(v);
1095 
1096                 logger.infof(mut_ids.length - pre_cnt > 0, "Found %s mutant(s) to test (%s:%s)",
1097                         mut_ids.length - pre_cnt, kv.key, l.value).collectException;
1098             }
1099         }
1100 
1101         logger.infof(!mut_ids.empty, "Found %s mutants in the diff",
1102                 mut_ids.length).collectException;
1103 
1104         const seed = local.get!PullRequest.seed;
1105         logger.infof("Using random seed %s when choosing the mutants to test",
1106                 seed).collectException;
1107         auto rng = Mt19937_64(seed);
1108         local.get!NextPullRequestMutant.mutants = mut_ids.toArray.sort.randomCover(rng).array;
1109         logger.trace("Test sequence ", local.get!NextPullRequestMutant.mutants).collectException;
1110 
1111         if (mut_ids.empty) {
1112             logger.warning("None of the locations specified with -L exists").collectException;
1113             logger.info("Available files are:").collectException;
1114             foreach (f; spinSql!(() => global.data.db.getFiles))
1115                 logger.info(f).collectException;
1116         }
1117     }
1118 
1119     void opCall(ref MeasureTestSuite data) {
1120         if (!global.data.conf.mutationTesterRuntime.isNull) {
1121             global.testSuiteRuntime = global.data.conf.mutationTesterRuntime.get;
1122             return;
1123         }
1124 
1125         logger.info("Measuring the runtime of the test command: ",
1126                 global.testCmds).collectException;
1127         const tester = measureTestCommand(runner);
1128         if (tester.ok) {
1129             // The sampling of the test suite become too unreliable when the timeout is <1s.
1130             // This is a quick and dirty fix.
1131             // A proper fix requires an update of the sampler in runTester.
1132             auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime;
1133             logger.info("Test command runtime: ", t).collectException;
1134             global.testSuiteRuntime = t;
1135         } else {
1136             data.unreliableTestSuite = true;
1137             logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected")
1138                 .collectException;
1139         }
1140     }
1141 
1142     void opCall(PreMutationTest) {
1143         auto factory(DriverData d, MutationEntry mutp, TestRunner* runner) @safe nothrow {
1144             import std.typecons : Unique;
1145             import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
1146 
1147             try {
1148                 auto global = MutationTestDriver.Global(d.filesysIO, d.db,
1149                         d.autoCleanup, mutp, runner);
1150                 return Unique!MutationTestDriver(new MutationTestDriver(global,
1151                         MutationTestDriver.TestMutantData(!(d.conf.mutationTestCaseAnalyze.empty
1152                         && d.conf.mutationTestCaseBuiltin.empty), d.conf.mutationCompile,),
1153                         MutationTestDriver.TestCaseAnalyzeData(d.conf.mutationTestCaseAnalyze,
1154                         d.conf.mutationTestCaseBuiltin)));
1155             } catch (Exception e) {
1156                 logger.error(e.msg).collectException;
1157             }
1158             assert(0, "should not happen");
1159         }
1160 
1161         runner.timeout = calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime);
1162         global.mut_driver = factory(global.data, global.nextMutant, () @trusted {
1163             return &runner;
1164         }());
1165     }
1166 
1167     void opCall(ref MutationTest data) {
1168         if (global.mut_driver.isRunning) {
1169             global.mut_driver.execute();
1170         } else if (global.mut_driver.stopBecauseError) {
1171             data.mutationError = true;
1172         } else {
1173             data.result = global.mut_driver.result;
1174             data.next = true;
1175         }
1176     }
1177 
1178     void opCall(ref CheckTimeout data) {
1179         data.timeoutUnchanged = global.hardcodedTimeout || global.timeoutFsm.output.done;
1180     }
1181 
1182     void opCall(UpdateTimeout) {
1183         spinSql!(() { global.timeoutFsm.execute(global.data.db); });
1184 
1185         const lastIter = local.get!UpdateTimeout.lastTimeoutIter;
1186 
1187         if (lastIter != global.timeoutFsm.output.iter) {
1188             logger.infof("Changed the timeout from %s to %s (iteration %s)",
1189                     calculateTimeout(lastIter, global.testSuiteRuntime),
1190                     calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime),
1191                     global.timeoutFsm.output.iter).collectException;
1192             local.get!UpdateTimeout.lastTimeoutIter = global.timeoutFsm.output.iter;
1193         }
1194     }
1195 
1196     void opCall(ref NextPullRequestMutant data) {
1197         global.nextMutant = MutationEntry.init;
1198         data.noUnknownMutantsLeft = true;
1199 
1200         while (!local.get!NextPullRequestMutant.mutants.empty) {
1201             const id = local.get!NextPullRequestMutant.mutants[$ - 1];
1202             const status = spinSql!(() => global.data.db.getMutationStatus(id));
1203 
1204             if (status.isNull)
1205                 continue;
1206 
1207             if (status.get == Mutation.Status.alive) {
1208                 local.get!NextPullRequestMutant.alive++;
1209             }
1210 
1211             if (status.get != Mutation.Status.unknown) {
1212                 local.get!NextPullRequestMutant.mutants
1213                     = local.get!NextPullRequestMutant.mutants[0 .. $ - 1];
1214                 continue;
1215             }
1216 
1217             const info = spinSql!(() => global.data.db.getMutantsInfo(global.data.mutKind, [
1218                         id
1219                     ]));
1220             if (info.empty)
1221                 continue;
1222 
1223             global.nextMutant = spinSql!(() => global.data.db.getMutation(info[0].id));
1224             data.noUnknownMutantsLeft = false;
1225             break;
1226         }
1227 
1228         if (!local.get!NextPullRequestMutant.maxAlive.isNull) {
1229             const alive = local.get!NextPullRequestMutant.alive;
1230             const maxAlive = local.get!NextPullRequestMutant.maxAlive.get;
1231             logger.infof(alive > 0, "Found %s/%s alive mutants", alive, maxAlive).collectException;
1232             if (alive >= maxAlive) {
1233                 data.noUnknownMutantsLeft = true;
1234             }
1235         }
1236     }
1237 
1238     void opCall(ref NextMutant data) {
1239         global.nextMutant = MutationEntry.init;
1240 
1241         auto next = spinSql!(() {
1242             return global.data.db.nextMutation(global.data.mutKind);
1243         });
1244 
1245         data.noUnknownMutantsLeft = next.st == NextMutationEntry.Status.done;
1246 
1247         if (!next.entry.isNull) {
1248             global.nextMutant = next.entry.get;
1249         }
1250     }
1251 
1252     void opCall(HandleTestResult data) {
1253         void statusUpdate(MutationTestResult.StatusUpdate result) {
1254             import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus;
1255 
1256             const cnt_action = () {
1257                 if (result.status == Mutation.Status.alive)
1258                     return Database.CntAction.incr;
1259                 return Database.CntAction.reset;
1260             }();
1261 
1262             auto statusId = spinSql!(() {
1263                 return global.data.db.getMutationStatusId(result.id);
1264             });
1265             if (statusId.isNull)
1266                 return;
1267 
1268             spinSql!(() @trusted {
1269                 auto t = global.data.db.transaction;
1270                 updateMutantStatus(global.data.db, statusId.get, result.status,
1271                     global.timeoutFsm.output.iter);
1272                 global.data.db.updateMutation(statusId.get, cnt_action);
1273                 global.data.db.updateMutation(statusId.get, result.testTime);
1274                 global.data.db.updateMutationTestCases(statusId.get, result.testCases);
1275                 t.commit;
1276             });
1277 
1278             logger.infof("%s %s (%s)", result.id, result.status, result.testTime).collectException;
1279             logger.infof(!result.testCases.empty, `%s killed by [%-(%s, %)]`,
1280                     result.id, result.testCases.sort.map!"a.name").collectException;
1281         }
1282 
1283         data.result.value.match!((MutationTestResult.NoResult a) {},
1284                 (MutationTestResult.StatusUpdate a) => statusUpdate(a));
1285     }
1286 
1287     void opCall(SetMaxRuntime) {
1288         global.maxRuntime = Clock.currTime + global.data.conf.maxRuntime;
1289     }
1290 
1291     void opCall(ref CheckRuntime data) {
1292         data.reachedMax = Clock.currTime > global.maxRuntime;
1293         if (data.reachedMax) {
1294             logger.infof("Max runtime of %s reached at %s",
1295                     global.data.conf.maxRuntime, global.maxRuntime).collectException;
1296         }
1297     }
1298 }
1299 
1300 private:
1301 
1302 /** Run an external program that analyze the output from the test suite for
1303  * test cases that failed.
1304  *
1305  * Params:
1306  * cmd = user analyze command to execute on the output
1307  * output = output from the test command to be passed on to the analyze command
1308  * report = the result is stored in the report
1309  *
1310  * Returns: True if it successfully analyzed the output
1311  */
1312 bool externalProgram(ShellCommand cmd, DrainElement[] output,
1313         TestCaseReport report, AutoCleanup cleanup) @safe nothrow {
1314     import std.algorithm : copy;
1315     import std.ascii : newline;
1316     import std.string : strip, startsWith;
1317     import process;
1318 
1319     immutable passed = "passed:";
1320     immutable failed = "failed:";
1321     immutable unstable = "unstable:";
1322 
1323     auto tmpdir = createTmpDir();
1324     if (tmpdir.empty) {
1325         return false;
1326     }
1327 
1328     ShellCommand writeOutput(ShellCommand cmd) @safe {
1329         import std.stdio : File;
1330 
1331         const stdoutPath = buildPath(tmpdir, "stdout.log");
1332         const stderrPath = buildPath(tmpdir, "stderr.log");
1333         auto stdout = File(stdoutPath, "w");
1334         auto stderr = File(stderrPath, "w");
1335 
1336         foreach (a; output) {
1337             final switch (a.type) {
1338             case DrainElement.Type.stdout:
1339                 stdout.write(a.data);
1340                 break;
1341             case DrainElement.Type.stderr:
1342                 stderr.write(a.data);
1343                 break;
1344             }
1345         }
1346 
1347         cmd.value ~= [stdoutPath, stderrPath];
1348         return cmd;
1349     }
1350 
1351     try {
1352         cleanup.add(tmpdir.Path.AbsolutePath);
1353         cmd = writeOutput(cmd);
1354         auto p = pipeProcess(cmd.value).sandbox.scopeKill;
1355         foreach (l; p.process
1356                 .drainByLineCopy
1357                 .map!(a => a.strip)
1358                 .filter!(a => !a.empty)) {
1359             if (l.startsWith(passed))
1360                 report.reportFound(TestCase(l[passed.length .. $].strip.idup));
1361             else if (l.startsWith(failed))
1362                 report.reportFailed(TestCase(l[failed.length .. $].strip.idup));
1363             else if (l.startsWith(unstable))
1364                 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup));
1365         }
1366 
1367         if (p.wait == 0) {
1368             return true;
1369         }
1370 
1371         logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd);
1372     } catch (Exception e) {
1373         logger.warning(e.msg).collectException;
1374     }
1375 
1376     return false;
1377 }
1378 
1379 /** Analyze the output from the test suite with one of the builtin analyzers.
1380  */
1381 bool builtin(DrainElement[] output,
1382         const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @safe nothrow {
1383     import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze;
1384     import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze;
1385     import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze;
1386 
1387     GtestParser gtest;
1388     CtestParser ctest;
1389     MakefileParser makefile;
1390 
1391     void analyzeLine(const(char)[] line) {
1392         // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this?
1393         immutable magic_nr = 2048;
1394         if (line.length > magic_nr) {
1395             // The byLine split may fail and thus result in one huge line.
1396             // The result of this is that regex's that use backtracking become really slow.
1397             // By skipping these lines dextool at list doesn't hang.
1398             logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...",
1399                     line.length, magic_nr);
1400             return;
1401         }
1402 
1403         foreach (const p; tc_analyze_builtin) {
1404             final switch (p) {
1405             case TestCaseAnalyzeBuiltin.gtest:
1406                 gtest.process(line, app);
1407                 break;
1408             case TestCaseAnalyzeBuiltin.ctest:
1409                 ctest.process(line, app);
1410                 break;
1411             case TestCaseAnalyzeBuiltin.makefile:
1412                 makefile.process(line, app);
1413                 break;
1414             }
1415         }
1416     }
1417 
1418     const(char)[] buf;
1419     void parseLine() {
1420         import std.algorithm : countUntil;
1421 
1422         try {
1423             const idx = buf.countUntil('\n');
1424             if (idx != -1) {
1425                 analyzeLine(buf[0 .. idx]);
1426                 if (idx < buf.length) {
1427                     buf = buf[idx + 1 .. $];
1428                 } else {
1429                     buf = null;
1430                 }
1431             }
1432         } catch (Exception e) {
1433             logger.warning("A error encountered when trying to analyze the output from the test suite. Dumping the rest of the buffer")
1434                 .collectException;
1435             logger.warning(e.msg).collectException;
1436             buf = null;
1437         }
1438     }
1439 
1440     foreach (d; output.map!(a => a.byUTF8.array)) {
1441         buf ~= d;
1442         parseLine;
1443     }
1444     while (!buf.empty) {
1445         parseLine;
1446     }
1447 
1448     return true;
1449 }
1450 
1451 /// Returns: path to a tmp directory or null on failure.
1452 string createTmpDir() @safe nothrow {
1453     import std.random : uniform;
1454     import std.format : format;
1455     import std.file : mkdir;
1456 
1457     string test_tmp_output;
1458 
1459     // try 5 times or bailout
1460     foreach (const _; 0 .. 5) {
1461         try {
1462             auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong);
1463             mkdir(tmp);
1464             test_tmp_output = AbsolutePath(FileName(tmp));
1465             break;
1466         } catch (Exception e) {
1467             logger.warning(e.msg).collectException;
1468         }
1469     }
1470 
1471     if (test_tmp_output.length == 0) {
1472         logger.warning("Unable to create a temporary directory to store stdout/stderr in")
1473             .collectException;
1474     }
1475 
1476     return test_tmp_output;
1477 }
1478 
1479 /** Compare the old test cases with those that have been found this run.
1480  *
1481  * TODO: the side effect that this function print to the console is NOT good.
1482  */
1483 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow {
1484     bool rval;
1485 
1486     auto new_tcs = found_tcs.setDifference(old_tcs);
1487     foreach (tc; new_tcs.toRange) {
1488         logger.info(!rval, "Found new test case(s):").collectException;
1489         logger.infof("%s", tc).collectException;
1490         rval = true;
1491     }
1492 
1493     return rval;
1494 }
1495 
1496 /** Compare old and new test cases to print those that have been removed.
1497  */
1498 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow {
1499     auto diff = old_tcs.setDifference(changed_tcs);
1500     auto removed = diff.toArray;
1501 
1502     logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException;
1503     foreach (tc; removed) {
1504         logger.infof("%s", tc).collectException;
1505     }
1506 }
1507 
1508 /// Returns: true if all tests cases have unique identifiers
1509 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow {
1510     Set!TestCase checked;
1511     bool conflict;
1512 
1513     foreach (tc; found_tcs) {
1514         if (checked.contains(tc)) {
1515             logger.info(!conflict,
1516                     "Found test cases that do not have global, unique identifiers")
1517                 .collectException;
1518             logger.info(!conflict,
1519                     "This make the report of test cases that has killed zero mutants unreliable")
1520                 .collectException;
1521             logger.info("%s", tc).collectException;
1522             conflict = true;
1523         }
1524     }
1525 }
1526 
1527 /** Paths stored will be removed automatically either when manually called or goes out of scope.
1528  */
1529 class AutoCleanup {
1530     private string[] remove_dirs;
1531 
1532     void add(AbsolutePath p) @safe nothrow {
1533         remove_dirs ~= cast(string) p;
1534     }
1535 
1536     // trusted: the paths are forced to be valid paths.
1537     void cleanup() @trusted nothrow {
1538         import std.file : rmdirRecurse, exists;
1539 
1540         foreach (ref p; remove_dirs.filter!(a => !a.empty)) {
1541             try {
1542                 if (exists(p))
1543                     rmdirRecurse(p);
1544                 if (!exists(p))
1545                     p = null;
1546             } catch (Exception e) {
1547                 logger.info(e.msg).collectException;
1548             }
1549         }
1550 
1551         remove_dirs = remove_dirs.filter!(a => !a.empty).array;
1552     }
1553 }
1554 
1555 /// The result of testing a mutant.
1556 struct MutationTestResult {
1557     import process : DrainElement;
1558 
1559     static struct NoResult {
1560     }
1561 
1562     static struct StatusUpdate {
1563         MutationId id;
1564         Mutation.Status status;
1565         Duration testTime;
1566         TestCase[] testCases;
1567         DrainElement[] output;
1568     }
1569 
1570     alias Value = SumType!(NoResult, StatusUpdate);
1571     Value value;
1572 
1573     void opAssign(MutationTestResult rhs) @trusted pure nothrow @nogc {
1574         this.value = rhs.value;
1575     }
1576 
1577     void opAssign(StatusUpdate rhs) @trusted pure nothrow @nogc {
1578         this.value = Value(rhs);
1579     }
1580 }