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