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