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