1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module dextool.plugin.mutate.backend.test_mutant;
11 
12 import core.time : Duration, dur;
13 import logger = std.experimental.logger;
14 import std.algorithm : map, filter, joiner, among, max;
15 import std.array : empty, array, appender;
16 import std.datetime : SysTime, Clock;
17 import std.datetime.stopwatch : StopWatch, AutoStart;
18 import std.exception : collectException;
19 import std.format : format;
20 import std.random : randomCover;
21 import std.traits : EnumMembers;
22 import std.typecons : Nullable, Tuple, Yes, tuple;
23 
24 import blob_model : Blob;
25 import miniorm : spinSql, silentLog;
26 import my.actor;
27 import my.container.vector;
28 import my.fsm : Fsm, next, act, get, TypeDataMap;
29 import my.gc.refc;
30 import my.hash : Checksum64;
31 import my.named_type;
32 import my.optional;
33 import my.set;
34 import proc : DrainElement;
35 import sumtype;
36 static import my.fsm;
37 
38 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
39     NextMutationEntry, TestFile, ChecksumTestCmdOriginal;
40 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
41 import dextool.plugin.mutate.backend.test_mutant.common;
42 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner,
43     findExecutables, TestRunResult = TestResult;
44 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor;
45 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm;
46 import dextool.plugin.mutate.backend.type : Mutation, TestCase, ExitStatus;
47 import dextool.plugin.mutate.config;
48 import dextool.plugin.mutate.type : ShellCommand;
49 import dextool.type : AbsolutePath, ExitStatusType, Path;
50 
51 @safe:
52 
53 auto makeTestMutant() {
54     return BuildTestMutant();
55 }
56 
57 private:
58 
59 struct BuildTestMutant {
60 @safe:
61 
62     import dextool.plugin.mutate.type : MutationKind;
63 
64     private struct InternalData {
65         Mutation.Kind[] kinds;
66         FilesysIO filesys_io;
67         ConfigMutationTest config;
68         ConfigSchema schemaConf;
69         ConfigCoverage covConf;
70     }
71 
72     private InternalData data;
73 
74     auto config(ConfigMutationTest c) @trusted nothrow {
75         data.config = c;
76         return this;
77     }
78 
79     auto config(ConfigSchema c) @trusted nothrow {
80         data.schemaConf = c;
81         return this;
82     }
83 
84     auto config(ConfigCoverage c) @trusted nothrow {
85         data.covConf = c;
86         return this;
87     }
88 
89     auto mutations(MutationKind[] v) nothrow {
90         import dextool.plugin.mutate.backend.mutation_type : toInternal;
91 
92         logger.infof("mutation operators: %(%s, %)", v).collectException;
93         data.kinds = toInternal(v);
94         return this;
95     }
96 
97     ExitStatusType run(const AbsolutePath dbPath, FilesysIO fio) @trusted {
98         try {
99             auto db = spinSql!(() => Database.make(dbPath))(dbOpenTimeout);
100             return internalRun(dbPath, &db, fio);
101         } catch (Exception e) {
102             logger.error(e.msg).collectException;
103         }
104 
105         return ExitStatusType.Errors;
106     }
107 
108     private ExitStatusType internalRun(AbsolutePath dbPath, Database* db, FilesysIO fio) {
109         auto system = makeSystem;
110 
111         auto cleanup = new AutoCleanup;
112         scope (exit)
113             cleanup.cleanup;
114 
115         auto test_driver = TestDriver(dbPath, db, () @trusted { return &system; }(),
116                 fio, data.kinds, cleanup, data.config, data.covConf, data.schemaConf);
117 
118         while (test_driver.isRunning) {
119             test_driver.execute;
120         }
121 
122         return test_driver.status;
123     }
124 }
125 
126 struct MeasureTestDurationResult {
127     bool ok;
128     Duration[] runtime;
129 }
130 
131 /** Measure the time it takes to run the test command.
132  *
133  * The runtime is the lowest of three executions. Anything else is assumed to
134  * be variations in the system.
135  *
136  * If the tests fail (exit code isn't 0) any time then they are too unreliable
137  * to use for mutation testing.
138  *
139  * Params:
140  *  runner = ?
141  *  samples = number of times to run the test suite
142  */
143 MeasureTestDurationResult measureTestCommand(ref TestRunner runner, int samples) @safe nothrow {
144     import std.algorithm : min;
145     import proc;
146 
147     if (runner.empty) {
148         collectException(logger.error("No test command(s) specified (--test-cmd)"));
149         return MeasureTestDurationResult(false);
150     }
151 
152     static struct Rval {
153         TestRunResult result;
154         Duration runtime;
155     }
156 
157     auto runTest() @safe {
158         auto sw = StopWatch(AutoStart.yes);
159         auto res = runner.run(4.dur!"hours");
160         return Rval(res, sw.peek);
161     }
162 
163     static void print(TestRunResult res) @trusted {
164         import std.stdio : stdout, write;
165 
166         foreach (kv; res.output.byKeyValue) {
167             logger.info("test_cmd: ", kv.key);
168             foreach (l; kv.value)
169                 write(l.byUTF8);
170         }
171 
172         stdout.flush;
173     }
174 
175     static void printFailing(ref TestRunResult res) {
176         print(res);
177         logger.info("failing commands: ", res.output.byKey);
178         logger.info("exit status: ", res.exitStatus.get);
179     }
180 
181     Duration[] runtimes;
182     bool failed;
183     for (int i; i < samples && !failed; ++i) {
184         try {
185             auto res = runTest;
186             final switch (res.result.status) with (TestRunResult) {
187             case Status.passed:
188                 runtimes ~= res.runtime;
189                 break;
190             case Status.failed:
191                 goto case;
192             case Status.timeout:
193                 goto case;
194             case Status.memOverload:
195                 goto case;
196             case Status.error:
197                 failed = true;
198                 printFailing(res.result);
199                 break;
200             }
201             logger.infof("%s: Measured test command runtime %s", i, res.runtime);
202         } catch (Exception e) {
203             logger.error(e.msg).collectException;
204             failed = true;
205         }
206     }
207 
208     return MeasureTestDurationResult(!failed, runtimes);
209 }
210 
211 struct TestDriver {
212     import std.datetime : SysTime;
213     import dextool.plugin.mutate.backend.database : SchemataId, MutationStatusId;
214     import dextool.plugin.mutate.backend.test_mutant.source_mutant : MutationTestDriver;
215     import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig;
216     import dextool.plugin.mutate.type : MutationOrder;
217 
218     Database* db;
219     AbsolutePath dbPath;
220 
221     FilesysIO filesysIO;
222     Mutation.Kind[] kinds;
223     AutoCleanup autoCleanup;
224 
225     ConfigMutationTest conf;
226     ConfigSchema schemaConf;
227     ConfigCoverage covConf;
228 
229     System* system;
230 
231     /// Async communication with the database
232     DbSaveActor.Address dbSave;
233 
234     /// Async stat update from the database every 30s.
235     StatActor.Address stat;
236 
237     /// Runs the test commands.
238     TestRunner runner;
239 
240     ///
241     TestCaseAnalyzer testCaseAnalyzer;
242 
243     /// Stop conditions (most of them)
244     TestStopCheck stopCheck;
245 
246     /// assuming that there are no more than 100 instances running in
247     /// parallel.
248     uint maxParallelInstances;
249 
250     // need to use 10000 because in an untested code base it is not
251     // uncommon for mutants being in the thousands.
252     enum long unknownWeight = 10000;
253     // using a factor 1000 to make a pull request mutant very high prio
254     enum long pullRequestWeight = unknownWeight * 1000;
255 
256     TimeoutFsm timeoutFsm;
257 
258     /// the next mutant to test, if there are any.
259     MutationEntry nextMutant;
260 
261     TimeoutConfig timeout;
262 
263     /// Test commands to execute.
264     ShellCommand[] testCmds;
265 
266     // The order to test mutants. It is either affected by the user directly or if pull request mode is activated.
267     MutationOrder mutationOrder;
268 
269     static struct UpdateTimeoutData {
270         long lastTimeoutIter;
271     }
272 
273     static struct None {
274     }
275 
276     static struct Initialize {
277         bool halt;
278     }
279 
280     static struct PullRequest {
281     }
282 
283     static struct PullRequestData {
284         import dextool.plugin.mutate.type : TestConstraint;
285 
286         TestConstraint constraint;
287     }
288 
289     static struct SanityCheck {
290         bool sanityCheckFailed;
291     }
292 
293     static struct AnalyzeTestCmdForTestCase {
294         bool failed;
295         TestCase[][ShellCommand] foundTestCases;
296     }
297 
298     static struct UpdateAndResetAliveMutants {
299         TestCase[][ShellCommand] foundTestCases;
300     }
301 
302     static struct RetestOldMutant {
303     }
304 
305     static struct ResetOldMutantData {
306         /// Number of mutants that where reset.
307         long maxReset;
308         NamedType!(double, Tag!"OldMutantPercentage", double.init, TagStringable) resetPercentage;
309     }
310 
311     static struct Cleanup {
312     }
313 
314     static struct CheckMutantsLeft {
315         bool allMutantsTested;
316     }
317 
318     static struct SaveMutationScore {
319     }
320 
321     static struct UpdateTestCaseTag {
322     }
323 
324     static struct ParseStdin {
325     }
326 
327     static struct PreCompileSut {
328         bool compilationError;
329     }
330 
331     static struct FindTestCmds {
332     }
333 
334     static struct ChooseMode {
335     }
336 
337     static struct MeasureTestSuite {
338         bool unreliableTestSuite;
339     }
340 
341     static struct MutationTest {
342         NamedType!(bool, Tag!"MutationError", bool.init, TagStringable) mutationError;
343         MutationTestResult[] result;
344     }
345 
346     static struct MutationTestData {
347         TestBinaryDb testBinaryDb;
348     }
349 
350     static struct CheckTimeout {
351         bool timeoutUnchanged;
352     }
353 
354     static struct NextSchemataData {
355         SchemataId[] schematas;
356         long totalSchematas;
357         long invalidSchematas;
358     }
359 
360     static struct NextSchemata {
361         NamedType!(bool, Tag!"HasSchema", bool.init, TagStringable, ImplicitConvertable) hasSchema;
362         SchemataId schemataId;
363 
364         /// stop mutation testing because the last schema has been used and the
365         /// user has configured that the testing should stop now.
366         NamedType!(bool, Tag!"StopTesting", bool.init, TagStringable, ImplicitConvertable) stop;
367     }
368 
369     static struct SchemataTest {
370         SchemataId id;
371         bool fatalError;
372     }
373 
374     static struct Done {
375     }
376 
377     static struct Error {
378     }
379 
380     static struct UpdateTimeout {
381     }
382 
383     static struct CheckPullRequestMutant {
384         NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft;
385     }
386 
387     static struct CheckPullRequestMutantData {
388         long startWorklistCnt;
389         long stopAfter;
390     }
391 
392     static struct NextMutant {
393         NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft;
394     }
395 
396     static struct NextMutantData {
397         import dextool.plugin.mutate.backend.database.type : MutationId;
398 
399         // because of the asynchronous nature it may be so that the result of
400         // the last executed hasn't finished being written to the DB when we
401         // request a new mutant. This is used to block repeating the same
402         // mutant.
403         MutationId lastTested;
404     }
405 
406     static struct HandleTestResult {
407         MutationTestResult[] result;
408     }
409 
410     static struct CheckStopCond {
411         bool halt;
412     }
413 
414     static struct LoadSchematas {
415     }
416 
417     static struct OverloadCheck {
418         bool sleep;
419     }
420 
421     static struct ContinuesCheckTestSuite {
422         bool ok;
423     }
424 
425     static struct ContinuesCheckTestSuiteData {
426         long lastWorklistCnt;
427         SysTime lastCheck;
428     }
429 
430     static struct Stop {
431     }
432 
433     static struct Coverage {
434         bool propagate;
435         bool fatalError;
436     }
437 
438     static struct PropagateCoverage {
439     }
440 
441     static struct ChecksumTestCmds {
442     }
443 
444     static struct SaveTestBinary {
445     }
446 
447     alias Fsm = my.fsm.Fsm!(None, Initialize, SanityCheck,
448             AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, RetestOldMutant,
449             Cleanup, CheckMutantsLeft, PreCompileSut, MeasureTestSuite, NextMutant,
450             MutationTest, HandleTestResult, CheckTimeout, Done, Error,
451             UpdateTimeout, CheckStopCond, PullRequest, CheckPullRequestMutant, ParseStdin,
452             FindTestCmds, ChooseMode, NextSchemata, SchemataTest, LoadSchematas,
453             Stop, SaveMutationScore, UpdateTestCaseTag, OverloadCheck,
454             Coverage, PropagateCoverage, ContinuesCheckTestSuite,
455             ChecksumTestCmds, SaveTestBinary);
456     alias LocalStateDataT = Tuple!(UpdateTimeoutData, CheckPullRequestMutantData, PullRequestData, ResetOldMutantData,
457             NextSchemataData, ContinuesCheckTestSuiteData, MutationTestData, NextMutantData);
458 
459     private {
460         Fsm fsm;
461         TypeDataMap!(LocalStateDataT, UpdateTimeout, CheckPullRequestMutant, PullRequest,
462                 RetestOldMutant, NextSchemata, ContinuesCheckTestSuite, MutationTest, NextMutant) local;
463         bool isRunning_ = true;
464         bool isDone = false;
465     }
466 
467     this(AbsolutePath dbPath, Database* db, System* sys, FilesysIO filesysIO, Mutation.Kind[] kinds,
468             AutoCleanup autoCleanup, ConfigMutationTest conf,
469             ConfigCoverage coverage, ConfigSchema schema) {
470         this.db = db;
471         this.dbPath = dbPath;
472 
473         this.system = sys;
474 
475         this.filesysIO = filesysIO;
476         this.kinds = kinds;
477         this.autoCleanup = autoCleanup;
478         this.conf = conf;
479         this.covConf = coverage;
480         this.schemaConf = schema;
481 
482         this.timeoutFsm = TimeoutFsm(kinds);
483 
484         if (!conf.mutationTesterRuntime.isNull)
485             timeout.userConfigured(conf.mutationTesterRuntime.get);
486 
487         local.get!PullRequest.constraint = conf.constraint;
488         local.get!RetestOldMutant.maxReset = conf.oldMutantsNr;
489         local.get!RetestOldMutant.resetPercentage = conf.oldMutantPercentage;
490         this.testCmds = conf.mutationTester;
491         this.mutationOrder = conf.mutationOrder;
492 
493         this.runner.useEarlyStop(conf.useEarlyTestCmdStop);
494         this.runner = TestRunner.make(conf.testPoolSize);
495         this.runner.useEarlyStop(conf.useEarlyTestCmdStop);
496         this.runner.maxOutputCapture(
497                 TestRunner.MaxCaptureBytes(conf.maxTestCaseOutput.get * 1024 * 1024));
498         this.runner.minAvailableMem(
499                 TestRunner.MinAvailableMemBytes(toMinMemory(conf.maxMemUsage.get)));
500         this.runner.put(conf.mutationTester);
501 
502         // TODO: allow a user, as is for test_cmd, to specify an array of
503         // external analyzers.
504         this.testCaseAnalyzer = TestCaseAnalyzer(conf.mutationTestCaseBuiltin,
505                 conf.mutationTestCaseAnalyze, autoCleanup);
506 
507         this.stopCheck = TestStopCheck(conf);
508 
509         this.maxParallelInstances = () {
510             if (mutationOrder.among(MutationOrder.random, MutationOrder.bySize))
511                 return 100;
512             return 1;
513         }();
514 
515         if (logger.globalLogLevel.among(logger.LogLevel.trace, logger.LogLevel.all))
516             fsm.logger = (string s) { logger.trace(s); };
517     }
518 
519     static void execute_(ref TestDriver self) @trusted {
520         // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a
521         // graphical view of the state machine.
522 
523         self.fsm.next!((None a) => fsm(Initialize.init), (Initialize a) {
524             if (a.halt)
525                 return fsm(CheckStopCond.init);
526             return fsm(SanityCheck.init);
527         }, (SanityCheck a) {
528             if (a.sanityCheckFailed)
529                 return fsm(Error.init);
530             if (self.conf.unifiedDiffFromStdin)
531                 return fsm(ParseStdin.init);
532             return fsm(PreCompileSut.init);
533         }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) {
534             if (a.failed)
535                 return fsm(Error.init);
536             return fsm(UpdateAndResetAliveMutants(a.foundTestCases));
537         }, (UpdateAndResetAliveMutants a) {
538             if (self.conf.onOldMutants == ConfigMutationTest.OldMutant.test)
539                 return fsm(RetestOldMutant.init);
540             return fsm(CheckMutantsLeft.init);
541         }, (RetestOldMutant a) => fsm(CheckMutantsLeft.init), (Cleanup a) {
542             if (self.local.get!PullRequest.constraint.empty)
543                 return fsm(NextSchemata.init);
544             return fsm(CheckPullRequestMutant.init);
545         }, (CheckMutantsLeft a) {
546             if (a.allMutantsTested)
547                 return fsm(Done.init);
548             if (self.conf.testCmdChecksum.get)
549                 return fsm(ChecksumTestCmds.init);
550             return fsm(MeasureTestSuite.init);
551         }, (ChecksumTestCmds a) => MeasureTestSuite.init, (SaveMutationScore a) => UpdateTestCaseTag.init,
552                 (UpdateTestCaseTag a) => SaveTestBinary.init,
553                 (SaveTestBinary a) => Stop.init, (PreCompileSut a) {
554             if (a.compilationError)
555                 return fsm(Error.init);
556             if (self.conf.testCommandDir.empty)
557                 return fsm(ChooseMode.init);
558             return fsm(FindTestCmds.init);
559         }, (FindTestCmds a) { return fsm(ChooseMode.init); }, (ChooseMode a) {
560             if (!self.local.get!PullRequest.constraint.empty)
561                 return fsm(PullRequest.init);
562             if (!self.conf.mutationTestCaseAnalyze.empty
563                 || !self.conf.mutationTestCaseBuiltin.empty)
564                 return fsm(AnalyzeTestCmdForTestCase.init);
565             if (self.conf.onOldMutants == ConfigMutationTest.OldMutant.test)
566                 return fsm(RetestOldMutant.init);
567             return fsm(CheckMutantsLeft.init);
568         }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) {
569             if (a.unreliableTestSuite)
570                 return fsm(Error.init);
571             if (self.covConf.use)
572                 return fsm(Coverage.init);
573             return fsm(LoadSchematas.init);
574         }, (Coverage a) {
575             if (a.fatalError)
576                 return fsm(Error.init);
577             if (a.propagate)
578                 return fsm(PropagateCoverage.init);
579             return fsm(LoadSchematas.init);
580         }, (PropagateCoverage a) => LoadSchematas.init,
581                 (LoadSchematas a) => fsm(UpdateTimeout.init), (CheckPullRequestMutant a) {
582             if (a.noUnknownMutantsLeft)
583                 return fsm(Done.init);
584             return fsm(NextMutant.init);
585         }, (NextSchemata a) {
586             if (a.hasSchema)
587                 return fsm(SchemataTest(a.schemataId));
588             if (a.stop)
589                 return fsm(Done.init);
590             return fsm(NextMutant.init);
591         }, (SchemataTest a) {
592             if (a.fatalError)
593                 return fsm(Error.init);
594             return fsm(CheckStopCond.init);
595         }, (NextMutant a) {
596             if (a.noUnknownMutantsLeft)
597                 return fsm(CheckTimeout.init);
598             return fsm(MutationTest.init);
599         }, (UpdateTimeout a) => fsm(OverloadCheck.init), (OverloadCheck a) {
600             if (a.sleep)
601                 return fsm(CheckStopCond.init);
602             return fsm(ContinuesCheckTestSuite.init);
603         }, (ContinuesCheckTestSuite a) {
604             if (a.ok)
605                 return fsm(Cleanup.init);
606             return fsm(Error.init);
607         }, (MutationTest a) {
608             if (a.mutationError)
609                 return fsm(Error.init);
610             return fsm(HandleTestResult(a.result));
611         }, (HandleTestResult a) => fsm(CheckStopCond.init), (CheckStopCond a) {
612             if (a.halt)
613                 return fsm(Done.init);
614             return fsm(UpdateTimeout.init);
615         }, (CheckTimeout a) {
616             if (a.timeoutUnchanged)
617                 return fsm(Done.init);
618             return fsm(UpdateTimeout.init);
619         }, (Done a) => fsm(SaveMutationScore.init), (Error a) => fsm(Stop.init), (Stop a) => fsm(a));
620 
621         self.fsm.act!(self);
622     }
623 
624 nothrow:
625 
626     void execute() {
627         try {
628             execute_(this);
629         } catch (Exception e) {
630             logger.warning(e.msg).collectException;
631         }
632     }
633 
634     bool isRunning() {
635         return isRunning_;
636     }
637 
638     ExitStatusType status() {
639         if (isDone)
640             return ExitStatusType.Ok;
641         return ExitStatusType.Errors;
642     }
643 
644     void opCall(None data) {
645     }
646 
647     void opCall(ref Initialize data) {
648         logger.info("Initializing worklist").collectException;
649 
650         auto status = [Mutation.Status.unknown];
651         if (!conf.useSkipMutant)
652             status ~= Mutation.Status.skipped;
653 
654         spinSql!(() {
655             db.worklistApi.update(kinds, status, unknownWeight, mutationOrder);
656         });
657 
658         // detect if the system is overloaded before trying to do something
659         // slow such as compiling the SUT.
660         if (conf.loadBehavior == ConfigMutationTest.LoadBehavior.halt && stopCheck.isHalt) {
661             data.halt = true;
662         }
663 
664         logger.infof("Memory limit set minium %s Mbyte",
665                 cast(ulong)(toMinMemory(conf.maxMemUsage.get) / (1024.0 * 1024.0)))
666             .collectException;
667 
668         try {
669             dbSave = system.spawn(&spawnDbSaveActor, dbPath);
670             stat = system.spawn(&spawnStatActor, dbPath);
671         } catch (Exception e) {
672             logger.error(e.msg).collectException;
673             data.halt = true;
674         }
675     }
676 
677     void opCall(Stop data) {
678         isRunning_ = false;
679     }
680 
681     void opCall(Done data) {
682         import dextool.plugin.mutate.backend.test_mutant.common_actors : IsDone;
683 
684         try {
685             auto self = scopedActor;
686             // it should NOT take more than five minutes to save the last
687             // results to the database.
688             self.request(dbSave, delay(5.dur!"minutes")).send(IsDone.init).then((bool a) {
689             });
690         } catch (ScopedActorException e) {
691             logger.trace(e.error).collectException;
692         } catch (Exception e) {
693             logger.warning(e.msg).collectException;
694         }
695 
696         logger.info("Done!").collectException;
697         isDone = true;
698     }
699 
700     void opCall(Error data) {
701         autoCleanup.cleanup;
702     }
703 
704     void opCall(ref SanityCheck data) {
705         import core.sys.posix.sys.stat : S_IWUSR;
706         import std.path : buildPath;
707         import my.file : getAttrs;
708         import colorlog : color;
709         import dextool.plugin.mutate.backend.utility : checksum, Checksum;
710 
711         logger.info("Sanity check of files to mutate").collectException;
712 
713         auto failed = appender!(string[])();
714         auto checksumFailed = appender!(string[])();
715         auto writePermissionFailed = appender!(string[])();
716         foreach (file; spinSql!(() { return db.getFiles; })) {
717             auto db_checksum = spinSql!(() { return db.getFileChecksum(file); });
718 
719             try {
720                 auto abs_f = AbsolutePath(buildPath(filesysIO.getOutputDir, file));
721                 auto f_checksum = checksum(filesysIO.makeInput(abs_f).content[]);
722                 if (db_checksum != f_checksum) {
723                     checksumFailed.put(abs_f);
724                 }
725 
726                 uint attrs;
727                 if (getAttrs(abs_f, attrs)) {
728                     if ((attrs & S_IWUSR) == 0) {
729                         writePermissionFailed.put(abs_f);
730                     }
731                 } else {
732                     writePermissionFailed.put(abs_f);
733                 }
734             } catch (Exception e) {
735                 failed.put(file);
736                 logger.warningf("%s: %s", file, e.msg).collectException;
737             }
738         }
739 
740         data.sanityCheckFailed = !failed.data.empty
741             || !checksumFailed.data.empty || !writePermissionFailed.data.empty;
742 
743         if (data.sanityCheckFailed) {
744             logger.info(!failed.data.empty,
745                     "Unknown error when checking the files").collectException;
746             foreach (f; failed.data)
747                 logger.info(f).collectException;
748 
749             logger.info(!checksumFailed.data.empty,
750                     "Detected that file(s) has changed since last analyze where done")
751                 .collectException;
752             logger.info(!checksumFailed.data.empty,
753                     "Either restore the file(s) or rerun the analyze").collectException;
754             foreach (f; checksumFailed.data)
755                 logger.info(f).collectException;
756 
757             logger.info(!writePermissionFailed.data.empty,
758                     "Files to mutate are not writable").collectException;
759             foreach (f; writePermissionFailed.data)
760                 logger.info(f).collectException;
761 
762             logger.info("Failed".color.fgRed).collectException;
763         } else {
764             logger.info("Ok".color.fgGreen).collectException;
765         }
766     }
767 
768     void opCall(ref OverloadCheck data) {
769         if (conf.loadBehavior == ConfigMutationTest.LoadBehavior.slowdown && stopCheck.isOverloaded) {
770             data.sleep = true;
771             logger.info(stopCheck.overloadToString).collectException;
772             stopCheck.pause;
773         }
774     }
775 
776     void opCall(ref ContinuesCheckTestSuite data) {
777         import colorlog : color;
778 
779         data.ok = true;
780 
781         if (!conf.contCheckTestSuite)
782             return;
783 
784         enum forceCheckEach = 1.dur!"hours";
785 
786         const wlist = spinSql!(() => db.worklistApi.getCount);
787         if (local.get!ContinuesCheckTestSuite.lastWorklistCnt == 0) {
788             // first time, just initialize.
789             local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist;
790             local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach;
791             return;
792         }
793 
794         const period = conf.contCheckTestSuitePeriod.get;
795         const diffCnt = local.get!ContinuesCheckTestSuite.lastWorklistCnt - wlist;
796         // period == 0 is mostly for test purpose because it makes it possible
797         // to force a check for every mutant.
798         if (!(period == 0 || wlist % period == 0 || diffCnt >= period
799                 || Clock.currTime > local.get!ContinuesCheckTestSuite.lastCheck))
800             return;
801 
802         logger.info("Checking the test environment").collectException;
803 
804         local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist;
805         local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach;
806 
807         compile(conf.mutationCompile, conf.buildCmdTimeout, PrintCompileOnFailure(true)).match!(
808                 (Mutation.Status a) { data.ok = false; }, (bool success) {
809             data.ok = success;
810         });
811 
812         if (data.ok) {
813             try {
814                 data.ok = measureTestCommand(runner, 1).ok;
815             } catch (Exception e) {
816                 logger.error(e.msg).collectException;
817                 data.ok = false;
818             }
819         }
820 
821         if (data.ok) {
822             logger.info("Ok".color.fgGreen).collectException;
823         } else {
824             logger.info("Failed".color.fgRed).collectException;
825             logger.warning("Continues sanity check of the test suite has failed.").collectException;
826             logger.infof("Rolling back the status of the last %s mutants to status unknown.",
827                     period).collectException;
828             foreach (a; spinSql!(() => db.mutantApi.getLatestMutants(kinds, max(diffCnt, period)))) {
829                 spinSql!(() => db.mutantApi.updateMutation(a.id,
830                         Mutation.Status.unknown, ExitStatus(0), MutantTimeProfile.init));
831             }
832         }
833     }
834 
835     void opCall(ParseStdin data) {
836         import dextool.plugin.mutate.backend.diff_parser : diffFromStdin;
837         import dextool.plugin.mutate.type : Line;
838 
839         try {
840             auto constraint = local.get!PullRequest.constraint;
841             foreach (pkv; diffFromStdin.toRange(filesysIO.getOutputDir)) {
842                 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array;
843             }
844             local.get!PullRequest.constraint = constraint;
845         } catch (Exception e) {
846             logger.warning(e.msg).collectException;
847         }
848     }
849 
850     void opCall(ref AnalyzeTestCmdForTestCase data) {
851         import std.conv : to;
852         import colorlog : color;
853 
854         TestCase[][ShellCommand] found;
855 
856         try {
857             runner.captureAll(true);
858             scope (exit)
859                 runner.captureAll(false);
860 
861             // using an unreasonable timeout to make it possible to analyze for
862             // test cases and measure the test suite.
863             auto res = runTester(runner, 999.dur!"hours");
864             data.failed = res.status != Mutation.Status.alive;
865 
866             foreach (testCmd; res.output.byKeyValue) {
867                 auto analyze = testCaseAnalyzer.analyze(testCmd.key, testCmd.value, Yes.allFound);
868 
869                 analyze.match!((TestCaseAnalyzer.Success a) {
870                     found[testCmd.key] = a.found;
871                 }, (TestCaseAnalyzer.Unstable a) {
872                     logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable);
873                     found[testCmd.key] = a.found;
874                 }, (TestCaseAnalyzer.Failed a) {
875                     logger.warning("The parser that analyze the output for test case(s) failed");
876                 });
877             }
878 
879             if (data.failed) {
880                 logger.infof("Some or all tests have status %s (exit code %s)",
881                         res.status.to!string.color.fgRed, res.exitStatus.get);
882                 try {
883                     // TODO: this is a lazy way to execute the test suite again
884                     // to show the failing tests. prettify....
885                     measureTestCommand(runner, 1);
886                 } catch (Exception e) {
887                 }
888                 logger.warning("Failing test suite");
889             }
890 
891             warnIfConflictingTestCaseIdentifiers(found.byValue.joiner.array);
892         } catch (Exception e) {
893             logger.warning(e.msg).collectException;
894         }
895 
896         if (!data.failed) {
897             data.foundTestCases = found;
898         }
899     }
900 
901     void opCall(UpdateAndResetAliveMutants data) {
902         // the test cases before anything has potentially changed.
903         auto old_tcs = spinSql!(() {
904             Set!string old_tcs;
905             foreach (tc; db.testCaseApi.getDetectedTestCases)
906                 old_tcs.add(tc.name);
907             return old_tcs;
908         });
909 
910         void transaction() @safe {
911             final switch (conf.onRemovedTestCases) with (ConfigMutationTest.RemovedTestCases) {
912             case doNothing:
913                 db.testCaseApi.addDetectedTestCases(data.foundTestCases.byValue.joiner.array);
914                 break;
915             case remove:
916                 bool update;
917                 // change all mutants which, if a test case is removed, no
918                 // longer has a test case that kills it to unknown status
919                 foreach (id; db.testCaseApi.setDetectedTestCases(
920                         data.foundTestCases.byValue.joiner.array)) {
921                     if (!db.testCaseApi.hasTestCases(id)) {
922                         update = true;
923                         db.mutantApi.updateMutationStatus(id,
924                                 Mutation.Status.unknown, ExitStatus(0));
925                     }
926                 }
927                 if (update) {
928                     db.worklistApi.update(kinds, [
929                             Mutation.Status.unknown, Mutation.Status.skipped
930                             ]);
931                 }
932                 break;
933             }
934         }
935 
936         auto found_tcs = spinSql!(() @trusted {
937             auto tr = db.transaction;
938             transaction();
939 
940             Set!string found_tcs;
941             foreach (tc; db.testCaseApi.getDetectedTestCases)
942                 found_tcs.add(tc.name);
943 
944             tr.commit;
945             return found_tcs;
946         });
947 
948         printDroppedTestCases(old_tcs, found_tcs);
949 
950         if (hasNewTestCases(old_tcs, found_tcs)
951                 && conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) {
952             logger.info("Adding alive mutants to worklist").collectException;
953             spinSql!(() {
954                 db.worklistApi.update(kinds, [
955                         Mutation.Status.alive, Mutation.Status.skipped,
956                         // if these mutants are covered by the tests then they will be
957                         // removed from the worklist in PropagateCoverage.
958                         Mutation.Status.noCoverage
959                     ]);
960             });
961         }
962     }
963 
964     void opCall(RetestOldMutant data) {
965         import std.range : enumerate;
966         import dextool.plugin.mutate.backend.database.type;
967         import dextool.plugin.mutate.backend.test_mutant.timeout : resetTimeoutContext;
968 
969         const statusTypes = [EnumMembers!(Mutation.Status)].filter!(
970                 a => a != Mutation.Status.noCoverage).array;
971 
972         void printStatus(T0)(T0 oldestMutant, SysTime newestTest, SysTime newestFile) {
973             logger.info("Tests last changed ", newestTest).collectException;
974             logger.info("Source code last changed ", newestFile).collectException;
975 
976             if (!oldestMutant.empty) {
977                 logger.info("The oldest mutant is ", oldestMutant[0].updated).collectException;
978             }
979         }
980 
981         if (conf.onOldMutants == ConfigMutationTest.OldMutant.nothing)
982             return;
983 
984         // do not add mutants to worklist if there already are mutants there
985         // because other states and functions need it to sooner or late reach
986         // zero.
987         const wlist = spinSql!(() => db.worklistApi.getCount);
988         if (wlist != 0)
989             return;
990 
991         const oldestMutant = spinSql!(() => db.mutantApi.getOldestMutants(kinds, 1, statusTypes));
992         const newestTest = spinSql!(() => db.testFileApi.getNewestTestFile).orElse(
993                 TestFile.init).timeStamp;
994         const newestFile = spinSql!(() => db.getNewestFile).orElse(SysTime.init);
995         if (!oldestMutant.empty && oldestMutant[0].updated > newestTest
996                 && oldestMutant[0].updated > newestFile) {
997             // only re-test old mutants if needed.
998             logger.info("Mutation status is up to date").collectException;
999             printStatus(oldestMutant, newestTest, newestFile);
1000             return;
1001         } else {
1002             logger.info("Mutation status is out of sync").collectException;
1003             printStatus(oldestMutant, newestTest, newestFile);
1004         }
1005 
1006         const long testCnt = () {
1007             if (local.get!RetestOldMutant.resetPercentage.get == 0.0) {
1008                 return local.get!RetestOldMutant.maxReset;
1009             }
1010 
1011             const total = spinSql!(() => db.mutantApi.totalSrcMutants(kinds).count);
1012             const rval = cast(long)(1 + total
1013                     * local.get!RetestOldMutant.resetPercentage.get / 100.0);
1014             return rval;
1015         }();
1016 
1017         spinSql!(() {
1018             auto oldest = db.mutantApi.getOldestMutants(kinds, testCnt, statusTypes);
1019             logger.infof("Adding %s old mutants to the worklist", oldest.length);
1020             foreach (const old; oldest) {
1021                 db.worklistApi.add(old.id);
1022             }
1023             if (oldest.length > 3) {
1024                 logger.infof("Range of when the added mutants where last tested is %s -> %s",
1025                     oldest[0].updated, oldest[$ - 1].updated);
1026             }
1027 
1028             // because the mutants are zero it is assumed that they it is
1029             // starting from scratch thus the timeout algorithm need to
1030             // re-start from its initial state.
1031             logger.info("Resetting timeout context");
1032             resetTimeoutContext(*db);
1033         });
1034     }
1035 
1036     void opCall(Cleanup data) {
1037         autoCleanup.cleanup;
1038     }
1039 
1040     void opCall(ref CheckMutantsLeft data) {
1041         spinSql!(() { timeoutFsm.execute(*db); });
1042 
1043         data.allMutantsTested = timeoutFsm.output.done;
1044 
1045         if (timeoutFsm.output.done) {
1046             logger.info("All mutants are tested").collectException;
1047         }
1048     }
1049 
1050     void opCall(ChecksumTestCmds data) @trusted {
1051         import std.file : exists;
1052         import my.hash : Checksum64, makeCrc64Iso, checksum;
1053         import dextool.plugin.mutate.backend.database.type : ChecksumTestCmdOriginal;
1054 
1055         auto previous = spinSql!(() => db.testCmdApi.original);
1056 
1057         try {
1058             Set!Checksum64 current;
1059 
1060             foreach (testCmd; hashFiles(testCmds.filter!(a => !a.empty)
1061                     .map!(a => a.value[0]))) {
1062                 current.add(testCmd.cs);
1063 
1064                 if (testCmd.cs !in previous)
1065                     spinSql!(() => db.testCmdApi.set(testCmd.file,
1066                             ChecksumTestCmdOriginal(testCmd.cs)));
1067             }
1068 
1069             foreach (a; previous.setDifference(current).toRange)
1070                 spinSql!(() => db.testCmdApi.remove(ChecksumTestCmdOriginal(a)));
1071 
1072             local.get!MutationTest.testBinaryDb.original = current;
1073         } catch (Exception e) {
1074             logger.warning(e.msg).collectException;
1075         }
1076 
1077         local.get!MutationTest.testBinaryDb.mutated = spinSql!(
1078                 () @trusted => db.testCmdApi.mutated);
1079     }
1080 
1081     void opCall(SaveMutationScore data) {
1082         import dextool.plugin.mutate.backend.database.type : MutationScore;
1083         import dextool.plugin.mutate.backend.report.analyzers : reportScore;
1084 
1085         if (spinSql!(() => db.mutantApi.unknownSrcMutants(kinds)).count != 0)
1086             return;
1087         // users are unhappy when the score go first up and then down because
1088         // mutants are first classified as "timeout" (killed) and then changed
1089         // to alive when the timeout is increased. This lead to a trend graph
1090         // that always looks like /\ which inhibit the "motivational drive" to
1091         // work with mutation testing.  Thus if there are any timeout mutants
1092         // to test, do not sample the score. It avoids the "hill" behavior in
1093         // the trend.
1094         if (spinSql!(() => db.timeoutApi.countMutantTimeoutWorklist) != 0)
1095             return;
1096 
1097         const score = reportScore(*db, kinds).score;
1098 
1099         // 10000 mutation scores is only ~80kbyte. Should be enough entries
1100         // without taking up unreasonable amount of space.
1101         spinSql!(() @trusted {
1102             auto t = db.transaction;
1103             db.putMutationScore(MutationScore(Clock.currTime, typeof(MutationScore.score)(score)));
1104             db.trimMutationScore(10000);
1105             t.commit;
1106         });
1107     }
1108 
1109     void opCall(UpdateTestCaseTag data) {
1110         if (spinSql!(() => db.worklistApi.getCount([
1111                     Mutation.Status.alive, Mutation.Status.unknown
1112                 ])) == 0) {
1113             spinSql!(() => db.testCaseApi.removeNewTestCaseTag);
1114             logger.info("All alive in worklist tested. Removing 'new test' tag.").collectException;
1115         }
1116     }
1117 
1118     void opCall(SaveTestBinary data) {
1119         if (!local.get!MutationTest.testBinaryDb.empty)
1120             saveTestBinaryDb(local.get!MutationTest.testBinaryDb);
1121     }
1122 
1123     void opCall(ref PreCompileSut data) {
1124         import proc;
1125 
1126         logger.info("Checking the build command").collectException;
1127         compile(conf.mutationCompile, conf.buildCmdTimeout, PrintCompileOnFailure(true)).match!(
1128                 (Mutation.Status a) { data.compilationError = true; }, (bool success) {
1129             data.compilationError = !success;
1130         });
1131     }
1132 
1133     void opCall(FindTestCmds data) {
1134         auto cmds = appender!(ShellCommand[])();
1135         foreach (root; conf.testCommandDir) {
1136             try {
1137                 cmds.put(findExecutables(root.AbsolutePath)
1138                         .map!(a => ShellCommand([a] ~ conf.testCommandDirFlag)));
1139             } catch (Exception e) {
1140                 logger.warning(e.msg).collectException;
1141             }
1142         }
1143 
1144         if (!cmds.data.empty) {
1145             testCmds ~= cmds.data;
1146             runner.put(this.testCmds);
1147             logger.infof("Found test commands in %s:", conf.testCommandDir).collectException;
1148             foreach (c; cmds.data) {
1149                 logger.info(c).collectException;
1150             }
1151         }
1152     }
1153 
1154     void opCall(ChooseMode data) {
1155     }
1156 
1157     void opCall(PullRequest data) {
1158         import std.algorithm : sort;
1159         import my.set;
1160         import dextool.plugin.mutate.backend.database : MutationStatusId;
1161         import dextool.plugin.mutate.backend.type : SourceLoc;
1162 
1163         // deterministic testing of mutants and prioritized by their size.
1164         mutationOrder = MutationOrder.bySize;
1165         maxParallelInstances = 1;
1166 
1167         // make sure they are unique.
1168         Set!MutationStatusId mutantIds;
1169 
1170         foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) {
1171             const file_id = spinSql!(() => db.getFileId(kv.key));
1172             if (file_id.isNull) {
1173                 logger.infof("The file %s do not exist in the database. Skipping...",
1174                         kv.key).collectException;
1175                 continue;
1176             }
1177 
1178             foreach (l; kv.value) {
1179                 auto mutants = spinSql!(() => db.mutantApi.getMutationsOnLine(kinds,
1180                         file_id.get, SourceLoc(l.value, 0)));
1181 
1182                 const preCnt = mutantIds.length;
1183                 foreach (v; mutants)
1184                     mutantIds.add(v);
1185 
1186                 logger.infof(mutantIds.length - preCnt > 0, "Found %s mutant(s) to test (%s:%s)",
1187                         mutantIds.length - preCnt, kv.key, l.value).collectException;
1188             }
1189         }
1190 
1191         logger.infof(!mutantIds.empty, "Found %s mutants in the diff",
1192                 mutantIds.length).collectException;
1193         spinSql!(() {
1194             foreach (id; mutantIds.toArray.sort)
1195                 db.worklistApi.add(id, pullRequestWeight, MutationOrder.bySize);
1196         });
1197 
1198         local.get!CheckPullRequestMutant.startWorklistCnt = spinSql!(() => db.worklistApi.getCount);
1199         local.get!CheckPullRequestMutant.stopAfter = mutantIds.length;
1200 
1201         if (mutantIds.empty) {
1202             logger.warning("None of the locations specified with -L exists").collectException;
1203             logger.info("Available files are:").collectException;
1204             foreach (f; spinSql!(() => db.getFiles))
1205                 logger.info(f).collectException;
1206         }
1207     }
1208 
1209     void opCall(ref MeasureTestSuite data) {
1210         import std.algorithm : sum;
1211         import dextool.plugin.mutate.backend.database.type : TestCmdRuntime;
1212 
1213         if (timeout.isUserConfig) {
1214             runner.timeout = timeout.base;
1215             return;
1216         }
1217 
1218         logger.infof("Measuring the runtime of the test command(s):\n%(%s\n%)",
1219                 testCmds).collectException;
1220 
1221         auto measures = spinSql!(() => db.testCmdApi.getTestCmdRuntimes);
1222 
1223         const tester = () {
1224             try {
1225                 return measureTestCommand(runner, max(1, cast(int)(3 - measures.length)));
1226             } catch (Exception e) {
1227                 logger.error(e.msg).collectException;
1228                 return MeasureTestDurationResult(false);
1229             }
1230         }();
1231 
1232         if (tester.ok) {
1233             measures ~= tester.runtime.map!(a => TestCmdRuntime(Clock.currTime, a)).array;
1234             if (measures.length > 3) {
1235                 measures = measures[1 .. $]; // drop the oldest
1236             }
1237 
1238             auto mean = sum(measures.map!(a => a.runtime), Duration.zero) / measures.length;
1239             logger.info("Test command runtime: ", mean).collectException;
1240             timeout.set(mean);
1241             runner.timeout = timeout.value;
1242 
1243             spinSql!(() @trusted {
1244                 auto t = db.transaction;
1245                 db.testCmdApi.setTestCmdRuntimes(measures);
1246                 t.commit;
1247             });
1248         } else {
1249             data.unreliableTestSuite = true;
1250             logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected")
1251                 .collectException;
1252         }
1253     }
1254 
1255     void opCall(ref MutationTest data) {
1256         auto runnerPtr = () @trusted { return &runner; }();
1257         auto testBinaryDbPtr = () @trusted {
1258             return &local.get!MutationTest.testBinaryDb;
1259         }();
1260 
1261         try {
1262             auto g = MutationTestDriver.Global(filesysIO, db, nextMutant,
1263                     runnerPtr, testBinaryDbPtr, conf.useSkipMutant);
1264             auto driver = MutationTestDriver(g,
1265                     MutationTestDriver.TestMutantData(!(conf.mutationTestCaseAnalyze.empty
1266                         && conf.mutationTestCaseBuiltin.empty),
1267                         conf.mutationCompile, conf.buildCmdTimeout),
1268                     MutationTestDriver.TestCaseAnalyzeData(&testCaseAnalyzer));
1269 
1270             while (driver.isRunning) {
1271                 driver.execute();
1272             }
1273 
1274             if (driver.stopBecauseError) {
1275                 data.mutationError.get = true;
1276             } else {
1277                 data.result = driver.result;
1278             }
1279         } catch (Exception e) {
1280             data.mutationError.get = true;
1281             logger.error(e.msg).collectException;
1282         }
1283     }
1284 
1285     void opCall(ref CheckTimeout data) {
1286         data.timeoutUnchanged = timeout.isUserConfig || timeoutFsm.output.done;
1287     }
1288 
1289     void opCall(UpdateTimeout) {
1290         spinSql!(() { timeoutFsm.execute(*db); });
1291 
1292         const lastIter = local.get!UpdateTimeout.lastTimeoutIter;
1293 
1294         if (lastIter != timeoutFsm.output.iter) {
1295             const old = timeout.value;
1296             timeout.updateIteration(timeoutFsm.output.iter);
1297             logger.infof("Changed the timeout from %s to %s (iteration %s)",
1298                     old, timeout.value, timeoutFsm.output.iter).collectException;
1299             local.get!UpdateTimeout.lastTimeoutIter = timeoutFsm.output.iter;
1300         }
1301 
1302         runner.timeout = timeout.value;
1303     }
1304 
1305     void opCall(ref CheckPullRequestMutant data) {
1306         const left = spinSql!(() => db.worklistApi.getCount);
1307         data.noUnknownMutantsLeft.get = (
1308                 local.get!CheckPullRequestMutant.startWorklistCnt - left) >= local
1309             .get!CheckPullRequestMutant.stopAfter;
1310 
1311         logger.infof(stopCheck.aliveMutants > 0, "Found %s/%s alive mutants",
1312                 stopCheck.aliveMutants, conf.maxAlive.get).collectException;
1313     }
1314 
1315     void opCall(ref NextMutant data) {
1316         nextMutant = MutationEntry.init;
1317 
1318         // it is OK to re-test the same mutant thus using a somewhat short timeout. It isn't fatal.
1319         const giveUpAfter = Clock.currTime + 30.dur!"seconds";
1320         NextMutationEntry next;
1321         while (Clock.currTime < giveUpAfter) {
1322             next = spinSql!(() => db.nextMutation(kinds, maxParallelInstances));
1323 
1324             if (next.st == NextMutationEntry.Status.done)
1325                 break;
1326             else if (!next.entry.isNull && next.entry.get.id != local.get!NextMutant.lastTested)
1327                 break;
1328             else if (next.entry.isNull)
1329                 break;
1330         }
1331 
1332         data.noUnknownMutantsLeft.get = next.st == NextMutationEntry.Status.done;
1333 
1334         if (!next.entry.isNull) {
1335             nextMutant = next.entry.get;
1336             local.get!NextMutant.lastTested = next.entry.get.id;
1337         }
1338     }
1339 
1340     void opCall(HandleTestResult data) {
1341         saveTestResult(data.result);
1342         if (!local.get!MutationTest.testBinaryDb.empty)
1343             saveTestBinaryDb(local.get!MutationTest.testBinaryDb);
1344     }
1345 
1346     void opCall(ref CheckStopCond data) {
1347         const halt = stopCheck.isHalt;
1348         data.halt = halt != TestStopCheck.HaltReason.none;
1349 
1350         final switch (halt) with (TestStopCheck.HaltReason) {
1351         case none:
1352             break;
1353         case maxRuntime:
1354             logger.info(stopCheck.maxRuntimeToString).collectException;
1355             break;
1356         case aliveTested:
1357             logger.info("Alive mutants threshold reached").collectException;
1358             break;
1359         case overloaded:
1360             logger.info(stopCheck.overloadToString).collectException;
1361             break;
1362         }
1363         logger.warning(data.halt, "Halting").collectException;
1364     }
1365 
1366     void opCall(ref NextSchemata data) {
1367         auto schematas = local.get!NextSchemata.schematas;
1368 
1369         const threshold = schemataMutantsThreshold(schemaConf.minMutantsPerSchema.get,
1370                 local.get!NextSchemata.invalidSchematas, local.get!NextSchemata.totalSchematas);
1371 
1372         while (!schematas.empty && !data.hasSchema) {
1373             // TODO: replace with my.collection.vector
1374             const id = schematas[0];
1375             schematas = schematas[1 .. $];
1376             const mutants = spinSql!(() => db.schemaApi.countMutantsInWorklist(id, kinds));
1377 
1378             logger.infof("Schema %s has %s mutants (threshold %s)", id.get,
1379                     mutants, threshold).collectException;
1380 
1381             if (mutants >= threshold) {
1382                 data.hasSchema.get = true;
1383                 data.schemataId = id;
1384                 logger.infof("Use schema %s (%s/%s)", id.get, local.get!NextSchemata.totalSchematas - schematas.length,
1385                         local.get!NextSchemata.totalSchematas).collectException;
1386             }
1387         }
1388 
1389         local.get!NextSchemata.schematas = schematas;
1390 
1391         data.stop.get = !data.hasSchema && schemaConf.stopAfterLastSchema;
1392     }
1393 
1394     void opCall(ref SchemataTest data) {
1395         import core.thread : Thread;
1396         import core.time : dur;
1397         import dextool.plugin.mutate.backend.database : SchemaStatus;
1398         import dextool.plugin.mutate.backend.test_mutant.schemata;
1399 
1400         try {
1401             auto driver = system.spawn(&spawnSchema, filesysIO, runner, dbPath, testCaseAnalyzer, schemaConf,
1402                     data.id, stopCheck, kinds, conf.mutationCompile,
1403                     conf.buildCmdTimeout, dbSave, stat, timeout);
1404             scope (exit)
1405                 sendExit(driver, ExitReason.userShutdown);
1406             auto self = scopedActor;
1407 
1408             {
1409                 bool waiting = true;
1410                 while (waiting) {
1411                     try {
1412                         self.request(driver, infTimeout).send(IsDone.init).then((bool x) {
1413                             waiting = !x;
1414                         });
1415                     } catch (ScopedActorException e) {
1416                         if (e.error != ScopedActorError.timeout) {
1417                             logger.trace(e.error);
1418                             return;
1419                         }
1420                     }
1421                     () @trusted { Thread.sleep(100.dur!"msecs"); }();
1422                 }
1423             }
1424 
1425             FinalResult fr;
1426             {
1427                 try {
1428                     self.request(driver, delay(1.dur!"minutes"))
1429                         .send(GetDoneStatus.init).then((FinalResult x) { fr = x; });
1430                     logger.trace("final schema status ", fr.status);
1431                 } catch (ScopedActorException e) {
1432                     logger.trace(e.error);
1433                     return;
1434                 }
1435             }
1436 
1437             final switch (fr.status) with (FinalResult.Status) {
1438             case fatalError:
1439                 data.fatalError = true;
1440                 break;
1441             case invalidSchema:
1442                 local.get!NextSchemata.invalidSchematas++;
1443                 break;
1444             case ok:
1445                 break;
1446             }
1447 
1448             stopCheck.incrAliveMutants(fr.alive);
1449         } catch (Exception e) {
1450             logger.info(e.msg).collectException;
1451             logger.warning("Failed executing schemata ", data.id).collectException;
1452             spinSql!(() => db.schemaApi.markUsed(data.id, SchemaStatus.broken));
1453         }
1454     }
1455 
1456     void opCall(LoadSchematas data) {
1457         import dextool.plugin.mutate.backend.database.type : SchemaStatus;
1458 
1459         if (!schemaConf.use) {
1460             return;
1461         }
1462 
1463         auto app = appender!(SchemataId[])();
1464         foreach (id; spinSql!(() => db.schemaApi.getSchematas(SchemaStatus.broken))) {
1465             if (spinSql!(() => db.schemaApi.countMutantsInWorklist(id,
1466                     kinds)) >= schemataMutantsThreshold(schemaConf.minMutantsPerSchema.get, 0, 0)) {
1467                 app.put(id);
1468             }
1469         }
1470 
1471         logger.trace("Found schematas: ", app.data).collectException;
1472         // random reorder to reduce the chance that multipe instances of
1473         // dextool use the same schema
1474         local.get!NextSchemata.schematas = app.data.randomCover.array;
1475         local.get!NextSchemata.totalSchematas = app.data.length;
1476     }
1477 
1478     void opCall(ref Coverage data) {
1479         import dextool.plugin.mutate.backend.test_mutant.coverage;
1480 
1481         auto tracked = spinSql!(() => db.getLatestTimeStampOfTestOrSut).orElse(SysTime.init);
1482         auto covTimeStamp = spinSql!(() => db.coverageApi.getCoverageTimeStamp).orElse(
1483                 SysTime.init);
1484 
1485         if (tracked < covTimeStamp) {
1486             logger.info("Coverage information is up to date").collectException;
1487             return;
1488         } else {
1489             logger.infof("Coverage is out of date with SUT/tests (%s < %s)",
1490                     covTimeStamp, tracked).collectException;
1491         }
1492 
1493         try {
1494             auto driver = CoverageDriver(filesysIO, db, &runner, covConf,
1495                     conf.mutationCompile, conf.buildCmdTimeout);
1496             while (driver.isRunning) {
1497                 driver.execute;
1498             }
1499             data.propagate = true;
1500             data.fatalError = driver.hasFatalError;
1501         } catch (Exception e) {
1502             logger.warning(e.msg).collectException;
1503             data.fatalError = true;
1504         }
1505 
1506         if (data.fatalError)
1507             logger.warning("Error detected when trying to gather coverage information")
1508                 .collectException;
1509     }
1510 
1511     void opCall(PropagateCoverage data) {
1512         void propagate() @trusted {
1513             auto trans = db.transaction;
1514 
1515             auto noCov = db.coverageApi.getNotCoveredMutants;
1516             foreach (id; noCov)
1517                 db.mutantApi.updateMutationStatus(id, Mutation.Status.noCoverage, ExitStatus(0));
1518             db.worklistApi.remove(Mutation.Status.noCoverage);
1519 
1520             trans.commit;
1521             logger.infof("Marked %s mutants as alive because they where not covered by any test",
1522                     noCov.length);
1523         }
1524 
1525         spinSql!(() => propagate);
1526     }
1527 
1528     void saveTestResult(MutationTestResult[] results) @safe nothrow {
1529         import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft,
1530             UnknownMutantTested;
1531 
1532         foreach (a; results.filter!(a => a.status == Mutation.Status.alive)) {
1533             stopCheck.incrAliveMutants;
1534         }
1535 
1536         try {
1537             foreach (result; results)
1538                 send(dbSave, result, timeoutFsm);
1539             send(stat, UnknownMutantTested.init, cast(long) results.length);
1540         } catch (Exception e) {
1541             logger.warning("Failed to send the result to the database: ", e.msg).collectException;
1542         }
1543 
1544         try {
1545             auto self = scopedActor;
1546             self.request(stat, delay(2.dur!"msecs")).send(GetMutantsLeft.init).then((long x) {
1547                 logger.infof("%s mutants left to test.", x).collectException;
1548             });
1549         } catch (Exception e) {
1550             // just ignoring a slow answer
1551         }
1552     }
1553 
1554     void saveTestBinaryDb(ref TestBinaryDb testBinaryDb) @safe nothrow {
1555         import dextool.plugin.mutate.backend.database.type : ChecksumTestCmdMutated;
1556 
1557         spinSql!(() @trusted {
1558             auto t = db.transaction;
1559             foreach (a; testBinaryDb.added.byKeyValue) {
1560                 db.testCmdApi.add(ChecksumTestCmdMutated(a.key), a.value);
1561             }
1562             // magic number. about 10 Mbyte in the database (8+8+8)*20000
1563             db.testCmdApi.trimMutated(200000);
1564             t.commit;
1565         });
1566 
1567         testBinaryDb.clearAdded;
1568     }
1569 }
1570 
1571 private:
1572 
1573 /** A schemata must have at least this many mutants that have the status unknown
1574  * for it to be cost efficient to use schemata.
1575  *
1576  * The weights dynamically adjust with how many of the schemas that has failed
1577  * to compile.
1578  *
1579  * Params:
1580  *  checkSchemata = if the user has activated check_schemata that run all test cases before the schemata is used.
1581  */
1582 long schemataMutantsThreshold(const long minThreshold, const long invalidSchematas,
1583         const long totalSchematas) @safe pure nothrow @nogc {
1584     // "10" is a magic number that felt good but not too conservative. A future
1585     // improvement is to instead base it on the ratio between compilation time
1586     // and test suite execution time.
1587     if (totalSchematas > 0)
1588         return cast(long)(minThreshold + 10.0 * (
1589                 cast(double) invalidSchematas / cast(double) totalSchematas));
1590     return cast(long) minThreshold;
1591 }
1592 
1593 /** Compare the old test cases with those that have been found this run.
1594  *
1595  * TODO: the side effect that this function print to the console is NOT good.
1596  */
1597 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow {
1598     bool rval;
1599 
1600     auto new_tcs = found_tcs.setDifference(old_tcs);
1601     foreach (tc; new_tcs.toRange) {
1602         logger.info(!rval, "Found new test case(s):").collectException;
1603         logger.infof("%s", tc).collectException;
1604         rval = true;
1605     }
1606 
1607     return rval;
1608 }
1609 
1610 /** Compare old and new test cases to print those that have been removed.
1611  */
1612 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow {
1613     auto diff = old_tcs.setDifference(changed_tcs);
1614     auto removed = diff.toArray;
1615 
1616     logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException;
1617     foreach (tc; removed) {
1618         logger.infof("%s", tc).collectException;
1619     }
1620 }
1621 
1622 /// Returns: true if all tests cases have unique identifiers
1623 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow {
1624     Set!TestCase checked;
1625     bool conflict;
1626 
1627     foreach (tc; found_tcs) {
1628         if (checked.contains(tc)) {
1629             logger.info(!conflict,
1630                     "Found test cases that do not have global, unique identifiers")
1631                 .collectException;
1632             logger.info(!conflict,
1633                     "This make the report of test cases that has killed zero mutants unreliable")
1634                 .collectException;
1635             logger.info("%s", tc).collectException;
1636             conflict = true;
1637         }
1638     }
1639 }
1640 
1641 private:
1642 
1643 import dextool.plugin.mutate.backend.database : dbOpenTimeout;
1644 
1645 ulong toMinMemory(double percentageOfTotal) {
1646     import core.sys.posix.unistd : _SC_PHYS_PAGES, _SC_PAGESIZE, sysconf;
1647 
1648     return cast(ulong)((1.0 - (percentageOfTotal / 100.0)) * sysconf(
1649             _SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE));
1650 }
1651 
1652 auto spawnDbSaveActor(DbSaveActor.Impl self, AbsolutePath dbPath) @trusted {
1653     import dextool.plugin.mutate.backend.test_mutant.common_actors : Init, IsDone;
1654 
1655     static struct State {
1656         Database db;
1657     }
1658 
1659     auto st = tuple!("self", "state")(self, refCounted(State.init));
1660     alias Ctx = typeof(st);
1661 
1662     static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath) nothrow {
1663         try {
1664             ctx.state.get.db = spinSql!(() => Database.make(dbPath), silentLog)(dbOpenTimeout);
1665         } catch (Exception e) {
1666             logger.error(e.msg).collectException;
1667             ctx.self.shutdown;
1668         }
1669     }
1670 
1671     static void save2(ref Ctx ctx, MutationTestResult result, TimeoutFsm timeoutFsm) @safe nothrow {
1672         try {
1673             send(ctx.self, result, timeoutFsm.output.iter);
1674         } catch (Exception e) {
1675             logger.warning(e.msg).collectException;
1676         }
1677     }
1678 
1679     static void save(ref Ctx ctx, MutationTestResult result, long timeoutIter) @safe nothrow {
1680         void statusUpdate(MutationTestResult result) @safe {
1681             import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus;
1682 
1683             updateMutantStatus(ctx.state.get.db, result.id, result.status,
1684                     result.exitStatus, timeoutIter);
1685             ctx.state.get.db.mutantApi.updateMutation(result.id, result.profile);
1686             ctx.state.get.db.testCaseApi.updateMutationTestCases(result.id, result.testCases);
1687             ctx.state.get.db.worklistApi.remove(result.id);
1688         }
1689 
1690         spinSql!(() @trusted {
1691             auto t = ctx.state.get.db.transaction;
1692             statusUpdate(result);
1693             t.commit;
1694         });
1695     }
1696 
1697     static bool isDone(IsDone _) @safe nothrow {
1698         // the mailbox is a FIFO queue. all results have been saved if this returns true.
1699         return true;
1700     }
1701 
1702     self.name = "db";
1703     send(self, Init.init, dbPath);
1704     return impl(self, &init_, st, &save, st, &save2, st, &isDone);
1705 }
1706 
1707 auto spawnStatActor(StatActor.Impl self, AbsolutePath dbPath) @trusted {
1708     import dextool.plugin.mutate.backend.test_mutant.common_actors : Init,
1709         GetMutantsLeft, UnknownMutantTested, Tick, ForceUpdate;
1710 
1711     static struct State {
1712         Database db;
1713         long worklistCount;
1714     }
1715 
1716     auto st = tuple!("self", "state")(self, refCounted(State.init));
1717     alias Ctx = typeof(st);
1718 
1719     static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath) nothrow {
1720         try {
1721             ctx.state.get.db = spinSql!(() => Database.make(dbPath), silentLog)(dbOpenTimeout);
1722             send(ctx.self, Tick.init);
1723         } catch (Exception e) {
1724             logger.error(e.msg).collectException;
1725             ctx.self.shutdown;
1726         }
1727     }
1728 
1729     static void tick(ref Ctx ctx, Tick _) @safe nothrow {
1730         try {
1731             ctx.state.get.worklistCount = spinSql!(() => ctx.state.get.db.worklistApi.getCount,
1732                     logger.trace);
1733             delayedSend(ctx.self, delay(30.dur!"seconds"), Tick.init);
1734         } catch (Exception e) {
1735             logger.error(e.msg).collectException;
1736         }
1737     }
1738 
1739     static void unknownTested(ref Ctx ctx, UnknownMutantTested _, long tested) @safe nothrow {
1740         ctx.state.get.worklistCount = max(0, ctx.state.get.worklistCount - tested);
1741     }
1742 
1743     static void forceUpdate(ref Ctx ctx, ForceUpdate _) @safe nothrow {
1744         tick(ctx, Tick.init);
1745     }
1746 
1747     static long left(ref Ctx ctx, GetMutantsLeft _) @safe nothrow {
1748         return ctx.state.get.worklistCount;
1749     }
1750 
1751     self.name = "stat";
1752     send(self, Init.init, dbPath);
1753     return impl(self, &init_, st, &tick, st, &left, st, &forceUpdate, st, &unknownTested, st);
1754 }