1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool.plugin.mutate.backend.test_mutant;
11 
12 import core.thread : Thread;
13 import core.time : Duration, dur;
14 import logger = std.experimental.logger;
15 import std.algorithm : sort, map, splitter, filter;
16 import std.array : empty, array, appender;
17 import std.datetime : SysTime, Clock;
18 import std.exception : collectException;
19 import std.path : buildPath;
20 import std.typecons : Nullable, Tuple, Yes;
21 
22 import blob_model : Blob;
23 import process : DrainElement;
24 import sumtype;
25 
26 import dextool.fsm : Fsm, next, act, get, TypeDataMap;
27 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
28     NextMutationEntry, spinSql, MutantTimeoutCtx, MutationId;
29 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
30 import dextool.plugin.mutate.backend.test_mutant.common;
31 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport;
32 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner;
33 import dextool.plugin.mutate.backend.type : Mutation, TestCase;
34 import dextool.plugin.mutate.config;
35 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand;
36 import dextool.set;
37 import dextool.type : AbsolutePath, ExitStatusType, Path;
38 
39 @safe:
40 
41 auto makeTestMutant() {
42     return BuildTestMutant();
43 }
44 
45 private:
46 
47 struct BuildTestMutant {
48 @safe:
49 nothrow:
50 
51     import dextool.plugin.mutate.type : MutationKind;
52 
53     private struct InternalData {
54         Mutation.Kind[] mut_kinds;
55         FilesysIO filesys_io;
56         ConfigMutationTest config;
57     }
58 
59     private InternalData data;
60 
61     auto config(ConfigMutationTest c) {
62         data.config = c;
63         return this;
64     }
65 
66     auto mutations(MutationKind[] v) {
67         import dextool.plugin.mutate.backend.utility : toInternal;
68 
69         data.mut_kinds = toInternal(v);
70         return this;
71     }
72 
73     ExitStatusType run(ref Database db, FilesysIO fio) nothrow {
74         // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope
75         auto db_ref = () @trusted { return &db; }();
76 
77         auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config);
78 
79         try {
80             auto test_driver = TestDriver(driver_data);
81 
82             while (test_driver.isRunning) {
83                 test_driver.execute;
84             }
85 
86             return test_driver.status;
87         } catch (Exception e) {
88             logger.error(e.msg).collectException;
89         }
90 
91         return ExitStatusType.Errors;
92     }
93 }
94 
95 struct DriverData {
96     Database* db;
97     FilesysIO filesysIO;
98     Mutation.Kind[] mutKind;
99     AutoCleanup autoCleanup;
100     ConfigMutationTest conf;
101 }
102 
103 struct MeasureTestDurationResult {
104     bool ok;
105     Duration runtime;
106 }
107 
108 /** Measure the time it takes to run the test command.
109  *
110  * The runtime is the lowest of three executions. Anything else is assumed to
111  * be variations in the system.
112  *
113  * If the tests fail (exit code isn't 0) any time then they are too unreliable
114  * to use for mutation testing.
115  *
116  * Params:
117  *  cmd = test command to measure
118  */
119 MeasureTestDurationResult measureTestCommand(ref TestRunner runner) @safe nothrow {
120     import std.algorithm : min;
121     import std.datetime.stopwatch : StopWatch, AutoStart;
122     import process;
123 
124     if (runner.empty) {
125         collectException(logger.error("No test command(s) specified (--test-cmd)"));
126         return MeasureTestDurationResult(false);
127     }
128 
129     static struct Rval {
130         TestResult result;
131         Duration runtime;
132     }
133 
134     auto runTest() @safe {
135         auto sw = StopWatch(AutoStart.yes);
136         auto res = runner.run;
137         return Rval(res, sw.peek);
138     }
139 
140     static void print(DrainElement[] data) @trusted {
141         import std.stdio : stdout, write;
142 
143         foreach (l; data) {
144             write(l.byUTF8);
145         }
146         stdout.flush;
147     }
148 
149     auto runtime = Duration.max;
150     bool failed;
151     for (int i; i < 3 && !failed; ++i) {
152         try {
153             auto res = runTest;
154             final switch (res.result.status) with (TestResult) {
155             case Status.passed:
156                 runtime = min(runtime, res.runtime);
157                 break;
158             case Status.failed:
159                 goto case;
160             case Status.timeout:
161                 goto case;
162             case Status.error:
163                 failed = true;
164                 print(res.result.output);
165                 break;
166             }
167         } catch (Exception e) {
168             logger.error(e.msg).collectException;
169             failed = true;
170         }
171     }
172 
173     return MeasureTestDurationResult(!failed, runtime);
174 }
175 
176 struct TestDriver {
177     import std.datetime : SysTime;
178     import std.typecons : Unique;
179     import dextool.plugin.mutate.backend.database : Schemata, SchemataId, MutationStatusId;
180     import dextool.plugin.mutate.backend.test_mutant.source_mutant : MutationTestDriver,
181         MutationTestResult;
182     import dextool.plugin.mutate.backend.test_mutant.timeout : calculateTimeout, TimeoutFsm;
183 
184     /// Runs the test commands.
185     TestRunner runner;
186 
187     ///
188     TestCaseAnalyzer testCaseAnalyzer;
189 
190     static struct Global {
191         DriverData data;
192         Unique!MutationTestDriver mut_driver;
193 
194         TimeoutFsm timeoutFsm;
195 
196         /// The time it takes to execute the test suite when no mutant is injected.
197         Duration testSuiteRuntime;
198 
199         /// the next mutant to test, if there are any.
200         MutationEntry nextMutant;
201 
202         // when the user manually configure the timeout it means that the
203         // timeout algorithm should not be used.
204         bool hardcodedTimeout;
205 
206         /// Max time to run the mutation testing for.
207         SysTime maxRuntime;
208 
209         /// Test commands to execute.
210         ShellCommand[] testCmds;
211     }
212 
213     static struct UpdateTimeoutData {
214         long lastTimeoutIter;
215     }
216 
217     static struct None {
218     }
219 
220     static struct Initialize {
221     }
222 
223     static struct PullRequest {
224     }
225 
226     static struct PullRequestData {
227         import dextool.plugin.mutate.type : TestConstraint;
228 
229         TestConstraint constraint;
230         long seed;
231     }
232 
233     static struct SanityCheck {
234         bool sanityCheckFailed;
235     }
236 
237     static struct AnalyzeTestCmdForTestCase {
238         TestCase[] foundTestCases;
239     }
240 
241     static struct UpdateAndResetAliveMutants {
242         TestCase[] foundTestCases;
243     }
244 
245     static struct ResetOldMutant {
246         bool doneTestingOldMutants;
247     }
248 
249     static struct ResetOldMutantData {
250         /// Number of mutants that where reset.
251         long resetCount;
252         long maxReset;
253     }
254 
255     static struct CleanupTempDirs {
256     }
257 
258     static struct CheckMutantsLeft {
259         bool allMutantsTested;
260     }
261 
262     static struct ParseStdin {
263     }
264 
265     static struct PreCompileSut {
266         bool compilationError;
267     }
268 
269     static struct FindTestCmds {
270     }
271 
272     static struct ChooseMode {
273     }
274 
275     static struct MeasureTestSuite {
276         bool unreliableTestSuite;
277     }
278 
279     static struct PreMutationTest {
280     }
281 
282     static struct MutationTest {
283         bool next;
284         bool mutationError;
285         MutationTestResult result;
286     }
287 
288     static struct CheckTimeout {
289         bool timeoutUnchanged;
290     }
291 
292     static struct NextSchemata {
293         bool hasSchema;
294         Schemata schemata;
295     }
296 
297     static struct PreSchemata {
298         Schemata schemata;
299 
300         bool error;
301     }
302 
303     static struct SchemataTest {
304         import dextool.plugin.mutate.backend.test_mutant.schemata : MutationTestResult;
305 
306         SchemataId id;
307         MutationTestResult[] result;
308     }
309 
310     static struct SchemataTestResult {
311         import dextool.plugin.mutate.backend.test_mutant.schemata : MutationTestResult;
312 
313         SchemataId id;
314         MutationTestResult[] result;
315     }
316 
317     static struct SchemataRestore {
318         bool error;
319     }
320 
321     static struct SchemataRestoreData {
322         static struct Original {
323             AbsolutePath path;
324             Blob original;
325         }
326 
327         Original[] original;
328     }
329 
330     static struct Done {
331     }
332 
333     static struct Error {
334     }
335 
336     static struct UpdateTimeout {
337     }
338 
339     static struct NextPullRequestMutant {
340         bool noUnknownMutantsLeft;
341     }
342 
343     static struct NextPullRequestMutantData {
344         import dextool.plugin.mutate.backend.database : MutationStatusId;
345 
346         MutationStatusId[] mutants;
347 
348         /// If set then stop after this many alive are found.
349         Nullable!int maxAlive;
350         /// number of alive mutants that has been found.
351         int alive;
352     }
353 
354     static struct NextMutant {
355         bool noUnknownMutantsLeft;
356     }
357 
358     static struct HandleTestResult {
359         MutationTestResult result;
360     }
361 
362     static struct CheckRuntime {
363         bool reachedMax;
364     }
365 
366     static struct SetMaxRuntime {
367     }
368 
369     alias Fsm = dextool.fsm.Fsm!(None, Initialize, SanityCheck,
370             AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, ResetOldMutant,
371             CleanupTempDirs, CheckMutantsLeft, PreCompileSut, MeasureTestSuite,
372             PreMutationTest, NextMutant, MutationTest, HandleTestResult,
373             CheckTimeout, Done, Error, UpdateTimeout, CheckRuntime,
374             SetMaxRuntime, PullRequest, NextPullRequestMutant, ParseStdin, FindTestCmds, ChooseMode,
375             NextSchemata, PreSchemata, SchemataTest, SchemataTestResult, SchemataRestore);
376     alias LocalStateDataT = Tuple!(UpdateTimeoutData, NextPullRequestMutantData,
377             PullRequestData, ResetOldMutantData, SchemataRestoreData);
378 
379     private {
380         Fsm fsm;
381         Global global;
382         TypeDataMap!(LocalStateDataT, UpdateTimeout, NextPullRequestMutant,
383                 PullRequest, ResetOldMutant, SchemataRestore) local;
384         bool isRunning_ = true;
385     }
386 
387     this(DriverData data) {
388         this.global = Global(data);
389         this.global.timeoutFsm = TimeoutFsm(data.mutKind);
390         this.global.hardcodedTimeout = !global.data.conf.mutationTesterRuntime.isNull;
391         local.get!PullRequest.constraint = global.data.conf.constraint;
392         local.get!PullRequest.seed = global.data.conf.pullRequestSeed;
393         local.get!NextPullRequestMutant.maxAlive = global.data.conf.maxAlive;
394         local.get!ResetOldMutant.maxReset = global.data.conf.oldMutantsNr;
395         this.global.testCmds = global.data.conf.mutationTester;
396 
397         this.runner = TestRunner.make(global.data.conf.testPoolSize);
398         // using an unreasonable timeout to make it possible to analyze for
399         // test cases and measure the test suite.
400         this.runner.timeout = 999.dur!"hours";
401         this.runner.put(data.conf.mutationTester);
402 
403         // TODO: allow a user, as is for test_cmd, to specify an array of
404         // external analyzers.
405         this.testCaseAnalyzer = TestCaseAnalyzer(global.data.conf.mutationTestCaseBuiltin,
406                 global.data.conf.mutationTestCaseAnalyze, global.data.autoCleanup);
407     }
408 
409     static void execute_(ref TestDriver self) @trusted {
410         // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a
411         // graphical view of the state machine.
412 
413         self.fsm.next!((None a) => fsm(Initialize.init),
414                 (Initialize a) => fsm(SanityCheck.init), (SanityCheck a) {
415             if (a.sanityCheckFailed)
416                 return fsm(Error.init);
417             if (self.global.data.conf.unifiedDiffFromStdin)
418                 return fsm(ParseStdin.init);
419             return fsm(PreCompileSut.init);
420         }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) => fsm(
421                 UpdateAndResetAliveMutants(a.foundTestCases)),
422                 (UpdateAndResetAliveMutants a) => fsm(CheckMutantsLeft.init), (ResetOldMutant a) {
423             if (a.doneTestingOldMutants)
424                 return fsm(Done.init);
425             return fsm(UpdateTimeout.init);
426         }, (CleanupTempDirs a) {
427             if (self.local.get!PullRequest.constraint.empty)
428                 return fsm(NextSchemata.init);
429             return fsm(NextPullRequestMutant.init);
430         }, (CheckMutantsLeft a) {
431             if (a.allMutantsTested
432                 && self.global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing)
433                 return fsm(Done.init);
434             return fsm(MeasureTestSuite.init);
435         }, (PreCompileSut a) {
436             if (a.compilationError)
437                 return fsm(Error.init);
438             if (self.global.data.conf.testCommandDir.empty)
439                 return fsm(ChooseMode.init);
440             return fsm(FindTestCmds.init);
441         }, (FindTestCmds a) { return fsm(ChooseMode.init); }, (ChooseMode a) {
442             if (!self.local.get!PullRequest.constraint.empty)
443                 return fsm(PullRequest.init);
444             if (!self.global.data.conf.mutationTestCaseAnalyze.empty
445                 || !self.global.data.conf.mutationTestCaseBuiltin.empty)
446                 return fsm(AnalyzeTestCmdForTestCase.init);
447             return fsm(CheckMutantsLeft.init);
448         }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) {
449             if (a.unreliableTestSuite)
450                 return fsm(Error.init);
451             return fsm(SetMaxRuntime.init);
452         }, (SetMaxRuntime a) => fsm(UpdateTimeout.init), (NextPullRequestMutant a) {
453             if (a.noUnknownMutantsLeft)
454                 return fsm(Done.init);
455             return fsm(PreMutationTest.init);
456         }, (NextSchemata a) {
457             if (a.hasSchema)
458                 return fsm(PreSchemata(a.schemata));
459             return fsm(NextMutant.init);
460         }, (PreSchemata a) {
461             if (a.error)
462                 return fsm(Error.init);
463             return fsm(SchemataTest(a.schemata.id));
464         }, (SchemataTest a) { return fsm(SchemataTestResult(a.id, a.result)); },
465                 (SchemataTestResult a) => fsm(SchemataRestore.init), (SchemataRestore a) {
466             if (a.error)
467                 return fsm(Error.init);
468             return fsm(CheckRuntime.init);
469         }, (NextMutant a) {
470             if (a.noUnknownMutantsLeft)
471                 return fsm(CheckTimeout.init);
472             return fsm(PreMutationTest.init);
473         }, (PreMutationTest a) => fsm(MutationTest.init),
474                 (UpdateTimeout a) => fsm(CleanupTempDirs.init), (MutationTest a) {
475             if (a.next)
476                 return fsm(HandleTestResult(a.result));
477             else if (a.mutationError)
478                 return fsm(Error.init);
479             return fsm(a);
480         }, (HandleTestResult a) => fsm(CheckRuntime.init), (CheckRuntime a) {
481             if (a.reachedMax)
482                 return fsm(Done.init);
483             return fsm(UpdateTimeout.init);
484         }, (CheckTimeout a) {
485             if (a.timeoutUnchanged)
486                 return fsm(ResetOldMutant.init);
487             return fsm(UpdateTimeout.init);
488         }, (Done a) => fsm(a), (Error a) => fsm(a),);
489 
490         self.fsm.act!(self);
491     }
492 
493 nothrow:
494     void execute() {
495         try {
496             execute_(this);
497         } catch (Exception e) {
498             logger.warning(e.msg).collectException;
499         }
500     }
501 
502     bool isRunning() {
503         return isRunning_;
504     }
505 
506     ExitStatusType status() {
507         if (fsm.isState!Done)
508             return ExitStatusType.Ok;
509         return ExitStatusType.Errors;
510     }
511 
512     void opCall(None data) {
513     }
514 
515     void opCall(Initialize data) {
516     }
517 
518     void opCall(Done data) {
519         global.data.autoCleanup.cleanup;
520         logger.info("Done!").collectException;
521         isRunning_ = false;
522     }
523 
524     void opCall(Error data) {
525         global.data.autoCleanup.cleanup;
526         isRunning_ = false;
527     }
528 
529     void opCall(ref SanityCheck data) {
530         // #SPC-sanity_check_db_vs_filesys
531         import colorlog : color, Color;
532         import dextool.plugin.mutate.backend.utility : checksum, Checksum;
533 
534         logger.info("Checking that the file(s) on the filesystem match the database")
535             .collectException;
536 
537         auto failed = appender!(string[])();
538         foreach (file; spinSql!(() { return global.data.db.getFiles; })) {
539             auto db_checksum = spinSql!(() {
540                 return global.data.db.getFileChecksum(file);
541             });
542 
543             try {
544                 auto abs_f = AbsolutePath(file, global.data.filesysIO.getOutputDir);
545                 auto f_checksum = checksum(global.data.filesysIO.makeInput(abs_f).content[]);
546                 if (db_checksum != f_checksum) {
547                     failed.put(abs_f);
548                 }
549             } catch (Exception e) {
550                 // assume it is a problem reading the file or something like that.
551                 failed.put(file);
552                 logger.warningf("%s: %s", file, e.msg).collectException;
553             }
554         }
555 
556         data.sanityCheckFailed = failed.data.length != 0;
557 
558         if (data.sanityCheckFailed) {
559             logger.error("Detected that file(s) has changed since last analyze where done")
560                 .collectException;
561             logger.error("Either restore the file(s) or rerun the analyze").collectException;
562             foreach (f; failed.data) {
563                 logger.info(f).collectException;
564             }
565         } else {
566             logger.info("Ok".color(Color.green)).collectException;
567         }
568     }
569 
570     void opCall(ParseStdin data) {
571         import dextool.plugin.mutate.backend.diff_parser : diffFromStdin;
572         import dextool.plugin.mutate.type : Line;
573 
574         try {
575             auto constraint = local.get!PullRequest.constraint;
576             foreach (pkv; diffFromStdin.toRange(global.data.filesysIO.getOutputDir)) {
577                 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array;
578             }
579             local.get!PullRequest.constraint = constraint;
580         } catch (Exception e) {
581             logger.warning(e.msg).collectException;
582         }
583     }
584 
585     void opCall(ref AnalyzeTestCmdForTestCase data) {
586         import std.datetime.stopwatch : StopWatch;
587         import dextool.plugin.mutate.backend.type : TestCase;
588 
589         TestCase[] found;
590         try {
591             auto res = runTester(runner);
592             auto analyze = testCaseAnalyzer.analyze(res.output, Yes.allFound);
593 
594             analyze.match!((TestCaseAnalyzer.Success a) { found = a.found; },
595                     (TestCaseAnalyzer.Unstable a) {
596                 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable);
597                 found = a.found;
598             }, (TestCaseAnalyzer.Failed a) {
599                 logger.warning("The parser that analyze the output for test case(s) failed");
600             });
601         } catch (Exception e) {
602             logger.warning(e.msg).collectException;
603         }
604 
605         warnIfConflictingTestCaseIdentifiers(found);
606         data.foundTestCases = found;
607     }
608 
609     void opCall(UpdateAndResetAliveMutants data) {
610         import std.traits : EnumMembers;
611 
612         // the test cases before anything has potentially changed.
613         auto old_tcs = spinSql!(() {
614             Set!string old_tcs;
615             foreach (tc; global.data.db.getDetectedTestCases) {
616                 old_tcs.add(tc.name);
617             }
618             return old_tcs;
619         });
620 
621         void transaction() @safe {
622             final switch (global.data.conf.onRemovedTestCases) with (
623                 ConfigMutationTest.RemovedTestCases) {
624             case doNothing:
625                 global.data.db.addDetectedTestCases(data.foundTestCases);
626                 break;
627             case remove:
628                 foreach (id; global.data.db.setDetectedTestCases(data.foundTestCases)) {
629                     global.data.db.updateMutationStatus(id, Mutation.Status.unknown);
630                 }
631                 break;
632             }
633         }
634 
635         auto found_tcs = spinSql!(() @trusted {
636             auto tr = global.data.db.transaction;
637             transaction();
638 
639             Set!string found_tcs;
640             foreach (tc; global.data.db.getDetectedTestCases) {
641                 found_tcs.add(tc.name);
642             }
643 
644             tr.commit;
645             return found_tcs;
646         });
647 
648         printDroppedTestCases(old_tcs, found_tcs);
649 
650         if (hasNewTestCases(old_tcs, found_tcs)
651                 && global.data.conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) {
652             logger.info("Resetting alive mutants").collectException;
653             // there is no use in trying to limit the mutants to reset to those
654             // that are part of "this" execution because new test cases can
655             // only mean one thing: re-test all alive mutants.
656             spinSql!(() {
657                 global.data.db.resetMutant([EnumMembers!(Mutation.Kind)],
658                     Mutation.Status.alive, Mutation.Status.unknown);
659             });
660         }
661     }
662 
663     void opCall(ref ResetOldMutant data) {
664         import dextool.plugin.mutate.backend.database.type;
665 
666         if (global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) {
667             data.doneTestingOldMutants = true;
668             return;
669         }
670         if (Clock.currTime > global.maxRuntime) {
671             data.doneTestingOldMutants = true;
672             return;
673         }
674         if (local.get!ResetOldMutant.resetCount >= local.get!ResetOldMutant.maxReset) {
675             data.doneTestingOldMutants = true;
676             return;
677         }
678 
679         local.get!ResetOldMutant.resetCount++;
680 
681         logger.infof("Resetting an old mutant (%s/%s)", local.get!ResetOldMutant.resetCount,
682                 local.get!ResetOldMutant.maxReset).collectException;
683         auto oldest = spinSql!(() {
684             return global.data.db.getOldestMutants(global.data.mutKind, 1);
685         });
686 
687         foreach (const old; oldest) {
688             logger.info("Last updated ", old.updated).collectException;
689             spinSql!(() {
690                 global.data.db.updateMutationStatus(old.id, Mutation.Status.unknown);
691             });
692         }
693     }
694 
695     void opCall(CleanupTempDirs data) {
696         global.data.autoCleanup.cleanup;
697     }
698 
699     void opCall(ref CheckMutantsLeft data) {
700         spinSql!(() { global.timeoutFsm.execute(*global.data.db); });
701 
702         data.allMutantsTested = global.timeoutFsm.output.done;
703 
704         if (global.timeoutFsm.output.done) {
705             logger.info("All mutants are tested").collectException;
706         }
707     }
708 
709     void opCall(ref PreCompileSut data) {
710         import std.stdio : write;
711         import colorlog : color, Color;
712         import process;
713 
714         logger.info("Checking the build command").collectException;
715         try {
716             auto output = appender!(DrainElement[])();
717             auto p = pipeProcess(global.data.conf.mutationCompile.value).sandbox.drain(output,
718                     999.dur!"hours").scopeKill;
719             if (p.wait == 0) {
720                 logger.info("Ok".color(Color.green));
721                 return;
722             }
723 
724             logger.error("Build commman failed");
725             foreach (l; output.data) {
726                 write(l.byUTF8);
727             }
728         } catch (Exception e) {
729             // unable to for example execute the compiler
730             logger.error(e.msg).collectException;
731         }
732 
733         data.compilationError = true;
734     }
735 
736     void opCall(FindTestCmds data) {
737         auto cmds = appender!(ShellCommand[])();
738         foreach (root; global.data.conf.testCommandDir) {
739             try {
740                 cmds.put(findExecutables(root.AbsolutePath)
741                         .map!(a => ShellCommand([a] ~ global.data.conf.testCommandDirFlag)));
742             } catch (Exception e) {
743                 logger.warning(e.msg).collectException;
744             }
745         }
746 
747         if (!cmds.data.empty) {
748             this.runner.put(cmds.data);
749             this.global.testCmds ~= cmds.data;
750             logger.infof("Found test commands in %s:",
751                     global.data.conf.testCommandDir).collectException;
752             foreach (c; cmds.data) {
753                 logger.info(c).collectException;
754             }
755         }
756     }
757 
758     void opCall(ChooseMode data) {
759     }
760 
761     void opCall(PullRequest data) {
762         import std.array : appender;
763         import std.random : randomCover, Mt19937_64;
764         import dextool.plugin.mutate.backend.database : MutationStatusId;
765         import dextool.plugin.mutate.backend.type : SourceLoc;
766         import dextool.set;
767 
768         Set!MutationStatusId mut_ids;
769 
770         foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) {
771             const file_id = spinSql!(() => global.data.db.getFileId(kv.key));
772             if (file_id.isNull) {
773                 logger.infof("The file %s do not exist in the database. Skipping...",
774                         kv.key).collectException;
775                 continue;
776             }
777 
778             foreach (l; kv.value) {
779                 auto mutants = spinSql!(() {
780                     return global.data.db.getMutationsOnLine(global.data.mutKind,
781                         file_id.get, SourceLoc(l.value, 0));
782                 });
783 
784                 const pre_cnt = mut_ids.length;
785                 foreach (v; mutants)
786                     mut_ids.add(v);
787 
788                 logger.infof(mut_ids.length - pre_cnt > 0, "Found %s mutant(s) to test (%s:%s)",
789                         mut_ids.length - pre_cnt, kv.key, l.value).collectException;
790             }
791         }
792 
793         logger.infof(!mut_ids.empty, "Found %s mutants in the diff",
794                 mut_ids.length).collectException;
795 
796         const seed = local.get!PullRequest.seed;
797         logger.infof("Using random seed %s when choosing the mutants to test",
798                 seed).collectException;
799         auto rng = Mt19937_64(seed);
800         local.get!NextPullRequestMutant.mutants = mut_ids.toArray.sort.randomCover(rng).array;
801         logger.trace("Test sequence ", local.get!NextPullRequestMutant.mutants).collectException;
802 
803         if (mut_ids.empty) {
804             logger.warning("None of the locations specified with -L exists").collectException;
805             logger.info("Available files are:").collectException;
806             foreach (f; spinSql!(() => global.data.db.getFiles))
807                 logger.info(f).collectException;
808         }
809     }
810 
811     void opCall(ref MeasureTestSuite data) {
812         if (!global.data.conf.mutationTesterRuntime.isNull) {
813             global.testSuiteRuntime = global.data.conf.mutationTesterRuntime.get;
814             return;
815         }
816 
817         logger.info("Measuring the runtime of the test command: ",
818                 global.testCmds).collectException;
819 
820         const tester = () {
821             try {
822                 // need to measure the test suite single threaded to get the "worst"
823                 // test case execution time because if multiple instances are running
824                 // on the same computer the available CPU resources are variable. This
825                 // reduces the number of mutants marked as timeout. Further
826                 // improvements in the future could be to check the loadavg and let it
827                 // affect the number of threads.
828                 runner.poolSize = 1;
829                 scope (exit)
830                     runner.poolSize = global.data.conf.testPoolSize;
831                 return measureTestCommand(runner);
832             } catch (Exception e) {
833                 logger.error(e.msg).collectException;
834                 return MeasureTestDurationResult(false);
835             }
836         }();
837 
838         if (tester.ok) {
839             // The sampling of the test suite become too unreliable when the timeout is <1s.
840             // This is a quick and dirty fix.
841             // A proper fix requires an update of the sampler in runTester.
842             auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime;
843             logger.info("Test command runtime: ", t).collectException;
844             global.testSuiteRuntime = t;
845         } else {
846             data.unreliableTestSuite = true;
847             logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected")
848                 .collectException;
849         }
850     }
851 
852     void opCall(PreMutationTest) {
853         auto factory(DriverData d, MutationEntry mutp, TestRunner* runner) @safe nothrow {
854             import std.typecons : Unique;
855             import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase;
856 
857             try {
858                 auto global = MutationTestDriver.Global(d.filesysIO, d.db, mutp, runner);
859                 return Unique!MutationTestDriver(new MutationTestDriver(global,
860                         MutationTestDriver.TestMutantData(!(d.conf.mutationTestCaseAnalyze.empty
861                         && d.conf.mutationTestCaseBuiltin.empty), d.conf.mutationCompile,),
862                         MutationTestDriver.TestCaseAnalyzeData(&testCaseAnalyzer)));
863             } catch (Exception e) {
864                 logger.error(e.msg).collectException;
865             }
866             assert(0, "should not happen");
867         }
868 
869         runner.timeout = calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime);
870         global.mut_driver = factory(global.data, global.nextMutant, () @trusted {
871             return &runner;
872         }());
873     }
874 
875     void opCall(ref MutationTest data) {
876         if (global.mut_driver.isRunning) {
877             global.mut_driver.execute();
878         } else if (global.mut_driver.stopBecauseError) {
879             data.mutationError = true;
880         } else {
881             data.result = global.mut_driver.result;
882             data.next = true;
883         }
884     }
885 
886     void opCall(ref CheckTimeout data) {
887         data.timeoutUnchanged = global.hardcodedTimeout || global.timeoutFsm.output.done;
888     }
889 
890     void opCall(UpdateTimeout) {
891         spinSql!(() { global.timeoutFsm.execute(*global.data.db); });
892 
893         const lastIter = local.get!UpdateTimeout.lastTimeoutIter;
894 
895         if (lastIter != global.timeoutFsm.output.iter) {
896             logger.infof("Changed the timeout from %s to %s (iteration %s)",
897                     calculateTimeout(lastIter, global.testSuiteRuntime),
898                     calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime),
899                     global.timeoutFsm.output.iter).collectException;
900             local.get!UpdateTimeout.lastTimeoutIter = global.timeoutFsm.output.iter;
901         }
902     }
903 
904     void opCall(ref NextPullRequestMutant data) {
905         global.nextMutant = MutationEntry.init;
906         data.noUnknownMutantsLeft = true;
907 
908         while (!local.get!NextPullRequestMutant.mutants.empty) {
909             const id = local.get!NextPullRequestMutant.mutants[$ - 1];
910             const status = spinSql!(() => global.data.db.getMutationStatus(id));
911 
912             if (status.isNull)
913                 continue;
914 
915             if (status.get == Mutation.Status.alive) {
916                 local.get!NextPullRequestMutant.alive++;
917             }
918 
919             if (status.get != Mutation.Status.unknown) {
920                 local.get!NextPullRequestMutant.mutants
921                     = local.get!NextPullRequestMutant.mutants[0 .. $ - 1];
922                 continue;
923             }
924 
925             const info = spinSql!(() => global.data.db.getMutantsInfo(global.data.mutKind, [
926                         id
927                     ]));
928             if (info.empty)
929                 continue;
930 
931             global.nextMutant = spinSql!(() => global.data.db.getMutation(info[0].id));
932             data.noUnknownMutantsLeft = false;
933             break;
934         }
935 
936         if (!local.get!NextPullRequestMutant.maxAlive.isNull) {
937             const alive = local.get!NextPullRequestMutant.alive;
938             const maxAlive = local.get!NextPullRequestMutant.maxAlive.get;
939             logger.infof(alive > 0, "Found %s/%s alive mutants", alive, maxAlive).collectException;
940             if (alive >= maxAlive) {
941                 data.noUnknownMutantsLeft = true;
942             }
943         }
944     }
945 
946     void opCall(ref NextMutant data) {
947         global.nextMutant = MutationEntry.init;
948 
949         auto next = spinSql!(() {
950             return global.data.db.nextMutation(global.data.mutKind);
951         });
952 
953         data.noUnknownMutantsLeft = next.st == NextMutationEntry.Status.done;
954 
955         if (!next.entry.isNull) {
956             global.nextMutant = next.entry.get;
957         }
958     }
959 
960     void opCall(HandleTestResult data) {
961         void statusUpdate(MutationTestResult.StatusUpdate result) {
962             import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus;
963 
964             const cnt_action = () {
965                 if (result.status == Mutation.Status.alive)
966                     return Database.CntAction.incr;
967                 return Database.CntAction.reset;
968             }();
969 
970             auto statusId = spinSql!(() {
971                 return global.data.db.getMutationStatusId(result.id);
972             });
973             if (statusId.isNull)
974                 return;
975 
976             spinSql!(() @trusted {
977                 auto t = global.data.db.transaction;
978                 updateMutantStatus(*global.data.db, statusId.get,
979                     result.status, global.timeoutFsm.output.iter);
980                 global.data.db.updateMutation(statusId.get, cnt_action);
981                 global.data.db.updateMutation(statusId.get, result.testTime);
982                 global.data.db.updateMutationTestCases(statusId.get, result.testCases);
983                 t.commit;
984             });
985 
986             logger.infof("%s %s (%s)", result.id, result.status, result.testTime).collectException;
987             logger.infof(!result.testCases.empty, `%s killed by [%-(%s, %)]`,
988                     result.id, result.testCases.sort.map!"a.name").collectException;
989         }
990 
991         data.result.value.match!((MutationTestResult.NoResult a) {},
992                 (MutationTestResult.StatusUpdate a) => statusUpdate(a));
993     }
994 
995     void opCall(SetMaxRuntime) {
996         global.maxRuntime = Clock.currTime + global.data.conf.maxRuntime;
997     }
998 
999     void opCall(ref CheckRuntime data) {
1000         data.reachedMax = Clock.currTime > global.maxRuntime;
1001         if (data.reachedMax) {
1002             logger.infof("Max runtime of %s reached at %s",
1003                     global.data.conf.maxRuntime, global.maxRuntime).collectException;
1004         }
1005     }
1006 
1007     void opCall(ref NextSchemata data) {
1008         auto schema = spinSql!(() { return global.data.db.nextSchemata; });
1009 
1010         data.hasSchema = !schema.isNull;
1011         if (!schema.isNull) {
1012             data.schemata = schema.get;
1013             logger.info("Running schemata ", data.schemata.id).collectException;
1014         }
1015     }
1016 
1017     void opCall(ref PreSchemata data) {
1018         import std.format : format;
1019         import dextool.plugin.mutate.backend.database.type : SchemataFragment;
1020 
1021         Blob makeSchemata(Blob original, SchemataFragment[] fragments) {
1022             import blob_model;
1023 
1024             Edit[] edits;
1025             foreach (a; fragments) {
1026                 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text);
1027             }
1028             auto m = merge(original, edits);
1029             return change(new Blob(original.uri, original.content), m.edits);
1030         }
1031 
1032         SchemataFragment[] fragments(Path p) {
1033             return data.schemata.fragments.filter!(a => a.file == p).array;
1034         }
1035 
1036         SchemataRestoreData.Original[] orgs;
1037         try {
1038             auto files = data.schemata.fragments.map!(a => a.file).toSet;
1039             foreach (f; files.toRange) {
1040                 const absf = AbsolutePath(f, global.data.filesysIO.getOutputDir);
1041 
1042                 orgs ~= SchemataRestoreData.Original(absf, global.data.filesysIO.makeInput(absf));
1043 
1044                 // writing the schemata.
1045                 auto s = makeSchemata(orgs[$ - 1].original, fragments(f));
1046                 global.data.filesysIO.makeOutput(absf).write(s);
1047 
1048                 if (global.data.conf.logSchemata) {
1049                     global.data.filesysIO.makeOutput(AbsolutePath(format!"%s.schema%s"(absf,
1050                             data.schemata.id).Path)).write(s);
1051                 }
1052             }
1053         } catch (Exception e) {
1054             logger.warning(e.msg).collectException;
1055             data.error = true;
1056         }
1057         local.get!SchemataRestore.original = orgs;
1058     }
1059 
1060     void opCall(ref SchemataTest data) {
1061         import dextool.plugin.mutate.backend.test_mutant.schemata;
1062 
1063         bool successCompile;
1064         compile(global.data.conf.mutationCompile).match!((Mutation.Status a) {}, (bool success) {
1065             successCompile = success;
1066         },);
1067 
1068         if (!successCompile) {
1069             logger.infof("Schemata %s failed to compile", data.id).collectException;
1070             return;
1071         }
1072 
1073         auto mutants = spinSql!(() {
1074             return global.data.db.getSchemataMutants(data.id);
1075         });
1076 
1077         try {
1078             auto driver = SchemataTestDriver(global.data.filesysIO, &runner,
1079                     global.data.db, &testCaseAnalyzer, mutants);
1080             while (driver.isRunning) {
1081                 driver.execute;
1082             }
1083         } catch (Exception e) {
1084             logger.info(e.msg).collectException;
1085             logger.warning("Failed executing schemata ", data.id).collectException;
1086         }
1087     }
1088 
1089     void opCall(SchemataTestResult data) {
1090         foreach (m; data.result) {
1091             spinSql!(() {
1092                 global.data.db.updateMutation(m.id, m.status, m.testTime);
1093             });
1094             spinSql!(() {
1095                 global.data.db.updateMutationTestCases(m.id, m.testCases);
1096             });
1097         }
1098 
1099         spinSql!(() { global.data.db.finishSchemata(data.id); });
1100     }
1101 
1102     void opCall(ref SchemataRestore data) {
1103         foreach (o; local.get!SchemataRestore.original) {
1104             try {
1105                 global.data.filesysIO.makeOutput(o.path).write(o.original.content);
1106             } catch (Exception e) {
1107                 logger.error(e.msg).collectException;
1108                 data.error = true;
1109             }
1110         }
1111     }
1112 }
1113 
1114 private:
1115 
1116 /** Compare the old test cases with those that have been found this run.
1117  *
1118  * TODO: the side effect that this function print to the console is NOT good.
1119  */
1120 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow {
1121     bool rval;
1122 
1123     auto new_tcs = found_tcs.setDifference(old_tcs);
1124     foreach (tc; new_tcs.toRange) {
1125         logger.info(!rval, "Found new test case(s):").collectException;
1126         logger.infof("%s", tc).collectException;
1127         rval = true;
1128     }
1129 
1130     return rval;
1131 }
1132 
1133 /** Compare old and new test cases to print those that have been removed.
1134  */
1135 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow {
1136     auto diff = old_tcs.setDifference(changed_tcs);
1137     auto removed = diff.toArray;
1138 
1139     logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException;
1140     foreach (tc; removed) {
1141         logger.infof("%s", tc).collectException;
1142     }
1143 }
1144 
1145 /// Returns: true if all tests cases have unique identifiers
1146 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow {
1147     Set!TestCase checked;
1148     bool conflict;
1149 
1150     foreach (tc; found_tcs) {
1151         if (checked.contains(tc)) {
1152             logger.info(!conflict,
1153                     "Found test cases that do not have global, unique identifiers")
1154                 .collectException;
1155             logger.info(!conflict,
1156                     "This make the report of test cases that has killed zero mutants unreliable")
1157                 .collectException;
1158             logger.info("%s", tc).collectException;
1159             conflict = true;
1160         }
1161     }
1162 }