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