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.time : Duration;
13 import std.typecons : Nullable, NullableRef, nullableRef;
14 import std.exception : collectException;
15 
16 import logger = std.experimental.logger;
17 
18 import dextool.type : AbsolutePath, ExitStatusType, FileName, DirName;
19 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
20     NextMutationEntry;
21 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
22 import dextool.plugin.mutate.backend.type : Mutation;
23 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin;
24 
25 @safe:
26 
27 auto makeTestMutant() {
28     return BuildTestMutant();
29 }
30 
31 private:
32 
33 struct BuildTestMutant {
34 @safe:
35 nothrow:
36 
37     import dextool.plugin.mutate.type : MutationKind;
38 
39     private struct InternalData {
40         Mutation.Kind[] mut_kinds;
41         AbsolutePath test_suite_execute_program;
42         AbsolutePath compile_program;
43         AbsolutePath test_case_analyze_program;
44         Nullable!Duration test_suite_execute_timeout;
45         FilesysIO filesys_io;
46         TestCaseAnalyzeBuiltin[] tc_analyze_builtin;
47     }
48 
49     private InternalData data;
50 
51     auto mutations(MutationKind[] v) {
52         import dextool.plugin.mutate.backend.utility;
53 
54         data.mut_kinds = toInternal(v);
55         return this;
56     }
57 
58     /// a program to execute that test the mutant. The mutant is marked as alive if the exit code is 0, otherwise it is dead.
59     auto testSuiteProgram(AbsolutePath v) {
60         data.test_suite_execute_program = v;
61         return this;
62     }
63 
64     /// program to use to compile the SUT + tests after a mutation has been performed.
65     auto compileProgram(AbsolutePath v) {
66         data.compile_program = v;
67         return this;
68     }
69 
70     /// The time it takes to run the tests.
71     auto testSuiteTimeout(Nullable!Duration v) {
72         data.test_suite_execute_timeout = v;
73         return this;
74     }
75 
76     auto testCaseAnalyzeProgram(AbsolutePath v) {
77         data.test_case_analyze_program = v;
78         return this;
79     }
80 
81     auto testCaseAnalyzeBuiltin(TestCaseAnalyzeBuiltin[] v) {
82         data.tc_analyze_builtin = v;
83         return this;
84     }
85 
86     ExitStatusType run(ref Database db, FilesysIO fio) nothrow {
87         auto mutationFactory(DriverData data, Duration test_base_timeout) @safe {
88             static class Rval {
89                 ImplMutationDriver impl;
90                 MutationTestDriver!(ImplMutationDriver*) driver;
91 
92                 this(DriverData d, Duration test_base_timeout) {
93                     this.impl = ImplMutationDriver(d.filesysIO, d.db, d.mutKind, d.compilerProgram,
94                             d.testProgram, d.testCaseAnalyzeProgram,
95                             d.testCaseAnalyzeBuiltin, test_base_timeout);
96 
97                     this.driver = MutationTestDriver!(ImplMutationDriver*)(() @trusted{
98                         return &impl;
99                     }());
100                 }
101 
102                 alias driver this;
103             }
104 
105             return new Rval(data, test_base_timeout);
106         }
107 
108         // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope
109         auto db_ref = () @trusted{ return nullableRef(&db); }();
110 
111         auto driver_data = DriverData(db_ref, fio, data.mut_kinds,
112                 data.compile_program, data.test_suite_execute_program,
113                 data.test_case_analyze_program, data.tc_analyze_builtin,
114                 data.test_suite_execute_timeout);
115 
116         auto test_driver_impl = ImplTestDriver!mutationFactory(driver_data);
117         auto test_driver_impl_ref = () @trusted{
118             return nullableRef(&test_driver_impl);
119         }();
120         auto test_driver = TestDriver!(typeof(test_driver_impl_ref))(test_driver_impl_ref);
121 
122         while (test_driver.isRunning) {
123             test_driver.execute;
124         }
125 
126         return test_driver.status;
127     }
128 }
129 
130 immutable stdoutLog = "stdout.log";
131 immutable stderrLog = "stderr.log";
132 
133 struct DriverData {
134     NullableRef!Database db;
135     FilesysIO filesysIO;
136     Mutation.Kind[] mutKind;
137     AbsolutePath compilerProgram;
138     AbsolutePath testProgram;
139     AbsolutePath testCaseAnalyzeProgram;
140     TestCaseAnalyzeBuiltin[] testCaseAnalyzeBuiltin;
141     Nullable!Duration testProgramTimeout;
142 }
143 
144 /** Run the test suite to verify a mutation.
145  *
146  * Params:
147  *  p = ?
148  *  timeout = timeout threshold.
149  */
150 Mutation.Status runTester(WatchdogT)(AbsolutePath compile_p, AbsolutePath tester_p,
151         AbsolutePath test_output_dir, WatchdogT watchdog, FilesysIO fio) nothrow {
152     import core.thread : Thread;
153     import std.algorithm : among;
154     import std.datetime.stopwatch : StopWatch;
155     import dextool.plugin.mutate.backend.linux_process : spawnSession, tryWait,
156         kill, wait;
157     import std.stdio : File;
158     import core.sys.posix.signal : SIGKILL;
159 
160     Mutation.Status rval;
161 
162     try {
163         auto p = spawnSession([cast(string) compile_p]);
164         auto res = p.wait;
165         if (res.terminated && res.status != 0)
166             return Mutation.Status.killedByCompiler;
167         else if (!res.terminated) {
168             logger.warning("unknown error when executing the compiler").collectException;
169             return Mutation.Status.unknown;
170         }
171     }
172     catch (Exception e) {
173         logger.warning(e.msg).collectException;
174     }
175 
176     string stdout_p;
177     string stderr_p;
178 
179     if (test_output_dir.length != 0) {
180         import std.path : buildPath;
181 
182         stdout_p = buildPath(test_output_dir, stdoutLog);
183         stderr_p = buildPath(test_output_dir, stderrLog);
184     }
185 
186     try {
187         auto p = spawnSession([cast(string) tester_p], stdout_p, stderr_p);
188         // trusted: killing the process started in this scope
189         void cleanup() @safe nothrow {
190             import core.sys.posix.signal : SIGKILL;
191 
192             if (rval.among(Mutation.Status.timeout, Mutation.Status.unknown)) {
193                 kill(p, SIGKILL);
194                 wait(p);
195             }
196         }
197 
198         scope (exit)
199             cleanup;
200 
201         rval = Mutation.Status.timeout;
202         watchdog.start;
203         while (watchdog.isOk) {
204             auto res = tryWait(p);
205             if (res.terminated) {
206                 if (res.status == 0)
207                     rval = Mutation.Status.alive;
208                 else
209                     rval = Mutation.Status.killed;
210                 break;
211             }
212 
213             import core.time : dur;
214 
215             // trusted: a hard coded value is used, no user input.
216             () @trusted{ Thread.sleep(10.dur!"msecs"); }();
217         }
218     }
219     catch (Exception e) {
220         // unable to for example execute the test suite
221         logger.warning(e.msg).collectException;
222         return Mutation.Status.unknown;
223     }
224 
225     return rval;
226 }
227 
228 struct MeasureTestDurationResult {
229     ExitStatusType status;
230     Duration runtime;
231 }
232 
233 /**
234  * If the tests fail (exit code isn't 0) any time then they are too unreliable
235  * to use for mutation testing.
236  *
237  * The runtime is the lowest of the three executions.
238  *
239  * Params:
240  *  p = ?
241  */
242 auto measureTesterDuration(AbsolutePath p) nothrow {
243     if (p.length == 0) {
244         collectException(logger.error("No test suite runner specified (--mutant-tester)"));
245         return MeasureTestDurationResult(ExitStatusType.Errors);
246     }
247 
248     auto any_failure = ExitStatusType.Ok;
249 
250     void fun() {
251         import std.process : execute;
252 
253         auto res = execute([cast(string) p]);
254         if (res.status != 0)
255             any_failure = ExitStatusType.Errors;
256     }
257 
258     import std.datetime.stopwatch : benchmark;
259     import std.algorithm : minElement, map;
260     import core.time : dur;
261 
262     try {
263         auto bench = benchmark!fun(3);
264 
265         if (any_failure != ExitStatusType.Ok)
266             return MeasureTestDurationResult(ExitStatusType.Errors);
267 
268         auto a = (cast(long)((bench[0].total!"msecs") / 3.0)).dur!"msecs";
269         return MeasureTestDurationResult(ExitStatusType.Ok, a);
270     }
271     catch (Exception e) {
272         collectException(logger.error(e.msg));
273         return MeasureTestDurationResult(ExitStatusType.Errors);
274     }
275 }
276 
277 enum MutationDriverSignal {
278     /// stay in the current state
279     stop,
280     /// advance to the next state
281     next,
282     /// All mutants are tested. Stopping mutation testing
283     allMutantsTested,
284     /// An error occured when interacting with the filesystem (fatal). Stopping all mutation testing
285     filesysError,
286     /// An error for a single mutation. It is skipped.
287     mutationError,
288 }
289 
290 /** Drive the control flow when testing a mutant.
291  *
292  * The architecture assume that there will be behavior changes therefore a
293  * strict FSM that separate the context, action and next_state.
294  *
295  * The intention is to separate the control flow from the implementation of the
296  * actions that are done when mutation testing.
297  */
298 struct MutationTestDriver(ImplT) {
299     import std.experimental.typecons : Final;
300 
301     /// The internal state of the FSM.
302     private enum State {
303         none,
304         initialize,
305         mutateCode,
306         testMutant,
307         restoreCode,
308         testCaseAnalyze,
309         storeResult,
310         done,
311         allMutantsTested,
312         filesysError,
313         /// happens when an error occurs during mutations testing but that do not prohibit testing of other mutants
314         noResultRestoreCode,
315         noResult,
316     }
317 
318     private {
319         State st;
320         ImplT impl;
321     }
322 
323     this(ImplT impl) {
324         this.impl = impl;
325     }
326 
327     /// Returns: true as long as the driver is processing a mutant.
328     bool isRunning() {
329         import std.algorithm : among;
330 
331         return st.among(State.done, State.noResult, State.filesysError, State.allMutantsTested) == 0;
332     }
333 
334     bool stopBecauseError() {
335         return st == State.filesysError;
336     }
337 
338     /// Returns: true when the mutation testing should be stopped
339     bool stopMutationTesting() {
340         return st == State.allMutantsTested;
341     }
342 
343     void execute() {
344         const auto signal = impl.signal;
345 
346         debug auto old_st = st;
347 
348         st = nextState(st, signal);
349 
350         debug logger.trace(old_st, "->", st, ":", signal).collectException;
351 
352         final switch (st) {
353         case State.none:
354             break;
355         case State.initialize:
356             impl.initialize;
357             break;
358         case State.mutateCode:
359             impl.mutateCode;
360             break;
361         case State.testMutant:
362             impl.testMutant;
363             break;
364         case State.testCaseAnalyze:
365             impl.testCaseAnalyze;
366             break;
367         case State.restoreCode:
368             impl.cleanup;
369             break;
370         case State.storeResult:
371             impl.storeResult;
372             break;
373         case State.done:
374             break;
375         case State.allMutantsTested:
376             break;
377         case State.filesysError:
378             logger.warning("Filesystem error").collectException;
379             break;
380         case State.noResultRestoreCode:
381             impl.cleanup;
382             break;
383         case State.noResult:
384             break;
385         }
386     }
387 
388     private static State nextState(immutable State current, immutable MutationDriverSignal signal) @safe pure nothrow @nogc {
389         State next_ = current;
390 
391         final switch (current) {
392         case State.none:
393             next_ = State.initialize;
394             break;
395         case State.initialize:
396             if (signal == MutationDriverSignal.next)
397                 next_ = State.mutateCode;
398             break;
399         case State.mutateCode:
400             if (signal == MutationDriverSignal.next)
401                 next_ = State.testMutant;
402             else if (signal == MutationDriverSignal.allMutantsTested)
403                 next_ = State.allMutantsTested;
404             else if (signal == MutationDriverSignal.filesysError)
405                 next_ = State.filesysError;
406             else if (signal == MutationDriverSignal.mutationError)
407                 next_ = State.noResultRestoreCode;
408             break;
409         case State.testMutant:
410             if (signal == MutationDriverSignal.next)
411                 next_ = State.testCaseAnalyze;
412             else if (signal == MutationDriverSignal.mutationError)
413                 next_ = State.noResultRestoreCode;
414             else if (signal == MutationDriverSignal.allMutantsTested)
415                 next_ = State.allMutantsTested;
416             break;
417         case State.testCaseAnalyze:
418             if (signal == MutationDriverSignal.next)
419                 next_ = State.restoreCode;
420             else if (signal == MutationDriverSignal.mutationError)
421                 next_ = State.noResultRestoreCode;
422             break;
423         case State.restoreCode:
424             if (signal == MutationDriverSignal.next)
425                 next_ = State.storeResult;
426             else if (signal == MutationDriverSignal.filesysError)
427                 next_ = State.filesysError;
428             break;
429         case State.storeResult:
430             if (signal == MutationDriverSignal.next)
431                 next_ = State.done;
432             break;
433         case State.done:
434             break;
435         case State.allMutantsTested:
436             break;
437         case State.filesysError:
438             break;
439         case State.noResultRestoreCode:
440             next_ = State.noResult;
441             break;
442         case State.noResult:
443             break;
444         }
445 
446         return next_;
447     }
448 }
449 
450 /** Implementation of the actions during the test of a mutant.
451  *
452  * The intention is that this driver do NOT control the flow.
453  */
454 struct ImplMutationDriver {
455     import std.datetime.stopwatch : StopWatch;
456     import dextool.plugin.mutate.backend.type : TestCase;
457 
458 nothrow:
459 
460     FilesysIO fio;
461     NullableRef!Database db;
462 
463     StopWatch sw;
464     MutationDriverSignal driver_sig;
465 
466     Nullable!MutationEntry mutp;
467     AbsolutePath mut_file;
468     const(ubyte)[] original_content;
469 
470     const(Mutation.Kind)[] mut_kind;
471     const TestCaseAnalyzeBuiltin[] tc_analyze_builtin;
472 
473     AbsolutePath compile_cmd;
474     AbsolutePath test_cmd;
475     AbsolutePath test_case_cmd;
476     Duration tester_runtime;
477 
478     /// Temporary directory where stdout/stderr should be written.
479     AbsolutePath test_tmp_output;
480 
481     Mutation.Status mut_status;
482 
483     TestCase[] test_cases;
484 
485     this(FilesysIO fio, NullableRef!Database db, Mutation.Kind[] mut_kind, AbsolutePath compile_cmd, AbsolutePath test_cmd,
486             AbsolutePath test_case_cmd,
487             TestCaseAnalyzeBuiltin[] tc_analyze_builtin, Duration tester_runtime) {
488         this.fio = fio;
489         this.db = db;
490         this.mut_kind = mut_kind;
491         this.compile_cmd = compile_cmd;
492         this.test_cmd = test_cmd;
493         this.test_case_cmd = test_case_cmd;
494         this.tc_analyze_builtin = tc_analyze_builtin;
495         this.tester_runtime = tester_runtime;
496     }
497 
498     void initialize() {
499         sw.start;
500         driver_sig = MutationDriverSignal.next;
501     }
502 
503     void mutateCode() {
504         import core.thread : Thread;
505         import std.random : uniform;
506         import dextool.plugin.mutate.backend.generate_mutant : generateMutant,
507             GenerateMutantResult, GenerateMutantStatus;
508 
509         driver_sig = MutationDriverSignal.stop;
510 
511         auto next_m = db.nextMutation(mut_kind);
512         if (next_m.st == NextMutationEntry.Status.done) {
513             logger.info("Done! All mutants are tested").collectException;
514             driver_sig = MutationDriverSignal.allMutantsTested;
515             return;
516         } else if (next_m.st == NextMutationEntry.Status.queryError) {
517             // the database is locked. It will automatically sleep and continue.
518             return;
519         } else {
520             mutp = next_m.entry;
521         }
522 
523         try {
524             mut_file = AbsolutePath(FileName(mutp.file), DirName(fio.getOutputDir));
525 
526             // must duplicate because the buffer is memory mapped thus it can change
527             original_content = fio.makeInput(mut_file).read.dup;
528         }
529         catch (Exception e) {
530             logger.error(e.msg).collectException;
531             driver_sig = MutationDriverSignal.filesysError;
532             return;
533         }
534 
535         if (original_content.length == 0) {
536             logger.warning("Unable to read ", mut_file).collectException;
537             driver_sig = MutationDriverSignal.filesysError;
538             return;
539         }
540 
541         // mutate
542         try {
543             auto fout = fio.makeOutput(mut_file);
544             auto mut_res = generateMutant(db.get, mutp, original_content, fout);
545 
546             final switch (mut_res.status) with (GenerateMutantStatus) {
547             case error:
548                 driver_sig = MutationDriverSignal.mutationError;
549                 break;
550             case filesysError:
551                 driver_sig = MutationDriverSignal.filesysError;
552                 break;
553             case databaseError:
554                 // such as when the database is locked
555                 driver_sig = MutationDriverSignal.mutationError;
556                 break;
557             case checksumError:
558                 driver_sig = MutationDriverSignal.filesysError;
559                 break;
560             case noMutation:
561                 driver_sig = MutationDriverSignal.mutationError;
562                 break;
563             case ok:
564                 driver_sig = MutationDriverSignal.next;
565                 logger.infof("%s from '%s' to '%s' in %s:%s:%s", mutp.id, mut_res.from,
566                         mut_res.to, mut_file, mutp.sloc.line, mutp.sloc.column);
567                 break;
568             }
569         }
570         catch (Exception e) {
571             logger.warning(e.msg).collectException;
572             driver_sig = MutationDriverSignal.mutationError;
573         }
574     }
575 
576     void testMutant() {
577         import std.random : uniform;
578         import std.format : format;
579         import std.file : mkdir, exists;
580 
581         assert(!mutp.isNull);
582         driver_sig = MutationDriverSignal.mutationError;
583 
584         if (test_case_cmd.length != 0 || tc_analyze_builtin.length != 0) {
585             // try 5 times or bailout
586             foreach (const _; 0 .. 5) {
587                 try {
588                     auto tmp = format("dextool_tmp_%s", uniform!ulong);
589                     mkdir(tmp);
590                     test_tmp_output = AbsolutePath(FileName(tmp));
591                     break;
592                 }
593                 catch (Exception e) {
594                     logger.warning(e.msg).collectException;
595                 }
596             }
597 
598             if (test_tmp_output.length == 0) {
599                 logger.warning("Unable to create a temporary directory to store stdout/stderr in")
600                     .collectException;
601                 return;
602             }
603         }
604 
605         try {
606             import dextool.plugin.mutate.backend.watchdog : StaticTime;
607 
608             auto watchdog = StaticTime!StopWatch(tester_runtime);
609 
610             mut_status = runTester(compile_cmd, test_cmd, test_tmp_output, watchdog, fio);
611             driver_sig = MutationDriverSignal.next;
612         }
613         catch (Exception e) {
614             logger.warning(e.msg).collectException;
615         }
616     }
617 
618     void testCaseAnalyze() {
619         import std.algorithm : splitter, map, filter;
620         import std.array : array;
621         import std.ascii : newline;
622         import std.file : exists;
623         import std.path : buildPath;
624         import std.process : execute;
625         import std..string : strip;
626 
627         if (mut_status != Mutation.Status.killed || test_tmp_output.length == 0) {
628             driver_sig = MutationDriverSignal.next;
629             return;
630         }
631 
632         bool externalProgram(T)(string stdout_, string stderr_, ref T app) {
633             import std.algorithm : copy;
634 
635             auto p = execute([test_case_cmd, stdout_, stderr_]);
636             if (p.status == 0) {
637                 p.output.splitter(newline).map!(a => a.strip)
638                     .filter!(a => a.length != 0).map!(a => TestCase(a)).copy(app);
639                 return true;
640             } else {
641                 logger.warning(p.output);
642                 logger.warning("Failed to analyze the test case output");
643                 return false;
644             }
645         }
646 
647         // trusted: because the paths to the File object are created by this
648         // program and can thus not lead to memory related problems.
649         bool builtin(T)(string stdout_, string stderr_, ref T app) @trusted {
650             import std.stdio : File;
651             import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze;
652             import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze;
653 
654             auto reldir = fio.getOutputDir;
655 
656             foreach (f; [stdout_, stderr_]) {
657                 auto gtest = GtestParser(reldir);
658                 CtestParser ctest;
659 
660                 foreach (l; File(f).byLine) {
661                     foreach (const p; tc_analyze_builtin) {
662                         final switch (p) {
663                         case TestCaseAnalyzeBuiltin.gtest:
664                             gtest.process(l, app);
665                             break;
666                         case TestCaseAnalyzeBuiltin.ctest:
667                             ctest.process(l, app);
668                             break;
669                         }
670                     }
671                 }
672             }
673 
674             return true;
675         }
676 
677         driver_sig = MutationDriverSignal.mutationError;
678 
679         try {
680             auto stdout_ = buildPath(test_tmp_output, stdoutLog);
681             auto stderr_ = buildPath(test_tmp_output, stderrLog);
682 
683             if (!exists(stdout_) || !exists(stderr_)) {
684                 logger.warningf("Unable to open %s and %s for test case analyze", stdout_, stderr_);
685                 return;
686             }
687 
688             import std.array : appender;
689 
690             auto app = appender!(TestCase[])();
691 
692             bool success = true;
693             if (test_case_cmd.length != 0)
694                 success = success && externalProgram(stdout_, stderr_, app);
695             if (tc_analyze_builtin.length != 0)
696                 success = success && builtin(stdout_, stderr_, app);
697 
698             if (success) {
699                 test_cases = app.data;
700                 driver_sig = MutationDriverSignal.next;
701             }
702         }
703         catch (Exception e) {
704             logger.warning(e.msg).collectException;
705         }
706     }
707 
708     void storeResult() {
709         import dextool.plugin.mutate.backend.mutation_type : broadcast;
710 
711         driver_sig = MutationDriverSignal.stop;
712 
713         sw.stop;
714 
715         try {
716             auto bcast = broadcast(mutp.mp.mutations[0].kind);
717 
718             db.updateMutationBroadcast(mutp.id, mut_status, sw.peek, test_cases, bcast);
719             logger.infof("%s %s (%s)", mutp.id, mut_status, sw.peek);
720             logger.infof(test_cases.length != 0, "%s killed by [%(%s,%)]", mutp.id, test_cases);
721             driver_sig = MutationDriverSignal.next;
722         }
723         catch (Exception e) {
724             logger.warning(e.msg).collectException;
725         }
726     }
727 
728     void cleanup() {
729         driver_sig = MutationDriverSignal.next;
730 
731         // restore the original file.
732         try {
733             fio.makeOutput(mut_file).write(original_content);
734         }
735         catch (Exception e) {
736             logger.error(e.msg).collectException;
737             // fatal error because being unable to restore a file prohibit
738             // future mutations.
739             driver_sig = MutationDriverSignal.filesysError;
740         }
741 
742         if (test_tmp_output.length != 0) {
743             import std.file : rmdirRecurse;
744 
745             // trusted: test_tmp_output is tested to be valid data.
746             // it is further created via mkdtemp which I assume can be
747             // considered safe because its input is created wholly in this
748             // driver.
749             () @trusted{
750                 try {
751                     rmdirRecurse(test_tmp_output);
752                 }
753                 catch (Exception e) {
754                     logger.info(e.msg).collectException;
755                 }
756             }();
757         }
758     }
759 
760     /// Signal from the ImplMutationDriver to the Driver.
761     auto signal() {
762         return driver_sig;
763     }
764 }
765 
766 enum TestDriverSignal {
767     stop,
768     next,
769     allMutantsTested,
770     unreliableTestSuite,
771     compilationError,
772     mutationError,
773     timeoutUnchanged,
774     sanityCheckFailed,
775 }
776 
777 struct TestDriver(ImplT) {
778     private enum State {
779         none,
780         initialize,
781         sanityCheck,
782         checkMutantsLeft,
783         preCompileSut,
784         measureTestSuite,
785         preMutationTest,
786         mutationTest,
787         checkTimeout,
788         incrWatchdog,
789         resetTimeout,
790         done,
791         error,
792     }
793 
794     private {
795         State st;
796         ImplT impl;
797     }
798 
799     this(ImplT impl) {
800         this.impl = impl;
801     }
802 
803     bool isRunning() {
804         import std.algorithm : among;
805 
806         return st.among(State.done, State.error) == 0;
807     }
808 
809     ExitStatusType status() {
810         if (st == State.done)
811             return ExitStatusType.Ok;
812         else
813             return ExitStatusType.Errors;
814     }
815 
816     void execute() {
817         const auto signal = impl.signal;
818 
819         debug auto old_st = st;
820 
821         st = nextState(st, signal);
822 
823         debug logger.trace(old_st, "->", st, ":", signal).collectException;
824 
825         final switch (st) with (State) {
826         case none:
827             break;
828         case initialize:
829             impl.initialize;
830             break;
831         case sanityCheck:
832             impl.sanityCheck;
833             break;
834         case checkMutantsLeft:
835             impl.checkMutantsLeft;
836             break;
837         case preCompileSut:
838             impl.compileProgram;
839             break;
840         case measureTestSuite:
841             impl.measureTestSuite;
842             break;
843         case preMutationTest:
844             impl.preMutationTest;
845             break;
846         case mutationTest:
847             impl.testMutant;
848             break;
849         case checkTimeout:
850             impl.checkTimeout;
851             break;
852         case incrWatchdog:
853             impl.incrWatchdog;
854             break;
855         case resetTimeout:
856             impl.resetTimeout;
857             break;
858         case done:
859             break;
860         case error:
861             break;
862         }
863     }
864 
865     private static State nextState(const State current, const TestDriverSignal signal) {
866         State next_ = current;
867 
868         final switch (current) with (State) {
869         case none:
870             next_ = State.initialize;
871             break;
872         case initialize:
873             if (signal == TestDriverSignal.next)
874                 next_ = State.sanityCheck;
875             break;
876         case sanityCheck:
877             if (signal == TestDriverSignal.next)
878                 next_ = State.checkMutantsLeft;
879             else if (signal == TestDriverSignal.sanityCheckFailed)
880                 next_ = State.error;
881             break;
882         case checkMutantsLeft:
883             if (signal == TestDriverSignal.next)
884                 next_ = State.preCompileSut;
885             else if (signal == TestDriverSignal.allMutantsTested)
886                 next_ = State.done;
887             break;
888         case preCompileSut:
889             if (signal == TestDriverSignal.next)
890                 next_ = State.measureTestSuite;
891             else if (signal == TestDriverSignal.compilationError)
892                 next_ = State.error;
893             break;
894         case measureTestSuite:
895             if (signal == TestDriverSignal.next)
896                 next_ = State.preMutationTest;
897             else if (signal == TestDriverSignal.unreliableTestSuite)
898                 next_ = State.error;
899             break;
900         case preMutationTest:
901             next_ = State.mutationTest;
902             break;
903         case mutationTest:
904             if (signal == TestDriverSignal.next)
905                 next_ = State.preMutationTest;
906             else if (signal == TestDriverSignal.allMutantsTested)
907                 next_ = State.checkTimeout;
908             else if (signal == TestDriverSignal.mutationError)
909                 next_ = State.error;
910             break;
911         case checkTimeout:
912             if (signal == TestDriverSignal.timeoutUnchanged)
913                 next_ = State.done;
914             else if (signal == TestDriverSignal.next)
915                 next_ = State.incrWatchdog;
916             break;
917         case incrWatchdog:
918             next_ = State.resetTimeout;
919             break;
920         case resetTimeout:
921             if (signal == TestDriverSignal.next)
922                 next_ = State.preMutationTest;
923             break;
924         case done:
925             break;
926         case error:
927             break;
928         }
929 
930         return next_;
931     }
932 }
933 
934 struct ImplTestDriver(alias mutationDriverFactory) {
935     import dextool.plugin.mutate.backend.watchdog : ProgressivWatchdog;
936     import std.traits : ReturnType;
937 
938 nothrow:
939     DriverData data;
940 
941     ProgressivWatchdog prog_wd;
942     TestDriverSignal driver_sig;
943     ReturnType!mutationDriverFactory mut_driver;
944     long last_timeout_mutant_count = long.max;
945 
946     this(DriverData data) {
947         this.data = data;
948     }
949 
950     void initialize() {
951         driver_sig = TestDriverSignal.next;
952     }
953 
954     void sanityCheck() {
955         // #SPC-plugin_mutate_sanity_check_db_vs_filesys
956         import dextool.type : Path;
957         import dextool.plugin.mutate.backend.utility : checksum,
958             trustedRelativePath;
959         import dextool.plugin.mutate.backend.type : Checksum;
960 
961         const(Path)[] files;
962         try {
963             files = data.db.getFiles;
964         }
965         catch (Exception e) {
966             // assume the database is locked thus need to retry
967             driver_sig = TestDriverSignal.stop;
968             logger.trace(e.msg).collectException;
969             return;
970         }
971 
972         bool has_sanity_check_failed;
973         for (size_t i; i < files.length;) {
974             Checksum db_checksum;
975             try {
976                 db_checksum = data.db.getFileChecksum(files[i]);
977             }
978             catch (Exception e) {
979                 // the database is locked
980                 logger.trace(e.msg).collectException;
981                 // retry
982                 continue;
983             }
984 
985             try {
986                 auto abs_f = AbsolutePath(FileName(files[i]),
987                         DirName(cast(string) data.filesysIO.getOutputDir));
988                 auto f_checksum = checksum(data.filesysIO.makeInput(abs_f).read[]);
989                 if (db_checksum != f_checksum) {
990                     logger.errorf("Mismatch between the file on the filesystem and the analyze of '%s'",
991                             abs_f);
992                     has_sanity_check_failed = true;
993                 }
994             }
995             catch (Exception e) {
996                 // assume it is a problem reading the file or something like that.
997                 has_sanity_check_failed = true;
998                 logger.trace(e.msg).collectException;
999             }
1000 
1001             // all done. continue with the next file
1002             ++i;
1003         }
1004 
1005         if (has_sanity_check_failed) {
1006             driver_sig = TestDriverSignal.sanityCheckFailed;
1007             logger.error("Detected that one or more file has changed since last analyze where done")
1008                 .collectException;
1009             logger.error("Either restore the files to the previous state or rerun the analyzer")
1010                 .collectException;
1011         } else {
1012             logger.info("Sanity check passed. Files on the filesystem are consistent")
1013                 .collectException;
1014             driver_sig = TestDriverSignal.next;
1015         }
1016     }
1017 
1018     void checkMutantsLeft() {
1019         driver_sig = TestDriverSignal.next;
1020 
1021         const auto mutant = data.db.nextMutation(data.mutKind);
1022 
1023         if (mutant.st == NextMutationEntry.Status.queryError) {
1024             // the database is locked
1025             driver_sig = TestDriverSignal.stop;
1026         } else if (mutant.st == NextMutationEntry.Status.done) {
1027             logger.info("Done! All mutants are tested").collectException;
1028             driver_sig = TestDriverSignal.allMutantsTested;
1029         }
1030     }
1031 
1032     void compileProgram() {
1033         driver_sig = TestDriverSignal.compilationError;
1034 
1035         logger.info("Preparing for mutation testing by checking that the program and tests compile without any errors (no mutants injected)")
1036             .collectException;
1037 
1038         try {
1039             import std.process : execute;
1040 
1041             const comp_res = execute([cast(string) data.compilerProgram]);
1042 
1043             if (comp_res.status == 0) {
1044                 driver_sig = TestDriverSignal.next;
1045             } else {
1046                 logger.info(comp_res.output);
1047                 logger.error("Compiler command failed: ", comp_res.status);
1048             }
1049         }
1050         catch (Exception e) {
1051             // unable to for example execute the compiler
1052             logger.error(e.msg).collectException;
1053         }
1054     }
1055 
1056     void measureTestSuite() {
1057         driver_sig = TestDriverSignal.unreliableTestSuite;
1058 
1059         if (data.testProgramTimeout.isNull) {
1060             logger.info("Measuring the time to run the tests: ", data.testProgram).collectException;
1061             auto tester = measureTesterDuration(data.testProgram);
1062             if (tester.status == ExitStatusType.Ok) {
1063                 logger.info("Tester measured to: ", tester.runtime).collectException;
1064                 prog_wd = ProgressivWatchdog(tester.runtime);
1065                 driver_sig = TestDriverSignal.next;
1066             } else {
1067                 logger.error(
1068                         "Test suite is unreliable. It must return exit status '0' when running with unmodified mutants")
1069                     .collectException;
1070             }
1071         } else {
1072             prog_wd = ProgressivWatchdog(data.testProgramTimeout.get);
1073             driver_sig = TestDriverSignal.next;
1074         }
1075     }
1076 
1077     void preMutationTest() {
1078         driver_sig = TestDriverSignal.next;
1079         mut_driver = mutationDriverFactory(data, prog_wd.timeout);
1080     }
1081 
1082     void testMutant() {
1083         if (mut_driver.isRunning) {
1084             mut_driver.execute();
1085             driver_sig = TestDriverSignal.stop;
1086         } else if (mut_driver.stopBecauseError) {
1087             driver_sig = TestDriverSignal.mutationError;
1088         } else if (mut_driver.stopMutationTesting) {
1089             driver_sig = TestDriverSignal.allMutantsTested;
1090         } else {
1091             driver_sig = TestDriverSignal.next;
1092         }
1093     }
1094 
1095     void checkTimeout() {
1096         driver_sig = TestDriverSignal.stop;
1097 
1098         auto entry = data.db.timeoutMutants(data.mutKind);
1099         if (entry.isNull) {
1100             // the database is locked
1101             return;
1102         }
1103 
1104         try {
1105             if (!data.testProgramTimeout.isNull) {
1106                 // the user have supplied a timeout thus ignore this algorithm
1107                 // for increasing the timeout
1108                 driver_sig = TestDriverSignal.timeoutUnchanged;
1109             } else if (entry.count == 0) {
1110                 driver_sig = TestDriverSignal.timeoutUnchanged;
1111             } else if (entry.count == last_timeout_mutant_count) {
1112                 // no change between current pool of timeout mutants and the previous
1113                 driver_sig = TestDriverSignal.timeoutUnchanged;
1114             } else if (entry.count < last_timeout_mutant_count) {
1115                 driver_sig = TestDriverSignal.next;
1116                 logger.info("Mutants with the status timeout: ", entry.count);
1117             }
1118 
1119             last_timeout_mutant_count = entry.count;
1120         }
1121         catch (Exception e) {
1122             logger.warning(e.msg).collectException;
1123         }
1124     }
1125 
1126     void incrWatchdog() {
1127         driver_sig = TestDriverSignal.next;
1128         prog_wd.incrTimeout;
1129         logger.info("Increasing timeout to: ", prog_wd.timeout).collectException;
1130     }
1131 
1132     void resetTimeout() {
1133         // database is locked
1134         driver_sig = TestDriverSignal.stop;
1135 
1136         try {
1137             data.db.resetMutant(data.mutKind, Mutation.Status.timeout, Mutation.Status.unknown);
1138             driver_sig = TestDriverSignal.next;
1139         }
1140         catch (Exception e) {
1141             logger.warning(e.msg).collectException;
1142         }
1143     }
1144 
1145     auto signal() {
1146         return driver_sig;
1147     }
1148 }