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 std.datetime : SysTime;
15 import std.typecons : Nullable, NullableRef, nullableRef;
16 import std.exception : collectException;
17 
18 import logger = std.experimental.logger;
19 
20 import blob_model : Blob, Uri;
21 
22 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
23     NextMutationEntry, spinSqlQuery;
24 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
25 import dextool.plugin.mutate.backend.type : Mutation;
26 import dextool.plugin.mutate.config;
27 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin;
28 import dextool.type : AbsolutePath, ShellCommand, ExitStatusType, FileName, DirName;
29 
30 @safe:
31 
32 auto makeTestMutant() {
33     return BuildTestMutant();
34 }
35 
36 private:
37 
38 struct BuildTestMutant {
39 @safe:
40 nothrow:
41 
42     import dextool.plugin.mutate.type : MutationKind;
43 
44     private struct InternalData {
45         Mutation.Kind[] mut_kinds;
46         FilesysIO filesys_io;
47         ConfigMutationTest config;
48     }
49 
50     private InternalData data;
51 
52     auto config(ConfigMutationTest c) {
53         data.config = c;
54         return this;
55     }
56 
57     auto mutations(MutationKind[] v) {
58         import dextool.plugin.mutate.backend.utility : toInternal;
59 
60         data.mut_kinds = toInternal(v);
61         return this;
62     }
63 
64     ExitStatusType run(ref Database db, FilesysIO fio) nothrow {
65         auto mutationFactory(DriverData data, Duration test_base_timeout) @safe {
66             import std.typecons : Unique;
67 
68             static struct Rval {
69                 ImplMutationDriver impl;
70                 MutationTestDriver!(ImplMutationDriver*) driver;
71 
72                 this(DriverData d, Duration test_base_timeout) {
73                     this.impl = ImplMutationDriver(d.filesysIO, d.db, d.autoCleanup,
74                             d.mutKind, d.conf.mutationCompile, d.conf.mutationTester, d.conf.mutationTestCaseAnalyze,
75                             d.conf.mutationTestCaseBuiltin, test_base_timeout);
76 
77                     this.driver = MutationTestDriver!(ImplMutationDriver*)(() @trusted {
78                         return &impl;
79                     }());
80                 }
81 
82                 alias driver this;
83             }
84 
85             return Unique!Rval(new Rval(data, test_base_timeout));
86         }
87 
88         // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope
89         auto db_ref = () @trusted { return nullableRef(&db); }();
90 
91         auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config);
92 
93         auto test_driver_impl = ImplTestDriver!mutationFactory(driver_data);
94         auto test_driver_impl_ref = () @trusted {
95             return nullableRef(&test_driver_impl);
96         }();
97         auto test_driver = TestDriver!(typeof(test_driver_impl_ref))(test_driver_impl_ref);
98 
99         while (test_driver.isRunning) {
100             test_driver.execute;
101         }
102 
103         return test_driver.status;
104     }
105 }
106 
107 immutable stdoutLog = "stdout.log";
108 immutable stderrLog = "stderr.log";
109 
110 struct DriverData {
111     NullableRef!Database db;
112     FilesysIO filesysIO;
113     Mutation.Kind[] mutKind;
114     AutoCleanup autoCleanup;
115     ConfigMutationTest conf;
116 }
117 
118 /** Run the test suite to verify a mutation.
119  *
120  * Params:
121  *  p = ?
122  *  timeout = timeout threshold.
123  */
124 Mutation.Status runTester(WatchdogT)(ShellCommand compile_p, ShellCommand tester_p,
125         AbsolutePath test_output_dir, WatchdogT watchdog, FilesysIO fio) nothrow {
126     import std.algorithm : among;
127     import std.datetime.stopwatch : StopWatch;
128     import dextool.plugin.mutate.backend.linux_process : spawnSession, tryWait, kill, wait;
129     import std.stdio : File;
130     import core.sys.posix.signal : SIGKILL;
131     import dextool.plugin.mutate.backend.utility : rndSleep;
132 
133     Mutation.Status rval;
134 
135     try {
136         auto p = spawnSession(compile_p.program ~ compile_p.arguments);
137         auto res = p.wait;
138         if (res.terminated && res.status != 0)
139             return Mutation.Status.killedByCompiler;
140         else if (!res.terminated) {
141             logger.warning("unknown error when executing the compiler").collectException;
142             return Mutation.Status.unknown;
143         }
144     } catch (Exception e) {
145         logger.warning(e.msg).collectException;
146     }
147 
148     string stdout_p;
149     string stderr_p;
150 
151     if (test_output_dir.length != 0) {
152         import std.path : buildPath;
153 
154         stdout_p = buildPath(test_output_dir, stdoutLog);
155         stderr_p = buildPath(test_output_dir, stderrLog);
156     }
157 
158     try {
159         auto p = spawnSession(tester_p.program ~ tester_p.arguments, stdout_p, stderr_p);
160         // trusted: killing the process started in this scope
161         void cleanup() @safe nothrow {
162             import core.sys.posix.signal : SIGKILL;
163 
164             if (rval.among(Mutation.Status.timeout, Mutation.Status.unknown)) {
165                 kill(p, SIGKILL);
166                 wait(p);
167             }
168         }
169 
170         scope (exit)
171             cleanup;
172 
173         rval = Mutation.Status.timeout;
174         watchdog.start;
175         while (watchdog.isOk) {
176             auto res = tryWait(p);
177             if (res.terminated) {
178                 if (res.status == 0)
179                     rval = Mutation.Status.alive;
180                 else
181                     rval = Mutation.Status.killed;
182                 break;
183             }
184 
185             rndSleep(10.dur!"msecs", 50);
186         }
187     } catch (Exception e) {
188         // unable to for example execute the test suite
189         logger.warning(e.msg).collectException;
190         return Mutation.Status.unknown;
191     }
192 
193     return rval;
194 }
195 
196 struct MeasureTestDurationResult {
197     ExitStatusType status;
198     Duration runtime;
199 }
200 
201 /**
202  * If the tests fail (exit code isn't 0) any time then they are too unreliable
203  * to use for mutation testing.
204  *
205  * The runtime is the lowest of the three executions.
206  *
207  * Params:
208  *  p = ?
209  */
210 MeasureTestDurationResult measureTesterDuration(ShellCommand cmd) nothrow {
211     if (cmd.program.length == 0) {
212         collectException(logger.error("No test suite runner specified (--mutant-tester)"));
213         return MeasureTestDurationResult(ExitStatusType.Errors);
214     }
215 
216     auto any_failure = ExitStatusType.Ok;
217 
218     void fun() {
219         import std.process : execute;
220 
221         auto res = execute(cmd.program ~ cmd.arguments);
222         if (res.status != 0)
223             any_failure = ExitStatusType.Errors;
224     }
225 
226     import std.datetime.stopwatch : benchmark;
227     import std.algorithm : minElement, map;
228     import core.time : dur;
229 
230     try {
231         auto bench = benchmark!fun(3);
232 
233         if (any_failure != ExitStatusType.Ok)
234             return MeasureTestDurationResult(ExitStatusType.Errors);
235 
236         auto a = (cast(long)((bench[0].total!"msecs") / 3.0)).dur!"msecs";
237         return MeasureTestDurationResult(ExitStatusType.Ok, a);
238     } catch (Exception e) {
239         collectException(logger.error(e.msg));
240         return MeasureTestDurationResult(ExitStatusType.Errors);
241     }
242 }
243 
244 enum MutationDriverSignal {
245     /// stay in the current state
246     stop,
247     /// advance to the next state
248     next,
249     /// All mutants are tested. Stopping mutation testing
250     allMutantsTested,
251     /// An error occured when interacting with the filesystem (fatal). Stopping all mutation testing
252     filesysError,
253     /// An error for a single mutation. It is skipped.
254     mutationError,
255     /// The test suite is unreliable which mean the mutant should be re-tested.
256     unstableTests,
257 }
258 
259 /** Drive the control flow when testing **a** mutant.
260  *
261  * The architecture assume that there will be behavior changes therefore a
262  * strict FSM that separate the context, action and next_state.
263  *
264  * The intention is to separate the control flow from the implementation of the
265  * actions that are done when mutation testing.
266  */
267 struct MutationTestDriver(ImplT) {
268     import std.experimental.typecons : Final;
269 
270     /// The internal state of the FSM.
271     private enum State {
272         none,
273         initialize,
274         mutateCode,
275         testMutant,
276         restoreCode,
277         testCaseAnalyze,
278         storeResult,
279         done,
280         allMutantsTested,
281         filesysError,
282         /// happens when an error occurs during mutations testing but that do not prohibit testing of other mutants
283         noResultRestoreCode,
284         noResult,
285     }
286 
287     private {
288         State st;
289         ImplT impl;
290     }
291 
292     this(ImplT impl) {
293         this.impl = impl;
294     }
295 
296     /// Returns: true as long as the driver is processing a mutant.
297     bool isRunning() {
298         import std.algorithm : among;
299 
300         return st.among(State.done, State.noResult, State.filesysError, State.allMutantsTested) == 0;
301     }
302 
303     bool stopBecauseError() {
304         return st == State.filesysError;
305     }
306 
307     /// Returns: true when the mutation testing should be stopped
308     bool stopMutationTesting() {
309         return st == State.allMutantsTested;
310     }
311 
312     void execute() {
313         import dextool.fsm : makeCallbacks;
314 
315         const auto signal = impl.signal;
316 
317         debug auto old_st = st;
318 
319         st = nextState(st, signal);
320 
321         debug logger.trace(old_st, "->", st, ":", signal).collectException;
322 
323         mixin(makeCallbacks!State().switchOn("st").callbackOn("impl").finalize);
324     }
325 
326     private static State nextState(immutable State current, immutable MutationDriverSignal signal) @safe pure nothrow @nogc {
327         State next_ = current;
328 
329         final switch (current) {
330         case State.none:
331             next_ = State.initialize;
332             break;
333         case State.initialize:
334             if (signal == MutationDriverSignal.next)
335                 next_ = State.mutateCode;
336             break;
337         case State.mutateCode:
338             if (signal == MutationDriverSignal.next)
339                 next_ = State.testMutant;
340             else if (signal == MutationDriverSignal.allMutantsTested)
341                 next_ = State.allMutantsTested;
342             else if (signal == MutationDriverSignal.filesysError)
343                 next_ = State.filesysError;
344             else if (signal == MutationDriverSignal.mutationError)
345                 next_ = State.noResultRestoreCode;
346             break;
347         case State.testMutant:
348             if (signal == MutationDriverSignal.next)
349                 next_ = State.testCaseAnalyze;
350             else if (signal == MutationDriverSignal.mutationError)
351                 next_ = State.noResultRestoreCode;
352             else if (signal == MutationDriverSignal.allMutantsTested)
353                 next_ = State.allMutantsTested;
354             break;
355         case State.testCaseAnalyze:
356             if (signal == MutationDriverSignal.next)
357                 next_ = State.restoreCode;
358             else if (signal == MutationDriverSignal.mutationError)
359                 next_ = State.noResultRestoreCode;
360             else if (signal == MutationDriverSignal.unstableTests)
361                 next_ = State.noResultRestoreCode;
362             break;
363         case State.restoreCode:
364             if (signal == MutationDriverSignal.next)
365                 next_ = State.storeResult;
366             else if (signal == MutationDriverSignal.filesysError)
367                 next_ = State.filesysError;
368             break;
369         case State.storeResult:
370             if (signal == MutationDriverSignal.next)
371                 next_ = State.done;
372             break;
373         case State.done:
374             break;
375         case State.allMutantsTested:
376             break;
377         case State.filesysError:
378             break;
379         case State.noResultRestoreCode:
380             next_ = State.noResult;
381             break;
382         case State.noResult:
383             break;
384         }
385 
386         return next_;
387     }
388 }
389 
390 /** Implementation of the actions during the test of a mutant.
391  *
392  * The intention is that this driver do NOT control the flow.
393  */
394 struct ImplMutationDriver {
395     import std.datetime.stopwatch : StopWatch;
396     import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
397 
398 nothrow:
399 
400     FilesysIO fio;
401     NullableRef!Database db;
402 
403     StopWatch sw;
404     MutationDriverSignal driver_sig;
405 
406     Nullable!MutationEntry mutp;
407     AbsolutePath mut_file;
408     Blob original;
409 
410     const(Mutation.Kind)[] mut_kind;
411     const TestCaseAnalyzeBuiltin[] tc_analyze_builtin;
412 
413     ShellCommand compile_cmd;
414     ShellCommand test_cmd;
415     AbsolutePath test_case_cmd;
416     Duration tester_runtime;
417 
418     /// Temporary directory where stdout/stderr should be written.
419     AbsolutePath test_tmp_output;
420 
421     Mutation.Status mut_status;
422 
423     GatherTestCase test_cases;
424 
425     AutoCleanup auto_cleanup;
426 
427     this(FilesysIO fio, NullableRef!Database db, AutoCleanup auto_cleanup,
428             Mutation.Kind[] mut_kind, ShellCommand compile_cmd,
429             ShellCommand test_cmd, AbsolutePath test_case_cmd,
430             TestCaseAnalyzeBuiltin[] tc_analyze_builtin, Duration tester_runtime) {
431         this.fio = fio;
432         this.db = db;
433         this.mut_kind = mut_kind;
434         this.compile_cmd = compile_cmd;
435         this.test_cmd = test_cmd;
436         this.test_case_cmd = test_case_cmd;
437         this.tc_analyze_builtin = tc_analyze_builtin;
438         this.tester_runtime = tester_runtime;
439         this.test_cases = new GatherTestCase;
440         this.auto_cleanup = auto_cleanup;
441     }
442 
443     void none() {
444     }
445 
446     void done() {
447     }
448 
449     void allMutantsTested() {
450     }
451 
452     void filesysError() {
453         logger.warning("Filesystem error").collectException;
454     }
455 
456     void noResultRestoreCode() {
457         restoreCode;
458     }
459 
460     void noResult() {
461     }
462 
463     void initialize() {
464         sw.start;
465         driver_sig = MutationDriverSignal.next;
466     }
467 
468     void mutateCode() {
469         import core.thread : Thread;
470         import std.random : uniform;
471         import dextool.plugin.mutate.backend.generate_mutant : generateMutant,
472             GenerateMutantResult, GenerateMutantStatus;
473 
474         driver_sig = MutationDriverSignal.stop;
475 
476         auto next_m = spinSqlQuery!(() { return db.nextMutation(mut_kind); });
477         if (next_m.st == NextMutationEntry.Status.done) {
478             logger.info("Done! All mutants are tested").collectException;
479             driver_sig = MutationDriverSignal.allMutantsTested;
480             return;
481         } else {
482             mutp = next_m.entry;
483         }
484 
485         try {
486             mut_file = AbsolutePath(FileName(mutp.file), DirName(fio.getOutputDir));
487             original = fio.makeInput(mut_file);
488         } catch (Exception e) {
489             logger.error(e.msg).collectException;
490             logger.warning("Unable to read ", mut_file).collectException;
491             driver_sig = MutationDriverSignal.filesysError;
492             return;
493         }
494 
495         // mutate
496         try {
497             auto fout = fio.makeOutput(mut_file);
498             auto mut_res = generateMutant(db.get, mutp, original, fout);
499 
500             final switch (mut_res.status) with (GenerateMutantStatus) {
501             case error:
502                 driver_sig = MutationDriverSignal.mutationError;
503                 break;
504             case filesysError:
505                 driver_sig = MutationDriverSignal.filesysError;
506                 break;
507             case databaseError:
508                 // such as when the database is locked
509                 driver_sig = MutationDriverSignal.mutationError;
510                 break;
511             case checksumError:
512                 driver_sig = MutationDriverSignal.filesysError;
513                 break;
514             case noMutation:
515                 driver_sig = MutationDriverSignal.mutationError;
516                 break;
517             case ok:
518                 driver_sig = MutationDriverSignal.next;
519                 try {
520                     logger.infof("%s from '%s' to '%s' in %s:%s:%s", mutp.id,
521                             cast(const(char)[]) mut_res.from, cast(const(char)[]) mut_res.to,
522                             mut_file, mutp.sloc.line, mutp.sloc.column);
523 
524                 } catch (Exception e) {
525                     logger.warning("Mutation ID", e.msg);
526                 }
527                 break;
528             }
529         } catch (Exception e) {
530             logger.warning(e.msg).collectException;
531             driver_sig = MutationDriverSignal.mutationError;
532         }
533     }
534 
535     void testMutant() {
536         import dextool.type : Path;
537 
538         assert(!mutp.isNull);
539         driver_sig = MutationDriverSignal.mutationError;
540 
541         if (test_case_cmd.length != 0 || tc_analyze_builtin.length != 0) {
542             try {
543                 auto tmpdir = createTmpDir(mutp.id);
544                 if (tmpdir.length == 0)
545                     return;
546                 test_tmp_output = Path(tmpdir).AbsolutePath;
547                 auto_cleanup.add(test_tmp_output);
548             } catch (Exception e) {
549                 logger.warning(e.msg).collectException;
550                 return;
551             }
552         }
553 
554         try {
555             import dextool.plugin.mutate.backend.watchdog : StaticTime;
556 
557             auto watchdog = StaticTime!StopWatch(tester_runtime);
558 
559             mut_status = runTester(compile_cmd, test_cmd, test_tmp_output, watchdog, fio);
560             driver_sig = MutationDriverSignal.next;
561         } catch (Exception e) {
562             logger.warning(e.msg).collectException;
563         }
564     }
565 
566     void testCaseAnalyze() {
567         import std.algorithm : splitter, map, filter;
568         import std.array : array;
569         import std.ascii : newline;
570         import std.file : exists;
571         import std.path : buildPath;
572         import std.process : execute;
573         import std.string : strip;
574 
575         if (mut_status != Mutation.Status.killed || test_tmp_output.length == 0) {
576             driver_sig = MutationDriverSignal.next;
577             return;
578         }
579 
580         driver_sig = MutationDriverSignal.mutationError;
581 
582         try {
583             auto stdout_ = buildPath(test_tmp_output, stdoutLog);
584             auto stderr_ = buildPath(test_tmp_output, stderrLog);
585 
586             if (!exists(stdout_) || !exists(stderr_)) {
587                 logger.warningf("Unable to open %s and %s for test case analyze", stdout_, stderr_);
588                 return;
589             }
590 
591             auto gather_tc = new GatherTestCase;
592 
593             // the post processer must succeeed for the data to be stored. if
594             // is considered a major error that may corrupt existing data if it
595             // fails.
596             bool success = true;
597 
598             if (test_case_cmd.length != 0) {
599                 success = success && externalProgram([
600                         test_case_cmd, stdout_, stderr_
601                         ], gather_tc);
602             }
603             if (tc_analyze_builtin.length != 0) {
604                 success = success && builtin(fio.getOutputDir, [
605                         stdout_, stderr_
606                         ], tc_analyze_builtin, gather_tc);
607             }
608 
609             if (gather_tc.unstable.length != 0) {
610                 logger.warningf("Unstable test cases found: [%-(%s, %)]",
611                         gather_tc.unstableAsArray);
612                 logger.info(
613                         "As configured the result is ignored which will force the mutant to be re-tested");
614                 driver_sig = MutationDriverSignal.unstableTests;
615             } else if (success) {
616                 test_cases = gather_tc;
617                 driver_sig = MutationDriverSignal.next;
618             }
619         } catch (Exception e) {
620             logger.warning(e.msg).collectException;
621         }
622     }
623 
624     void storeResult() {
625         import std.algorithm : sort, map;
626 
627         driver_sig = MutationDriverSignal.next;
628 
629         sw.stop;
630 
631         const cnt_action = () {
632             if (mut_status == Mutation.Status.alive)
633                 return Database.CntAction.incr;
634             return Database.CntAction.reset;
635         }();
636 
637         spinSqlQuery!(() {
638             db.updateMutation(mutp.id, mut_status, sw.peek, test_cases.failedAsArray, cnt_action);
639         });
640 
641         logger.infof("%s %s (%s)", mutp.id, mut_status, sw.peek).collectException;
642         logger.infof(test_cases.failed.length != 0, `%s killed by [%-(%s, %)]`,
643                 mutp.id, test_cases.failedAsArray.sort.map!"a.name").collectException;
644     }
645 
646     void restoreCode() {
647         driver_sig = MutationDriverSignal.next;
648 
649         // restore the original file.
650         try {
651             fio.makeOutput(mut_file).write(original.content);
652         } catch (Exception e) {
653             logger.error(e.msg).collectException;
654             // fatal error because being unable to restore a file prohibit
655             // future mutations.
656             driver_sig = MutationDriverSignal.filesysError;
657         }
658 
659         if (test_tmp_output.length != 0) {
660             import std.file : rmdirRecurse;
661 
662             // trusted: test_tmp_output is tested to be valid data.
663             () @trusted {
664                 try {
665                     rmdirRecurse(test_tmp_output);
666                 } catch (Exception e) {
667                     logger.info(e.msg).collectException;
668                 }
669             }();
670         }
671     }
672 
673     /// Signal from the ImplMutationDriver to the Driver.
674     auto signal() {
675         return driver_sig;
676     }
677 }
678 
679 enum TestDriverSignal {
680     stop,
681     next,
682     allMutantsTested,
683     unreliableTestSuite,
684     compilationError,
685     mutationError,
686     timeoutUnchanged,
687     sanityCheckFailed,
688 }
689 
690 struct TestDriver(ImplT) {
691     private enum State {
692         none,
693         initialize,
694         sanityCheck,
695         updateAndResetAliveMutants,
696         resetOldMutants,
697         cleanupTempDirs,
698         checkMutantsLeft,
699         preCompileSut,
700         measureTestSuite,
701         preMutationTest,
702         mutationTest,
703         checkTimeout,
704         incrWatchdog,
705         resetTimeout,
706         done,
707         error,
708     }
709 
710     private {
711         State st;
712         ImplT impl;
713     }
714 
715     this(ImplT impl) {
716         this.impl = impl;
717     }
718 
719     bool isRunning() {
720         import std.algorithm : among;
721 
722         return st.among(State.done, State.error) == 0;
723     }
724 
725     ExitStatusType status() {
726         if (st == State.done)
727             return ExitStatusType.Ok;
728         else
729             return ExitStatusType.Errors;
730     }
731 
732     void execute() {
733         import dextool.fsm : makeCallbacks;
734 
735         const auto signal = impl.signal;
736 
737         debug auto old_st = st;
738 
739         st = nextState(st, signal);
740 
741         debug logger.trace(old_st, "->", st, ":", signal).collectException;
742 
743         mixin(makeCallbacks!State().switchOn("st").callbackOn("impl").finalize);
744     }
745 
746     private static State nextState(const State current, const TestDriverSignal signal) {
747         State next_ = current;
748 
749         final switch (current) with (State) {
750         case none:
751             next_ = State.initialize;
752             break;
753         case initialize:
754             if (signal == TestDriverSignal.next)
755                 next_ = State.sanityCheck;
756             break;
757         case sanityCheck:
758             if (signal == TestDriverSignal.next)
759                 next_ = State.preCompileSut;
760             else if (signal == TestDriverSignal.sanityCheckFailed)
761                 next_ = State.error;
762             break;
763         case updateAndResetAliveMutants:
764             next_ = resetOldMutants;
765             break;
766         case resetOldMutants:
767             next_ = checkMutantsLeft;
768             break;
769         case checkMutantsLeft:
770             if (signal == TestDriverSignal.next)
771                 next_ = State.measureTestSuite;
772             else if (signal == TestDriverSignal.allMutantsTested)
773                 next_ = State.done;
774             break;
775         case preCompileSut:
776             if (signal == TestDriverSignal.next)
777                 next_ = State.updateAndResetAliveMutants;
778             else if (signal == TestDriverSignal.compilationError)
779                 next_ = State.error;
780             break;
781         case measureTestSuite:
782             if (signal == TestDriverSignal.next)
783                 next_ = State.cleanupTempDirs;
784             else if (signal == TestDriverSignal.unreliableTestSuite)
785                 next_ = State.error;
786             break;
787         case cleanupTempDirs:
788             next_ = preMutationTest;
789             break;
790         case preMutationTest:
791             next_ = State.mutationTest;
792             break;
793         case mutationTest:
794             if (signal == TestDriverSignal.next)
795                 next_ = State.cleanupTempDirs;
796             else if (signal == TestDriverSignal.allMutantsTested)
797                 next_ = State.checkTimeout;
798             else if (signal == TestDriverSignal.mutationError)
799                 next_ = State.error;
800             break;
801         case checkTimeout:
802             if (signal == TestDriverSignal.timeoutUnchanged)
803                 next_ = State.done;
804             else if (signal == TestDriverSignal.next)
805                 next_ = State.incrWatchdog;
806             break;
807         case incrWatchdog:
808             next_ = State.resetTimeout;
809             break;
810         case resetTimeout:
811             if (signal == TestDriverSignal.next)
812                 next_ = State.cleanupTempDirs;
813             break;
814         case done:
815             break;
816         case error:
817             break;
818         }
819 
820         return next_;
821     }
822 }
823 
824 struct ImplTestDriver(alias mutationDriverFactory) {
825     import std.traits : ReturnType;
826     import dextool.plugin.mutate.backend.watchdog : ProgressivWatchdog;
827 
828 nothrow:
829     DriverData data;
830 
831     ProgressivWatchdog prog_wd;
832     TestDriverSignal driver_sig;
833     ReturnType!mutationDriverFactory mut_driver;
834     long last_timeout_mutant_count = long.max;
835 
836     this(DriverData data) {
837         this.data = data;
838     }
839 
840     void none() {
841     }
842 
843     void done() {
844         data.autoCleanup.cleanup;
845     }
846 
847     void error() {
848         data.autoCleanup.cleanup;
849     }
850 
851     void initialize() {
852         driver_sig = TestDriverSignal.next;
853     }
854 
855     void sanityCheck() {
856         // #SPC-sanity_check_db_vs_filesys
857         import dextool.type : Path;
858         import dextool.plugin.mutate.backend.utility : checksum, trustedRelativePath;
859         import dextool.plugin.mutate.backend.type : Checksum;
860 
861         driver_sig = TestDriverSignal.sanityCheckFailed;
862 
863         const(Path)[] files;
864         spinSqlQuery!(() { files = data.db.getFiles; });
865 
866         bool has_sanity_check_failed;
867         for (size_t i; i < files.length;) {
868             Checksum db_checksum;
869             spinSqlQuery!(() { db_checksum = data.db.getFileChecksum(files[i]); });
870 
871             try {
872                 auto abs_f = AbsolutePath(FileName(files[i]),
873                         DirName(cast(string) data.filesysIO.getOutputDir));
874                 auto f_checksum = checksum(data.filesysIO.makeInput(abs_f).content[]);
875                 if (db_checksum != f_checksum) {
876                     logger.errorf("Mismatch between the file on the filesystem and the analyze of '%s'",
877                             abs_f);
878                     has_sanity_check_failed = true;
879                 }
880             } catch (Exception e) {
881                 // assume it is a problem reading the file or something like that.
882                 has_sanity_check_failed = true;
883                 logger.trace(e.msg).collectException;
884             }
885 
886             // all done. continue with the next file
887             ++i;
888         }
889 
890         if (has_sanity_check_failed) {
891             driver_sig = TestDriverSignal.sanityCheckFailed;
892             logger.error("Detected that one or more file has changed since last analyze where done")
893                 .collectException;
894             logger.error("Either restore the files to the previous state or rerun the analyzer")
895                 .collectException;
896         } else {
897             logger.info("Sanity check passed. Files on the filesystem are consistent")
898                 .collectException;
899             driver_sig = TestDriverSignal.next;
900         }
901     }
902 
903     // TODO: refactor. This method is too long.
904     void updateAndResetAliveMutants() {
905         import core.time : dur;
906         import std.algorithm : map;
907         import std.datetime.stopwatch : StopWatch;
908         import std.path : buildPath;
909         import dextool.type : Path;
910         import dextool.plugin.mutate.backend.type : TestCase;
911 
912         driver_sig = TestDriverSignal.next;
913 
914         if (data.conf.mutationTestCaseAnalyze.length == 0
915                 && data.conf.mutationTestCaseBuiltin.length == 0)
916             return;
917 
918         AbsolutePath test_tmp_output;
919         try {
920             auto tmpdir = createTmpDir(0);
921             if (tmpdir.length == 0)
922                 return;
923             test_tmp_output = Path(tmpdir).AbsolutePath;
924             data.autoCleanup.add(test_tmp_output);
925         } catch (Exception e) {
926             logger.warning(e.msg).collectException;
927             return;
928         }
929 
930         TestCase[] all_found_tc;
931 
932         try {
933             import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
934             import dextool.plugin.mutate.backend.watchdog : StaticTime;
935 
936             auto stdout_ = buildPath(test_tmp_output, stdoutLog);
937             auto stderr_ = buildPath(test_tmp_output, stderrLog);
938 
939             // using an unreasonable timeout because this is more intended to reuse the functionality in runTester
940             auto watchdog = StaticTime!StopWatch(999.dur!"hours");
941             runTester(data.conf.mutationCompile, data.conf.mutationTester,
942                     test_tmp_output, watchdog, data.filesysIO);
943 
944             auto gather_tc = new GatherTestCase;
945 
946             if (data.conf.mutationTestCaseAnalyze.length != 0) {
947                 externalProgram([
948                         data.conf.mutationTestCaseAnalyze, stdout_, stderr_
949                         ], gather_tc);
950                 logger.warningf(gather_tc.unstable.length != 0,
951                         "Unstable test cases found: [%-(%s, %)]", gather_tc.unstableAsArray);
952             }
953             if (data.conf.mutationTestCaseBuiltin.length != 0) {
954                 builtin(data.filesysIO.getOutputDir, [stdout_, stderr_],
955                         data.conf.mutationTestCaseBuiltin, gather_tc);
956             }
957 
958             all_found_tc = gather_tc.foundAsArray;
959         } catch (Exception e) {
960             logger.warning(e.msg).collectException;
961         }
962 
963         warnIfConflictingTestCaseIdentifiers(all_found_tc);
964 
965         // the test cases before anything has potentially changed.
966         Set!string old_tcs;
967         spinSqlQuery!(() {
968             foreach (tc; data.db.getDetectedTestCases)
969                 old_tcs.add(tc.name);
970         });
971 
972         final switch (data.conf.onRemovedTestCases) with (ConfigMutationTest.RemovedTestCases) {
973         case doNothing:
974             spinSqlQuery!(() { data.db.addDetectedTestCases(all_found_tc); });
975             break;
976         case remove:
977             import dextool.plugin.mutate.backend.database : MutationStatusId;
978 
979             MutationStatusId[] ids;
980             spinSqlQuery!(() { ids = data.db.setDetectedTestCases(all_found_tc); });
981             foreach (id; ids)
982                 spinSqlQuery!(() {
983                     data.db.updateMutationStatus(id, Mutation.Status.unknown);
984                 });
985             break;
986         }
987 
988         Set!string found_tcs;
989         spinSqlQuery!(() {
990             found_tcs = null;
991             foreach (tc; data.db.getDetectedTestCases)
992                 found_tcs.add(tc.name);
993         });
994 
995         printDroppedTestCases(old_tcs, found_tcs);
996 
997         const new_test_cases = hasNewTestCases(old_tcs, found_tcs);
998 
999         if (new_test_cases && data.conf.onNewTestCases == ConfigMutationTest
1000                 .NewTestCases.resetAlive) {
1001             logger.info("Resetting alive mutants").collectException;
1002             resetAliveMutants(data.db);
1003         }
1004     }
1005 
1006     void resetOldMutants() {
1007         import dextool.plugin.mutate.backend.database.type;
1008 
1009         if (data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing)
1010             return;
1011 
1012         logger.infof("Resetting the %s oldest mutants", data.conf.oldMutantsNr).collectException;
1013         MutationStatusTime[] oldest;
1014         spinSqlQuery!(() {
1015             oldest = data.db.getOldestMutants(data.mutKind, data.conf.oldMutantsNr);
1016         });
1017         foreach (const old; oldest) {
1018             logger.info("  Last updated ", old.updated).collectException;
1019             spinSqlQuery!(() {
1020                 data.db.updateMutationStatus(old.id, Mutation.Status.unknown);
1021             });
1022         }
1023     }
1024 
1025     void cleanupTempDirs() {
1026         driver_sig = TestDriverSignal.next;
1027         data.autoCleanup.cleanup;
1028     }
1029 
1030     void checkMutantsLeft() {
1031         driver_sig = TestDriverSignal.next;
1032 
1033         auto mutant = spinSqlQuery!(() {
1034             return data.db.nextMutation(data.mutKind);
1035         });
1036 
1037         if (mutant.st == NextMutationEntry.Status.done) {
1038             logger.info("Done! All mutants are tested").collectException;
1039             driver_sig = TestDriverSignal.allMutantsTested;
1040         }
1041     }
1042 
1043     void preCompileSut() {
1044         driver_sig = TestDriverSignal.compilationError;
1045 
1046         logger.info("Preparing for mutation testing by checking that the program and tests compile without any errors (no mutants injected)")
1047             .collectException;
1048 
1049         try {
1050             import std.process : execute;
1051 
1052             const comp_res = execute(
1053                     data.conf.mutationCompile.program ~ data.conf.mutationCompile.arguments);
1054 
1055             if (comp_res.status == 0) {
1056                 driver_sig = TestDriverSignal.next;
1057             } else {
1058                 logger.info(comp_res.output);
1059                 logger.error("Compiler command failed: ", comp_res.status);
1060             }
1061         } catch (Exception e) {
1062             // unable to for example execute the compiler
1063             logger.error(e.msg).collectException;
1064         }
1065     }
1066 
1067     void measureTestSuite() {
1068         driver_sig = TestDriverSignal.unreliableTestSuite;
1069 
1070         if (data.conf.mutationTesterRuntime.isNull) {
1071             logger.info("Measuring the time to run the tests: ",
1072                     data.conf.mutationTester).collectException;
1073             auto tester = measureTesterDuration(data.conf.mutationTester);
1074             if (tester.status == ExitStatusType.Ok) {
1075                 // The sampling of the test suite become too unreliable when the timeout is <1s.
1076                 // This is a quick and dirty fix.
1077                 // A proper fix requires an update of the sampler in runTester.
1078                 auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime;
1079                 logger.info("Tester measured to: ", t).collectException;
1080                 prog_wd = ProgressivWatchdog(t);
1081                 driver_sig = TestDriverSignal.next;
1082             } else {
1083                 logger.error(
1084                         "Test suite is unreliable. It must return exit status '0' when running with unmodified mutants")
1085                     .collectException;
1086             }
1087         } else {
1088             prog_wd = ProgressivWatchdog(data.conf.mutationTesterRuntime.get);
1089             driver_sig = TestDriverSignal.next;
1090         }
1091     }
1092 
1093     void preMutationTest() {
1094         driver_sig = TestDriverSignal.next;
1095         mut_driver = mutationDriverFactory(data, prog_wd.timeout);
1096     }
1097 
1098     void mutationTest() {
1099         if (mut_driver.isRunning) {
1100             mut_driver.execute();
1101             driver_sig = TestDriverSignal.stop;
1102         } else if (mut_driver.stopBecauseError) {
1103             driver_sig = TestDriverSignal.mutationError;
1104         } else if (mut_driver.stopMutationTesting) {
1105             driver_sig = TestDriverSignal.allMutantsTested;
1106         } else {
1107             driver_sig = TestDriverSignal.next;
1108         }
1109     }
1110 
1111     void checkTimeout() {
1112         driver_sig = TestDriverSignal.stop;
1113 
1114         auto entry = spinSqlQuery!(() {
1115             return data.db.timeoutMutants(data.mutKind);
1116         });
1117 
1118         try {
1119             if (!data.conf.mutationTesterRuntime.isNull) {
1120                 // the user have supplied a timeout thus ignore this algorithm
1121                 // for increasing the timeout
1122                 driver_sig = TestDriverSignal.timeoutUnchanged;
1123             } else if (entry.count == 0) {
1124                 driver_sig = TestDriverSignal.timeoutUnchanged;
1125             } else if (entry.count == last_timeout_mutant_count) {
1126                 // no change between current pool of timeout mutants and the previous
1127                 driver_sig = TestDriverSignal.timeoutUnchanged;
1128             } else if (entry.count < last_timeout_mutant_count) {
1129                 driver_sig = TestDriverSignal.next;
1130                 logger.info("Mutants with the status timeout: ", entry.count);
1131             }
1132 
1133             last_timeout_mutant_count = entry.count;
1134         } catch (Exception e) {
1135             logger.warning(e.msg).collectException;
1136         }
1137     }
1138 
1139     void incrWatchdog() {
1140         driver_sig = TestDriverSignal.next;
1141         prog_wd.incrTimeout;
1142         logger.info("Increasing timeout to: ", prog_wd.timeout).collectException;
1143     }
1144 
1145     void resetTimeout() {
1146         // database is locked
1147         driver_sig = TestDriverSignal.stop;
1148 
1149         try {
1150             data.db.resetMutant(data.mutKind, Mutation.Status.timeout, Mutation.Status.unknown);
1151             driver_sig = TestDriverSignal.next;
1152         } catch (Exception e) {
1153             logger.warning(e.msg).collectException;
1154         }
1155     }
1156 
1157     auto signal() {
1158         return driver_sig;
1159     }
1160 }
1161 
1162 private:
1163 
1164 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport;
1165 import dextool.plugin.mutate.backend.type : TestCase;
1166 import dextool.set;
1167 
1168 /// Run an external program that analyze the output from the test suite for test cases that failed.
1169 bool externalProgram(string[] cmd, TestCaseReport report) nothrow {
1170     import std.algorithm : copy, splitter, filter, map;
1171     import std.ascii : newline;
1172     import std.process : execute;
1173     import std.string : strip, startsWith;
1174     import dextool.plugin.mutate.backend.type : TestCase;
1175 
1176     immutable passed = "passed:";
1177     immutable failed = "failed:";
1178     immutable unstable = "unstable:";
1179 
1180     try {
1181         // [test_case_cmd, stdout_, stderr_]
1182         auto p = execute(cmd);
1183         if (p.status == 0) {
1184             foreach (l; p.output.splitter(newline).map!(a => a.strip)
1185                     .filter!(a => a.length != 0)) {
1186                 if (l.startsWith(passed))
1187                     report.reportFound(TestCase(l[passed.length .. $].strip));
1188                 else if (l.startsWith(failed))
1189                     report.reportFailed(TestCase(l[failed.length .. $].strip));
1190                 else if (l.startsWith(unstable))
1191                     report.reportUnstable(TestCase(l[unstable.length .. $].strip));
1192             }
1193             return true;
1194         } else {
1195             logger.warning(p.output);
1196             logger.warning("Failed to analyze the test case output");
1197             return false;
1198         }
1199     } catch (Exception e) {
1200         logger.warning(e.msg).collectException;
1201     }
1202 
1203     return false;
1204 }
1205 
1206 /** Analyze the output from the test suite with one of the builtin analyzers.
1207  *
1208  * trusted: because the paths to the File object are created by this program
1209  * and can thus not lead to memory related problems.
1210  */
1211 bool builtin(AbsolutePath reldir, string[] analyze_files,
1212         const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @trusted nothrow {
1213     import std.stdio : File;
1214     import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze;
1215     import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze;
1216     import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze;
1217 
1218     foreach (f; analyze_files) {
1219         auto gtest = GtestParser(reldir);
1220         CtestParser ctest;
1221         MakefileParser makefile;
1222 
1223         File* fin;
1224         try {
1225             fin = new File(f);
1226         } catch (Exception e) {
1227             logger.warning(e.msg).collectException;
1228             return false;
1229         }
1230 
1231         scope (exit)
1232             () {
1233             try {
1234                 fin.close;
1235                 destroy(fin);
1236             } catch (Exception e) {
1237                 logger.warning(e.msg).collectException;
1238             }
1239         }();
1240 
1241         // an invalid UTF-8 char shall only result in the rest of the file being skipped
1242         try {
1243             foreach (l; fin.byLine) {
1244                 // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this?
1245                 immutable magic_nr = 2048;
1246                 if (l.length > magic_nr) {
1247                     // The byLine split may fail and thus result in one huge line.
1248                     // The result of this is that regex's that use backtracking become really slow.
1249                     // By skipping these lines dextool at list doesn't hang.
1250                     logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...",
1251                             l.length, magic_nr);
1252                     continue;
1253                 }
1254 
1255                 foreach (const p; tc_analyze_builtin) {
1256                     final switch (p) {
1257                     case TestCaseAnalyzeBuiltin.gtest:
1258                         gtest.process(l, app);
1259                         break;
1260                     case TestCaseAnalyzeBuiltin.ctest:
1261                         ctest.process(l, app);
1262                         break;
1263                     case TestCaseAnalyzeBuiltin.makefile:
1264                         makefile.process(l, app);
1265                         break;
1266                     }
1267                 }
1268             }
1269         } catch (Exception e) {
1270             logger.warning(e.msg).collectException;
1271         }
1272     }
1273 
1274     return true;
1275 }
1276 
1277 /// Returns: path to a tmp directory or null on failure.
1278 string createTmpDir(long id) nothrow {
1279     import std.random : uniform;
1280     import std.format : format;
1281     import std.file : mkdir, exists;
1282 
1283     string test_tmp_output;
1284 
1285     // try 5 times or bailout
1286     foreach (const _; 0 .. 5) {
1287         try {
1288             auto tmp = format("dextool_tmp_id_%s_%s", id, uniform!ulong);
1289             mkdir(tmp);
1290             test_tmp_output = AbsolutePath(FileName(tmp));
1291             break;
1292         } catch (Exception e) {
1293             logger.warning(e.msg).collectException;
1294         }
1295     }
1296 
1297     if (test_tmp_output.length == 0) {
1298         logger.warning("Unable to create a temporary directory to store stdout/stderr in")
1299             .collectException;
1300     }
1301 
1302     return test_tmp_output;
1303 }
1304 
1305 /// Reset all alive mutants.
1306 void resetAliveMutants(ref Database db) @safe nothrow {
1307     import std.traits : EnumMembers;
1308 
1309     // there is no use in trying to limit the mutants to reset to those that
1310     // are part of "this" execution because new test cases can only mean one
1311     // thing: re-test all alive mutants.
1312 
1313     spinSqlQuery!(() {
1314         db.resetMutant([EnumMembers!(Mutation.Kind)], Mutation.Status.alive,
1315             Mutation.Status.unknown);
1316     });
1317 }
1318 
1319 /** Compare the old test cases with those that have been found this run.
1320  *
1321  * TODO: the side effect that this function print to the console is NOT good.
1322  */
1323 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow {
1324     bool rval;
1325 
1326     auto new_tcs = found_tcs.setDifference(old_tcs);
1327     foreach (tc; new_tcs.byKey) {
1328         logger.info(!rval, "Found new test case(s):").collectException;
1329         logger.infof("%s", tc).collectException;
1330         rval = true;
1331     }
1332 
1333     return rval;
1334 }
1335 
1336 /** Compare old and new test cases to print those that have been removed.
1337  */
1338 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow {
1339     auto diff = old_tcs.setDifference(changed_tcs);
1340     auto removed = diff.setToList!string;
1341 
1342     logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException;
1343     foreach (tc; removed) {
1344         logger.infof("%s", tc).collectException;
1345     }
1346 }
1347 
1348 /// Returns: true if all tests cases have unique identifiers
1349 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow {
1350     Set!TestCase checked;
1351     bool conflict;
1352 
1353     foreach (tc; found_tcs) {
1354         if (checked.contains(tc)) {
1355             logger.info(!conflict,
1356                     "Found test cases that do not have global, unique identifiers")
1357                 .collectException;
1358             logger.info(!conflict,
1359                     "This make the report of test cases that has killed zero mutants unreliable")
1360                 .collectException;
1361             logger.info("%s", tc).collectException;
1362             conflict = true;
1363         }
1364     }
1365 }
1366 
1367 /** Paths stored will be removed automatically either when manually called or goes out of scope.
1368  */
1369 class AutoCleanup {
1370     private string[] remove_dirs;
1371 
1372     void add(AbsolutePath p) @safe nothrow {
1373         remove_dirs ~= cast(string) p;
1374     }
1375 
1376     // trusted: the paths are forced to be valid paths.
1377     void cleanup() @trusted nothrow {
1378         import std.algorithm : filter;
1379         import std.array : array;
1380         import std.file : rmdirRecurse, exists;
1381 
1382         foreach (ref p; remove_dirs.filter!(a => a.length != 0)) {
1383             try {
1384                 if (exists(p))
1385                     rmdirRecurse(p);
1386                 if (!exists(p))
1387                     p = null;
1388             } catch (Exception e) {
1389                 logger.info(e.msg).collectException;
1390             }
1391         }
1392 
1393         remove_dirs = remove_dirs.filter!(a => a.length != 0).array;
1394     }
1395 }