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;
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;
22 
23 import blob_model : Blob;
24 import my.container.vector;
25 import my.fsm : Fsm, next, act, get, TypeDataMap;
26 import my.named_type;
27 import my.optional;
28 import my.set;
29 import proc : DrainElement;
30 import sumtype;
31 static import my.fsm;
32 
33 import dextool.plugin.mutate.backend.database : Database, MutationEntry,
34     NextMutationEntry, spinSql, TestFile;
35 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
36 import dextool.plugin.mutate.backend.test_mutant.common;
37 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner,
38     findExecutables, TestRunResult = TestResult;
39 import dextool.plugin.mutate.backend.type : Mutation, TestCase, ExitStatus;
40 import dextool.plugin.mutate.config;
41 import dextool.plugin.mutate.type : ShellCommand;
42 import dextool.type : AbsolutePath, ExitStatusType, Path;
43 
44 @safe:
45 
46 auto makeTestMutant() {
47     return BuildTestMutant();
48 }
49 
50 private:
51 
52 struct BuildTestMutant {
53 @safe:
54 
55     import dextool.plugin.mutate.type : MutationKind;
56 
57     private struct InternalData {
58         Mutation.Kind[] kinds;
59         FilesysIO filesys_io;
60         ConfigMutationTest config;
61     }
62 
63     private InternalData data;
64 
65     auto config(ConfigMutationTest c) @trusted nothrow {
66         data.config = c;
67         return this;
68     }
69 
70     auto mutations(MutationKind[] v) nothrow {
71         import dextool.plugin.mutate.backend.mutation_type : toInternal;
72 
73         logger.infof("mutation operators: %(%s, %)", v).collectException;
74         data.kinds = toInternal(v);
75         return this;
76     }
77 
78     ExitStatusType run(const AbsolutePath dbPath, FilesysIO fio) @trusted {
79         try {
80             auto db = Database.make(dbPath);
81             return internalRun(db, fio);
82         } catch (Exception e) {
83             logger.error(e.msg).collectException;
84         }
85 
86         return ExitStatusType.Errors;
87     }
88 
89     private ExitStatusType internalRun(ref Database db, FilesysIO fio) {
90         // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope
91         auto db_ref = () @trusted { return &db; }();
92 
93         auto cleanup = new AutoCleanup;
94         scope (exit)
95             cleanup.cleanup;
96 
97         auto driver_data = DriverData(db_ref, fio, data.kinds, cleanup, data.config);
98 
99         auto test_driver = TestDriver(driver_data);
100 
101         while (test_driver.isRunning) {
102             test_driver.execute;
103         }
104 
105         return test_driver.status;
106     }
107 }
108 
109 struct DriverData {
110     Database* db;
111     FilesysIO filesysIO;
112     Mutation.Kind[] kinds;
113     AutoCleanup autoCleanup;
114     ConfigMutationTest conf;
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;
151         return Rval(res, sw.peek);
152     }
153 
154     static void print(DrainElement[] data) @trusted {
155         import std.stdio : stdout, write;
156 
157         foreach (l; data) {
158             write(l.byUTF8);
159         }
160         stdout.flush;
161     }
162 
163     Duration[] runtimes;
164     bool failed;
165     for (int i; i < samples && !failed; ++i) {
166         try {
167             auto res = runTest;
168             final switch (res.result.status) with (TestRunResult) {
169             case Status.passed:
170                 runtimes ~= res.runtime;
171                 break;
172             case Status.failed:
173                 goto case;
174             case Status.timeout:
175                 goto case;
176             case Status.error:
177                 failed = true;
178                 print(res.result.output);
179                 break;
180             }
181             logger.infof("%s: Measured test command runtime %s", i, res.runtime);
182         } catch (Exception e) {
183             logger.error(e.msg).collectException;
184             failed = true;
185         }
186     }
187 
188     return MeasureTestDurationResult(!failed, runtimes);
189 }
190 
191 struct TestDriver {
192     import std.datetime : SysTime;
193     import dextool.plugin.mutate.backend.database : SchemataId, MutationStatusId;
194     import dextool.plugin.mutate.backend.report.analyzers : EstimateScore;
195     import dextool.plugin.mutate.backend.test_mutant.source_mutant : MutationTestDriver;
196     import dextool.plugin.mutate.backend.test_mutant.timeout : calculateTimeout, TimeoutFsm;
197     import dextool.plugin.mutate.type : MutationOrder;
198 
199     /// Runs the test commands.
200     TestRunner runner;
201 
202     ///
203     TestCaseAnalyzer testCaseAnalyzer;
204 
205     /// Stop conditions (most of them)
206     TestStopCheck stopCheck;
207 
208     static struct Global {
209         DriverData data;
210 
211         TimeoutFsm timeoutFsm;
212 
213         /// The time it takes to execute the test suite when no mutant is injected.
214         Duration testSuiteRuntime;
215 
216         /// the next mutant to test, if there are any.
217         MutationEntry nextMutant;
218 
219         // when the user manually configure the timeout it means that the
220         // timeout algorithm should not be used.
221         bool hardcodedTimeout;
222 
223         /// Test commands to execute.
224         ShellCommand[] testCmds;
225 
226         /// mutation score estimation
227         EstimateScore estimate;
228 
229         // The order to test mutants. It is either affected by the user directly or if pull request mode is activated.
230         MutationOrder mutationOrder;
231     }
232 
233     static struct UpdateTimeoutData {
234         long lastTimeoutIter;
235     }
236 
237     static struct None {
238     }
239 
240     static struct Initialize {
241         bool halt;
242     }
243 
244     static struct PullRequest {
245     }
246 
247     static struct PullRequestData {
248         import dextool.plugin.mutate.type : TestConstraint;
249 
250         TestConstraint constraint;
251     }
252 
253     static struct SanityCheck {
254         bool sanityCheckFailed;
255     }
256 
257     static struct AnalyzeTestCmdForTestCase {
258         TestCase[] foundTestCases;
259     }
260 
261     static struct UpdateAndResetAliveMutants {
262         TestCase[] foundTestCases;
263     }
264 
265     static struct ResetOldMutant {
266     }
267 
268     static struct ResetOldMutantData {
269         /// Number of mutants that where reset.
270         long maxReset;
271         NamedType!(double, Tag!"OldMutantPercentage", double.init, TagStringable) resetPercentage;
272     }
273 
274     static struct Cleanup {
275     }
276 
277     static struct CheckMutantsLeft {
278         bool allMutantsTested;
279     }
280 
281     static struct SaveMutationScore {
282     }
283 
284     static struct ParseStdin {
285     }
286 
287     static struct PreCompileSut {
288         bool compilationError;
289     }
290 
291     static struct FindTestCmds {
292     }
293 
294     static struct ChooseMode {
295     }
296 
297     static struct MeasureTestSuite {
298         bool unreliableTestSuite;
299     }
300 
301     static struct MutationTest {
302         NamedType!(bool, Tag!"MutationError", bool.init, TagStringable) mutationError;
303         MutationTestResult[] result;
304     }
305 
306     static struct CheckTimeout {
307         bool timeoutUnchanged;
308     }
309 
310     static struct NextSchemataData {
311         SchemataId[] schematas;
312         long totalSchematas;
313         long invalidSchematas;
314     }
315 
316     static struct NextSchemata {
317         NamedType!(bool, Tag!"HasSchema", bool.init, TagStringable, ImplicitConvertable) hasSchema;
318         SchemataId schemataId;
319 
320         /// stop mutation testing because the last schema has been used and the
321         /// user has configured that the testing should stop now.
322         NamedType!(bool, Tag!"StopTesting", bool.init, TagStringable, ImplicitConvertable) stop;
323     }
324 
325     static struct SchemataTest {
326         SchemataId id;
327         MutationTestResult[] result;
328         bool fatalError;
329     }
330 
331     static struct SchemataPruneUsed {
332     }
333 
334     static struct Done {
335     }
336 
337     static struct Error {
338     }
339 
340     static struct UpdateTimeout {
341     }
342 
343     static struct CheckPullRequestMutant {
344         NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft;
345     }
346 
347     static struct CheckPullRequestMutantData {
348         long startWorklistCnt;
349         long stopAfter;
350     }
351 
352     static struct NextMutant {
353         NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft;
354     }
355 
356     static struct HandleTestResult {
357         MutationTestResult[] result;
358     }
359 
360     static struct CheckStopCond {
361         bool halt;
362     }
363 
364     static struct LoadSchematas {
365     }
366 
367     static struct OverloadCheck {
368         bool sleep;
369     }
370 
371     static struct ContinuesCheckTestSuite {
372         bool ok;
373     }
374 
375     static struct ContinuesCheckTestSuiteData {
376         long lastWorklistCnt;
377         SysTime lastCheck;
378     }
379 
380     static struct Stop {
381     }
382 
383     static struct Coverage {
384         bool propagate;
385         bool fatalError;
386     }
387 
388     static struct PropagateCoverage {
389     }
390 
391     alias Fsm = my.fsm.Fsm!(None, Initialize, SanityCheck,
392             AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, ResetOldMutant,
393             Cleanup, CheckMutantsLeft, PreCompileSut, MeasureTestSuite, NextMutant,
394             MutationTest, HandleTestResult, CheckTimeout, Done, Error,
395             UpdateTimeout, CheckStopCond, PullRequest, CheckPullRequestMutant, ParseStdin,
396             FindTestCmds, ChooseMode, NextSchemata, SchemataTest, LoadSchematas,
397             SchemataPruneUsed, Stop, SaveMutationScore, OverloadCheck,
398             Coverage, PropagateCoverage, ContinuesCheckTestSuite);
399     alias LocalStateDataT = Tuple!(UpdateTimeoutData, CheckPullRequestMutantData,
400             PullRequestData, ResetOldMutantData, NextSchemataData, ContinuesCheckTestSuiteData);
401 
402     private {
403         Fsm fsm;
404         Global global;
405         TypeDataMap!(LocalStateDataT, UpdateTimeout, CheckPullRequestMutant,
406                 PullRequest, ResetOldMutant, NextSchemata, ContinuesCheckTestSuite) local;
407         bool isRunning_ = true;
408         bool isDone = false;
409     }
410 
411     this(DriverData data) {
412         this.global = Global(data);
413         this.global.timeoutFsm = TimeoutFsm(data.kinds);
414         this.global.hardcodedTimeout = !global.data.conf.mutationTesterRuntime.isNull;
415         local.get!PullRequest.constraint = global.data.conf.constraint;
416         local.get!ResetOldMutant.maxReset = global.data.conf.oldMutantsNr;
417         local.get!ResetOldMutant.resetPercentage = global.data.conf.oldMutantPercentage;
418         this.global.testCmds = global.data.conf.mutationTester;
419         this.global.mutationOrder = global.data.conf.mutationOrder;
420 
421         this.runner.useEarlyStop(global.data.conf.useEarlyTestCmdStop);
422         this.runner = TestRunner.make(global.data.conf.testPoolSize);
423         this.runner.useEarlyStop(global.data.conf.useEarlyTestCmdStop);
424         // using an unreasonable timeout to make it possible to analyze for
425         // test cases and measure the test suite.
426         this.runner.timeout = 999.dur!"hours";
427         this.runner.put(data.conf.mutationTester);
428 
429         // TODO: allow a user, as is for test_cmd, to specify an array of
430         // external analyzers.
431         this.testCaseAnalyzer = TestCaseAnalyzer(global.data.conf.mutationTestCaseBuiltin,
432                 global.data.conf.mutationTestCaseAnalyze, global.data.autoCleanup);
433 
434         this.stopCheck = TestStopCheck(global.data.conf);
435 
436         if (logger.globalLogLevel == logger.LogLevel.trace)
437             fsm.logger = (string s) { logger.trace(s); };
438     }
439 
440     static void execute_(ref TestDriver self) @trusted {
441         // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a
442         // graphical view of the state machine.
443 
444         self.fsm.next!((None a) => fsm(Initialize.init), (Initialize a) {
445             if (a.halt)
446                 return fsm(CheckStopCond.init);
447             return fsm(SanityCheck.init);
448         }, (SanityCheck a) {
449             if (a.sanityCheckFailed)
450                 return fsm(Error.init);
451             if (self.global.data.conf.unifiedDiffFromStdin)
452                 return fsm(ParseStdin.init);
453             return fsm(PreCompileSut.init);
454         }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) => fsm(
455                 UpdateAndResetAliveMutants(a.foundTestCases)),
456                 (UpdateAndResetAliveMutants a) => fsm(CheckMutantsLeft.init),
457                 (ResetOldMutant a) => fsm(UpdateTimeout.init), (Cleanup a) {
458             if (self.local.get!PullRequest.constraint.empty)
459                 return fsm(NextSchemata.init);
460             return fsm(CheckPullRequestMutant.init);
461         }, (CheckMutantsLeft a) {
462             if (a.allMutantsTested
463                 && self.global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing)
464                 return fsm(Done.init);
465             return fsm(MeasureTestSuite.init);
466         }, (SaveMutationScore a) { return fsm(Stop.init); }, (PreCompileSut a) {
467             if (a.compilationError)
468                 return fsm(Error.init);
469             if (self.global.data.conf.testCommandDir.empty)
470                 return fsm(ChooseMode.init);
471             return fsm(FindTestCmds.init);
472         }, (FindTestCmds a) { return fsm(ChooseMode.init); }, (ChooseMode a) {
473             if (!self.local.get!PullRequest.constraint.empty)
474                 return fsm(PullRequest.init);
475             if (!self.global.data.conf.mutationTestCaseAnalyze.empty
476                 || !self.global.data.conf.mutationTestCaseBuiltin.empty)
477                 return fsm(AnalyzeTestCmdForTestCase.init);
478             return fsm(CheckMutantsLeft.init);
479         }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) {
480             if (a.unreliableTestSuite)
481                 return fsm(Error.init);
482             if (self.global.data.conf.useCoverage)
483                 return fsm(Coverage.init);
484             return fsm(LoadSchematas.init);
485         }, (Coverage a) {
486             if (a.fatalError)
487                 return fsm(Error.init);
488             if (a.propagate)
489                 return fsm(PropagateCoverage.init);
490             return fsm(LoadSchematas.init);
491         }, (PropagateCoverage a) => LoadSchematas.init,
492                 (LoadSchematas a) => fsm(ResetOldMutant.init), (CheckPullRequestMutant a) {
493             if (a.noUnknownMutantsLeft)
494                 return fsm(Done.init);
495             return fsm(NextMutant.init);
496         }, (NextSchemata a) {
497             if (a.hasSchema)
498                 return fsm(SchemataTest(a.schemataId));
499             if (a.stop)
500                 return fsm(Done.init);
501             return fsm(NextMutant.init);
502         }, (SchemataTest a) {
503             if (a.fatalError)
504                 return fsm(Error.init);
505             return fsm(CheckStopCond.init);
506         }, (NextMutant a) {
507             if (a.noUnknownMutantsLeft)
508                 return fsm(CheckTimeout.init);
509             return fsm(MutationTest.init);
510         }, (UpdateTimeout a) => fsm(OverloadCheck.init), (OverloadCheck a) {
511             if (a.sleep)
512                 return fsm(CheckStopCond.init);
513             return fsm(ContinuesCheckTestSuite.init);
514         }, (ContinuesCheckTestSuite a) {
515             if (a.ok)
516                 return fsm(Cleanup.init);
517             return fsm(Error.init);
518         }, (MutationTest a) {
519             if (a.mutationError)
520                 return fsm(Error.init);
521             return fsm(HandleTestResult(a.result));
522         }, (HandleTestResult a) => fsm(CheckStopCond.init), (CheckStopCond a) {
523             if (a.halt)
524                 return fsm(Done.init);
525             return fsm(UpdateTimeout.init);
526         }, (CheckTimeout a) {
527             if (a.timeoutUnchanged)
528                 return fsm(Done.init);
529             return fsm(UpdateTimeout.init);
530         }, (SchemataPruneUsed a) => SaveMutationScore.init,
531                 (Done a) => fsm(SchemataPruneUsed.init),
532                 (Error a) => fsm(Stop.init), (Stop a) => fsm(a));
533 
534         self.fsm.act!(self);
535     }
536 
537 nothrow:
538     void execute() {
539         try {
540             execute_(this);
541         } catch (Exception e) {
542             logger.warning(e.msg).collectException;
543         }
544     }
545 
546     bool isRunning() {
547         return isRunning_;
548     }
549 
550     ExitStatusType status() {
551         if (isDone)
552             return ExitStatusType.Ok;
553         return ExitStatusType.Errors;
554     }
555 
556     void opCall(None data) {
557     }
558 
559     void opCall(ref Initialize data) {
560         logger.info("Initializing worklist").collectException;
561         // need to use 10000 because in an untested code base it is not
562         // uncommon for mutants being in the thousands.
563         spinSql!(() {
564             global.data.db.updateWorklist(global.data.kinds,
565                 Mutation.Status.unknown, 10000, global.mutationOrder);
566         });
567 
568         // detect if the system is overloaded before trying to do something
569         // slow such as compiling the SUT.
570         if (global.data.conf.loadBehavior == ConfigMutationTest.LoadBehavior.halt
571                 && stopCheck.isHalt) {
572             data.halt = true;
573         }
574     }
575 
576     void opCall(Stop data) {
577         isRunning_ = false;
578     }
579 
580     void opCall(Done data) {
581         global.data.autoCleanup.cleanup;
582         logger.info("Done!").collectException;
583         isDone = true;
584     }
585 
586     void opCall(Error data) {
587         global.data.autoCleanup.cleanup;
588     }
589 
590     void opCall(ref SanityCheck data) {
591         import core.sys.posix.sys.stat : S_IWUSR;
592         import std.path : buildPath;
593         import my.file : getAttrs;
594         import colorlog : color, Color;
595         import dextool.plugin.mutate.backend.utility : checksum, Checksum;
596 
597         logger.info("Sanity check of files to mutate").collectException;
598 
599         auto failed = appender!(string[])();
600         auto checksumFailed = appender!(string[])();
601         auto writePermissionFailed = appender!(string[])();
602         foreach (file; spinSql!(() { return global.data.db.getFiles; })) {
603             auto db_checksum = spinSql!(() {
604                 return global.data.db.getFileChecksum(file);
605             });
606 
607             try {
608                 auto abs_f = AbsolutePath(buildPath(global.data.filesysIO.getOutputDir, file));
609                 auto f_checksum = checksum(global.data.filesysIO.makeInput(abs_f).content[]);
610                 if (db_checksum != f_checksum) {
611                     checksumFailed.put(abs_f);
612                 }
613 
614                 uint attrs;
615                 if (getAttrs(abs_f, attrs)) {
616                     if ((attrs & S_IWUSR) == 0) {
617                         writePermissionFailed.put(abs_f);
618                     }
619                 } else {
620                     writePermissionFailed.put(abs_f);
621                 }
622             } catch (Exception e) {
623                 failed.put(file);
624                 logger.warningf("%s: %s", file, e.msg).collectException;
625             }
626         }
627 
628         data.sanityCheckFailed = !failed.data.empty
629             || !checksumFailed.data.empty || !writePermissionFailed.data.empty;
630 
631         if (data.sanityCheckFailed) {
632             logger.info(!failed.data.empty,
633                     "Unknown error when checking the files").collectException;
634             foreach (f; failed.data)
635                 logger.info(f).collectException;
636 
637             logger.info(!checksumFailed.data.empty,
638                     "Detected that file(s) has changed since last analyze where done")
639                 .collectException;
640             logger.info(!checksumFailed.data.empty,
641                     "Either restore the file(s) or rerun the analyze").collectException;
642             foreach (f; checksumFailed.data)
643                 logger.info(f).collectException;
644 
645             logger.info(!writePermissionFailed.data.empty,
646                     "Files to mutate are not writable").collectException;
647             foreach (f; writePermissionFailed.data)
648                 logger.info(f).collectException;
649 
650             logger.info("Failed".color.fgRed).collectException;
651         } else {
652             logger.info("Ok".color.fgGreen).collectException;
653         }
654     }
655 
656     void opCall(ref OverloadCheck data) {
657         if (global.data.conf.loadBehavior == ConfigMutationTest.LoadBehavior.slowdown
658                 && stopCheck.isOverloaded) {
659             data.sleep = true;
660             logger.info(stopCheck.overloadToString).collectException;
661             stopCheck.pause;
662         }
663     }
664 
665     void opCall(ref ContinuesCheckTestSuite data) {
666         import std.algorithm : max;
667 
668         data.ok = true;
669 
670         if (!global.data.conf.contCheckTestSuite)
671             return;
672 
673         enum forceCheckEach = 1.dur!"hours";
674 
675         const wlist = spinSql!(() => global.data.db.getWorklistCount);
676         if (local.get!ContinuesCheckTestSuite.lastWorklistCnt == 0) {
677             // first time, just initialize.
678             local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist;
679             local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach;
680             return;
681         }
682 
683         const period = global.data.conf.contCheckTestSuitePeriod.get;
684         const diffCnt = local.get!ContinuesCheckTestSuite.lastWorklistCnt - wlist;
685         if (!(wlist % period == 0 || diffCnt >= period
686                 || Clock.currTime > local.get!ContinuesCheckTestSuite.lastCheck))
687             return;
688 
689         local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist;
690         local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach;
691 
692         compile(global.data.conf.mutationCompile, global.data.conf.buildCmdTimeout, true).match!(
693                 (Mutation.Status a) { data.ok = false; }, (bool success) {
694             data.ok = success;
695         });
696 
697         if (data.ok) {
698             try {
699                 data.ok = measureTestCommand(runner, 1).ok;
700             } catch (Exception e) {
701                 logger.error(e.msg).collectException;
702                 data.ok = false;
703             }
704         }
705 
706         if (!data.ok) {
707             logger.warning("Continues sanity check of the test suite has failed.").collectException;
708             logger.infof("Rolling back the status of the last %s mutants to status unknown.",
709                     period).collectException;
710             foreach (a; spinSql!(() => global.data.db.getLatestMutants(global.data.kinds,
711                     max(diffCnt, period)))) {
712                 spinSql!(() => global.data.db.updateMutation(a.id,
713                         Mutation.Status.unknown, ExitStatus(0), MutantTimeProfile.init));
714             }
715         }
716     }
717 
718     void opCall(ParseStdin data) {
719         import dextool.plugin.mutate.backend.diff_parser : diffFromStdin;
720         import dextool.plugin.mutate.type : Line;
721 
722         try {
723             auto constraint = local.get!PullRequest.constraint;
724             foreach (pkv; diffFromStdin.toRange(global.data.filesysIO.getOutputDir)) {
725                 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array;
726             }
727             local.get!PullRequest.constraint = constraint;
728         } catch (Exception e) {
729             logger.warning(e.msg).collectException;
730         }
731     }
732 
733     void opCall(ref AnalyzeTestCmdForTestCase data) {
734         import std.datetime.stopwatch : StopWatch;
735         import dextool.plugin.mutate.backend.type : TestCase;
736 
737         TestCase[] found;
738         try {
739             auto res = runTester(runner);
740             auto analyze = testCaseAnalyzer.analyze(res.output, Yes.allFound);
741 
742             analyze.match!((TestCaseAnalyzer.Success a) { found = a.found; },
743                     (TestCaseAnalyzer.Unstable a) {
744                 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable);
745                 found = a.found;
746             }, (TestCaseAnalyzer.Failed a) {
747                 logger.warning("The parser that analyze the output for test case(s) failed");
748             });
749             found ~= global.testCmds.map!(a => TestCase(a.toShortString)).array;
750         } catch (Exception e) {
751             logger.warning(e.msg).collectException;
752         }
753 
754         warnIfConflictingTestCaseIdentifiers(found);
755         data.foundTestCases = found;
756     }
757 
758     void opCall(UpdateAndResetAliveMutants data) {
759         import std.traits : EnumMembers;
760 
761         // the test cases before anything has potentially changed.
762         auto old_tcs = spinSql!(() {
763             Set!string old_tcs;
764             foreach (tc; global.data.db.getDetectedTestCases) {
765                 old_tcs.add(tc.name);
766             }
767             return old_tcs;
768         });
769 
770         void transaction() @safe {
771             final switch (global.data.conf.onRemovedTestCases) with (
772                 ConfigMutationTest.RemovedTestCases) {
773             case doNothing:
774                 global.data.db.addDetectedTestCases(data.foundTestCases);
775                 break;
776             case remove:
777                 bool update;
778                 // change all mutants which, if a test case is removed, no
779                 // longer has a test case that kills it to unknown status
780                 foreach (id; global.data.db.setDetectedTestCases(data.foundTestCases)) {
781                     if (!global.data.db.hasTestCases(id)) {
782                         update = true;
783                         global.data.db.updateMutationStatus(id,
784                                 Mutation.Status.unknown, ExitStatus(0));
785                     }
786                 }
787                 if (update) {
788                     global.data.db.updateWorklist(global.data.kinds, Mutation.Status.unknown);
789                 }
790                 break;
791             }
792         }
793 
794         auto found_tcs = spinSql!(() @trusted {
795             auto tr = global.data.db.transaction;
796             transaction();
797 
798             Set!string found_tcs;
799             foreach (tc; global.data.db.getDetectedTestCases) {
800                 found_tcs.add(tc.name);
801             }
802 
803             tr.commit;
804             return found_tcs;
805         });
806 
807         printDroppedTestCases(old_tcs, found_tcs);
808 
809         if (hasNewTestCases(old_tcs, found_tcs)
810                 && global.data.conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) {
811             logger.info("Adding alive mutants to worklist").collectException;
812             spinSql!(() {
813                 global.data.db.updateWorklist(global.data.kinds, Mutation.Status.alive);
814                 // if these mutants are covered by the tests then they will be
815                 // removed from the worklist in PropagateCoverage.
816                 global.data.db.updateWorklist(global.data.kinds, Mutation.Status.noCoverage);
817             });
818         }
819     }
820 
821     void opCall(ResetOldMutant data) {
822         import std.range : enumerate;
823         import dextool.plugin.mutate.backend.database.type;
824 
825         void printStatus(T0)(T0 oldestMutant, SysTime newestTest, SysTime newestFile) {
826             logger.info("Tests last changed ", newestTest).collectException;
827             logger.info("Source code last changed ", newestFile).collectException;
828 
829             if (!oldestMutant.empty) {
830                 logger.info("The oldest mutant is ", oldestMutant[0].updated).collectException;
831             }
832         }
833 
834         if (global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) {
835             return;
836         }
837         if (spinSql!(() { return global.data.db.getWorklistCount; }) != 0) {
838             // do not re-test any old mutants if there are still work to do in the worklist.
839             return;
840         }
841 
842         const oldestMutant = spinSql!(() => global.data.db.getOldestMutants(global.data.kinds, 1));
843         const newestTest = spinSql!(() => global.data.db.getNewestTestFile).orElse(
844                 TestFile.init).timeStamp;
845         const newestFile = spinSql!(() => global.data.db.getNewestFile).orElse(SysTime.init);
846         if (!oldestMutant.empty && oldestMutant[0].updated > newestTest
847                 && oldestMutant[0].updated > newestFile) {
848             // only re-test old mutants if needed.
849             logger.info("Mutation status is up to date").collectException;
850             printStatus(oldestMutant, newestTest, newestFile);
851             return;
852         } else {
853             logger.info("Mutation status is out of sync").collectException;
854             printStatus(oldestMutant, newestTest, newestFile);
855         }
856 
857         const long testCnt = () {
858             if (local.get!ResetOldMutant.resetPercentage.get == 0.0) {
859                 return local.get!ResetOldMutant.maxReset;
860             }
861 
862             const total = spinSql!(() {
863                 return global.data.db.totalSrcMutants(global.data.kinds).count;
864             });
865             const rval = cast(long)(1 + total * local.get!ResetOldMutant.resetPercentage.get
866                     / 100.0);
867             return rval;
868         }();
869 
870         auto oldest = spinSql!(() {
871             return global.data.db.getOldestMutants(global.data.kinds, testCnt);
872         });
873 
874         logger.infof("Adding %s old mutants to worklist", oldest.length).collectException;
875         spinSql!(() {
876             foreach (const old; oldest.enumerate) {
877                 logger.infof("%s Last updated %s", old.index + 1,
878                     old.value.updated).collectException;
879                 global.data.db.addToWorklist(old.value.id);
880             }
881         });
882     }
883 
884     void opCall(Cleanup data) {
885         global.data.autoCleanup.cleanup;
886     }
887 
888     void opCall(ref CheckMutantsLeft data) {
889         spinSql!(() { global.timeoutFsm.execute(*global.data.db); });
890 
891         data.allMutantsTested = global.timeoutFsm.output.done;
892 
893         if (global.timeoutFsm.output.done) {
894             logger.info("All mutants are tested").collectException;
895         }
896     }
897 
898     void opCall(SaveMutationScore data) {
899         import dextool.plugin.mutate.backend.database.type : MutationScore;
900         import dextool.plugin.mutate.backend.report.analyzers : reportScore;
901 
902         if (spinSql!(() => global.data.db.unknownSrcMutants(global.data.kinds)).count != 0)
903             return;
904 
905         const score = reportScore(*global.data.db, global.data.kinds).score;
906 
907         // 10000 mutation scores is only ~80kbyte. Should be enough entries
908         // without taking up unresonable amount of space.
909         spinSql!(() @trusted {
910             auto t = global.data.db.transaction;
911             global.data.db.putMutationScore(MutationScore(Clock.currTime,
912                 typeof(MutationScore.score)(score)));
913             global.data.db.trimMutationScore(10000);
914             t.commit;
915         });
916     }
917 
918     void opCall(ref PreCompileSut data) {
919         import std.stdio : write;
920         import colorlog : color, Color;
921         import proc;
922 
923         logger.info("Checking the build command").collectException;
924         compile(global.data.conf.mutationCompile, global.data.conf.buildCmdTimeout, true).match!(
925                 (Mutation.Status a) { data.compilationError = true; }, (bool success) {
926             data.compilationError = !success;
927         });
928     }
929 
930     void opCall(FindTestCmds data) {
931         auto cmds = appender!(ShellCommand[])();
932         foreach (root; global.data.conf.testCommandDir) {
933             try {
934                 cmds.put(findExecutables(root.AbsolutePath)
935                         .map!(a => ShellCommand([a] ~ global.data.conf.testCommandDirFlag)));
936             } catch (Exception e) {
937                 logger.warning(e.msg).collectException;
938             }
939         }
940 
941         if (!cmds.data.empty) {
942             global.testCmds ~= cmds.data;
943             runner.put(this.global.testCmds);
944             logger.infof("Found test commands in %s:",
945                     global.data.conf.testCommandDir).collectException;
946             foreach (c; cmds.data) {
947                 logger.info(c).collectException;
948             }
949         }
950     }
951 
952     void opCall(ChooseMode data) {
953     }
954 
955     void opCall(PullRequest data) {
956         import std.algorithm : sort;
957         import my.set;
958         import dextool.plugin.mutate.backend.database : MutationStatusId;
959         import dextool.plugin.mutate.backend.type : SourceLoc;
960 
961         // deterministic testing of mutants and prioritized by their size.
962         global.mutationOrder = MutationOrder.bySize;
963 
964         // make sure they are unique.
965         Set!MutationStatusId mutantIds;
966 
967         foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) {
968             const file_id = spinSql!(() => global.data.db.getFileId(kv.key));
969             if (file_id.isNull) {
970                 logger.infof("The file %s do not exist in the database. Skipping...",
971                         kv.key).collectException;
972                 continue;
973             }
974 
975             foreach (l; kv.value) {
976                 auto mutants = spinSql!(() {
977                     return global.data.db.getMutationsOnLine(global.data.kinds,
978                         file_id.get, SourceLoc(l.value, 0));
979                 });
980 
981                 const preCnt = mutantIds.length;
982                 foreach (v; mutants) {
983                     mutantIds.add(v);
984                 }
985 
986                 logger.infof(mutantIds.length - preCnt > 0, "Found %s mutant(s) to test (%s:%s)",
987                         mutantIds.length - preCnt, kv.key, l.value).collectException;
988             }
989         }
990 
991         logger.infof(!mutantIds.empty, "Found %s mutants in the diff",
992                 mutantIds.length).collectException;
993         spinSql!(() {
994             foreach (id; mutantIds.toArray.sort) {
995                 // using 100000 to make a pull request mutant very high prio
996                 global.data.db.addToWorklist(id, 100000, MutationOrder.bySize);
997             }
998         });
999 
1000         local.get!CheckPullRequestMutant.startWorklistCnt = spinSql!(() {
1001             return global.data.db.getWorklistCount;
1002         });
1003         local.get!CheckPullRequestMutant.stopAfter = mutantIds.length;
1004 
1005         if (mutantIds.empty) {
1006             logger.warning("None of the locations specified with -L exists").collectException;
1007             logger.info("Available files are:").collectException;
1008             foreach (f; spinSql!(() => global.data.db.getFiles))
1009                 logger.info(f).collectException;
1010         }
1011     }
1012 
1013     void opCall(ref MeasureTestSuite data) {
1014         import std.algorithm : max, sum;
1015         import dextool.plugin.mutate.backend.database.type : TestCmdRuntime;
1016 
1017         if (!global.data.conf.mutationTesterRuntime.isNull) {
1018             global.testSuiteRuntime = global.data.conf.mutationTesterRuntime.get;
1019             return;
1020         }
1021 
1022         logger.infof("Measuring the runtime of the test command(s):\n%(%s\n%)",
1023                 global.testCmds).collectException;
1024 
1025         auto measures = spinSql!(() => global.data.db.getTestCmdRuntimes);
1026 
1027         const tester = () {
1028             try {
1029                 return measureTestCommand(runner, max(1, cast(int)(3 - measures.length)));
1030             } catch (Exception e) {
1031                 logger.error(e.msg).collectException;
1032                 return MeasureTestDurationResult(false);
1033             }
1034         }();
1035 
1036         if (tester.ok) {
1037             measures ~= tester.runtime.map!(a => TestCmdRuntime(Clock.currTime, a)).array;
1038             if (measures.length > 3) {
1039                 measures = measures[1 .. $]; // drop the oldest
1040             }
1041 
1042             auto mean = sum(measures.map!(a => a.runtime), Duration.zero) / measures.length;
1043 
1044             // The sampling of the test suite become too unreliable when the timeout is <1s.
1045             // This is a quick and dirty fix.
1046             // A proper fix requires an update of the sampler in runTester.
1047             auto t = mean < 1.dur!"seconds" ? 1.dur!"seconds" : mean;
1048             logger.info("Test command runtime: ", t).collectException;
1049             global.testSuiteRuntime = t;
1050 
1051             spinSql!(() @trusted {
1052                 auto t = global.data.db.transaction;
1053                 global.data.db.setTestCmdRuntimes(measures);
1054                 t.commit;
1055             });
1056         } else {
1057             data.unreliableTestSuite = true;
1058             logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected")
1059                 .collectException;
1060         }
1061     }
1062 
1063     void opCall(ref MutationTest data) {
1064         auto p = () @trusted { return &runner; }();
1065 
1066         try {
1067             auto g = MutationTestDriver.Global(global.data.filesysIO,
1068                     global.data.db, global.nextMutant, p);
1069             auto driver = MutationTestDriver(g,
1070                     MutationTestDriver.TestMutantData(!(global.data.conf.mutationTestCaseAnalyze.empty
1071                         && global.data.conf.mutationTestCaseBuiltin.empty),
1072                         global.data.conf.mutationCompile, global.data.conf.buildCmdTimeout),
1073                     MutationTestDriver.TestCaseAnalyzeData(&testCaseAnalyzer));
1074 
1075             while (driver.isRunning) {
1076                 driver.execute();
1077             }
1078 
1079             if (driver.stopBecauseError) {
1080                 data.mutationError.get = true;
1081             } else {
1082                 data.result = driver.result;
1083             }
1084         } catch (Exception e) {
1085             data.mutationError.get = true;
1086             logger.error(e.msg).collectException;
1087         }
1088     }
1089 
1090     void opCall(ref CheckTimeout data) {
1091         data.timeoutUnchanged = global.hardcodedTimeout || global.timeoutFsm.output.done;
1092     }
1093 
1094     void opCall(UpdateTimeout) {
1095         spinSql!(() { global.timeoutFsm.execute(*global.data.db); });
1096 
1097         const lastIter = local.get!UpdateTimeout.lastTimeoutIter;
1098 
1099         if (lastIter != global.timeoutFsm.output.iter) {
1100             logger.infof("Changed the timeout from %s to %s (iteration %s)",
1101                     calculateTimeout(lastIter, global.testSuiteRuntime),
1102                     calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime),
1103                     global.timeoutFsm.output.iter).collectException;
1104             local.get!UpdateTimeout.lastTimeoutIter = global.timeoutFsm.output.iter;
1105         }
1106 
1107         runner.timeout = calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime);
1108     }
1109 
1110     void opCall(ref CheckPullRequestMutant data) {
1111         const left = spinSql!(() { return global.data.db.getWorklistCount; });
1112         data.noUnknownMutantsLeft.get = (
1113                 local.get!CheckPullRequestMutant.startWorklistCnt - left) >= local
1114             .get!CheckPullRequestMutant.stopAfter;
1115 
1116         logger.infof(stopCheck.aliveMutants > 0, "Found %s/%s alive mutants",
1117                 stopCheck.aliveMutants, global.data.conf.maxAlive.get).collectException;
1118     }
1119 
1120     void opCall(ref NextMutant data) {
1121         global.nextMutant = MutationEntry.init;
1122 
1123         auto next = spinSql!(() {
1124             return global.data.db.nextMutation(global.data.kinds, global.mutationOrder);
1125         });
1126 
1127         data.noUnknownMutantsLeft.get = next.st == NextMutationEntry.Status.done;
1128 
1129         if (!next.entry.isNull)
1130             global.nextMutant = next.entry.get;
1131     }
1132 
1133     void opCall(HandleTestResult data) {
1134         saveTestResult(data.result);
1135     }
1136 
1137     void opCall(ref CheckStopCond data) {
1138         const halt = stopCheck.isHalt;
1139         data.halt = halt != TestStopCheck.HaltReason.none;
1140 
1141         final switch (halt) with (TestStopCheck.HaltReason) {
1142         case none:
1143             break;
1144         case maxRuntime:
1145             logger.info(stopCheck.maxRuntimeToString).collectException;
1146             break;
1147         case aliveTested:
1148             logger.info("Alive mutants threshold reached").collectException;
1149             break;
1150         case overloaded:
1151             logger.info(stopCheck.overloadToString).collectException;
1152             break;
1153         }
1154         logger.warning(data.halt, "Halting").collectException;
1155     }
1156 
1157     void opCall(ref NextSchemata data) {
1158         auto schematas = local.get!NextSchemata.schematas;
1159 
1160         const threshold = schemataMutantsThreshold(global.data.conf.minMutantsPerSchema.get,
1161                 local.get!NextSchemata.invalidSchematas, local.get!NextSchemata.totalSchematas);
1162 
1163         while (!schematas.empty && !data.hasSchema) {
1164             // TODO: replace with my.collection.vector
1165             const id = schematas[0];
1166             schematas = schematas[1 .. $];
1167             const mutants = spinSql!(() {
1168                 return global.data.db.schemataMutantsCount(id, global.data.kinds);
1169             });
1170 
1171             logger.infof("Schema %s has %s mutants (threshold %s)", id.get,
1172                     mutants, threshold).collectException;
1173 
1174             if (mutants >= threshold) {
1175                 data.hasSchema.get = true;
1176                 data.schemataId = id;
1177                 logger.infof("Use schema %s (%s/%s)", id.get, local.get!NextSchemata.totalSchematas - schematas.length,
1178                         local.get!NextSchemata.totalSchematas).collectException;
1179             }
1180         }
1181 
1182         local.get!NextSchemata.schematas = schematas;
1183 
1184         data.stop.get = !data.hasSchema && global.data.conf.stopAfterLastSchema;
1185     }
1186 
1187     void opCall(ref SchemataTest data) {
1188         import dextool.plugin.mutate.backend.test_mutant.schemata;
1189 
1190         // only remove schemas that are of no further use.
1191         bool remove = true;
1192         void updateRemove(MutationTestResult[] result) {
1193             foreach (a; data.result) {
1194                 final switch (a.status) with (Mutation.Status) {
1195                 case unknown:
1196                     goto case;
1197                 case noCoverage:
1198                     goto case;
1199                 case alive:
1200                     remove = false;
1201                     return;
1202                 case killed:
1203                     goto case;
1204                 case timeout:
1205                     goto case;
1206                 case killedByCompiler:
1207                     break;
1208                 }
1209             }
1210         }
1211 
1212         void save(MutationTestResult[] result) {
1213             updateRemove(result);
1214             saveTestResult(result);
1215             logger.infof(result.length > 0, "Saving %s schemata mutant results",
1216                     result.length).collectException;
1217         }
1218 
1219         try {
1220             auto driver = SchemataTestDriver(global.data.filesysIO, &runner, global.data.db,
1221                     &testCaseAnalyzer, global.data.conf.userRuntimeCtrl, data.id, stopCheck,
1222                     global.data.kinds, global.data.conf.mutationCompile,
1223                     global.data.conf.buildCmdTimeout, global.data.conf.logSchemata);
1224 
1225             const saveResultInterval = 20.dur!"minutes";
1226             auto nextSave = Clock.currTime + saveResultInterval;
1227             while (driver.isRunning) {
1228                 driver.execute;
1229 
1230                 // to avoid loosing results in case of a crash etc save them
1231                 // continuously
1232                 if (Clock.currTime > nextSave && !driver.hasFatalError) {
1233                     save(driver.popResult);
1234                     nextSave = Clock.currTime + saveResultInterval;
1235                 }
1236             }
1237 
1238             data.fatalError = driver.hasFatalError;
1239 
1240             if (driver.hasFatalError) {
1241                 // do nothing
1242             } else if (driver.isInvalidSchema) {
1243                 local.get!NextSchemata.invalidSchematas++;
1244             } else {
1245                 save(driver.popResult);
1246             }
1247 
1248             if (remove) {
1249                 spinSql!(() { global.data.db.markUsed(data.id); });
1250             }
1251 
1252         } catch (Exception e) {
1253             logger.info(e.msg).collectException;
1254             logger.warning("Failed executing schemata ", data.id).collectException;
1255         }
1256     }
1257 
1258     void opCall(SchemataPruneUsed data) {
1259         try {
1260             const removed = global.data.db.pruneUsedSchemas;
1261 
1262             if (removed != 0) {
1263                 logger.infof("Removed %s schemas from the database", removed);
1264                 // vacuum the database because schemas take up a significant
1265                 // amount of space.
1266                 global.data.db.vacuum;
1267             }
1268         } catch (Exception e) {
1269             logger.warning(e.msg).collectException;
1270         }
1271     }
1272 
1273     void opCall(LoadSchematas data) {
1274         if (!global.data.conf.useSchemata) {
1275             return;
1276         }
1277 
1278         auto app = appender!(SchemataId[])();
1279         foreach (id; spinSql!(() { return global.data.db.getSchematas(); })) {
1280             if (spinSql!(() {
1281                     return global.data.db.schemataMutantsCount(id, global.data.kinds);
1282                 }) >= schemataMutantsThreshold(global.data.conf.minMutantsPerSchema.get, 0, 0)) {
1283                 app.put(id);
1284             }
1285         }
1286 
1287         logger.trace("Found schematas: ", app.data).collectException;
1288         // random reorder to reduce the chance that multipe instances of
1289         // dextool use the same schema
1290         local.get!NextSchemata.schematas = app.data.randomCover.array;
1291         local.get!NextSchemata.totalSchematas = app.data.length;
1292     }
1293 
1294     void opCall(ref Coverage data) {
1295         import dextool.plugin.mutate.backend.test_mutant.coverage;
1296 
1297         auto tracked = spinSql!(() => global.data.db.getLatestTimeStampOfTestOrSut).orElse(
1298                 SysTime.init);
1299         auto covTimeStamp = spinSql!(() => global.data.db.getCoverageTimeStamp).orElse(
1300                 SysTime.init);
1301 
1302         if (tracked < covTimeStamp) {
1303             logger.info("Coverage information is up to date").collectException;
1304             return;
1305         } else {
1306             logger.infof("Coverage is out of date with SUT/tests (%s < %s)",
1307                     covTimeStamp, tracked).collectException;
1308         }
1309 
1310         try {
1311             auto driver = CoverageDriver(global.data.filesysIO, global.data.db, &runner,
1312                     global.data.conf.userRuntimeCtrl, global.data.conf.mutationCompile,
1313                     global.data.conf.buildCmdTimeout, global.data.conf.logCoverage.get);
1314             while (driver.isRunning) {
1315                 driver.execute;
1316             }
1317             data.propagate = true;
1318             data.fatalError = driver.hasFatalError;
1319         } catch (Exception e) {
1320             logger.warning(e.msg).collectException;
1321             data.fatalError = true;
1322         }
1323 
1324         if (data.fatalError)
1325             logger.warning("Error detected when trying to gather coverage information")
1326                 .collectException;
1327     }
1328 
1329     void opCall(PropagateCoverage data) {
1330         void propagate() @trusted {
1331             auto trans = global.data.db.transaction;
1332 
1333             auto noCov = global.data.db.getNotCoveredMutants;
1334             foreach (id; noCov) {
1335                 global.data.db.updateMutationStatus(id, Mutation.Status.noCoverage, ExitStatus(0));
1336                 global.data.db.removeFromWorklist(id);
1337             }
1338 
1339             trans.commit;
1340             logger.infof("Marked %s mutants as alive because they where not covered by any test",
1341                     noCov.length);
1342         }
1343 
1344         spinSql!(() => propagate);
1345     }
1346 
1347     void saveTestResult(MutationTestResult[] results) @safe nothrow {
1348         void statusUpdate(MutationTestResult result) @safe {
1349             import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus;
1350 
1351             const cnt_action = () {
1352                 if (result.status == Mutation.Status.alive)
1353                     return Database.CntAction.incr;
1354                 return Database.CntAction.reset;
1355             }();
1356 
1357             global.estimate.update(result.status);
1358 
1359             updateMutantStatus(*global.data.db, result.id, result.status,
1360                     result.exitStatus, global.timeoutFsm.output.iter);
1361             global.data.db.updateMutation(result.id, result.profile);
1362             global.data.db.updateMutationTestCases(result.id, result.testCases);
1363             global.data.db.removeFromWorklist(result.id);
1364 
1365             if (result.status == Mutation.Status.alive)
1366                 stopCheck.incrAliveMutants;
1367         }
1368 
1369         spinSql!(() @trusted {
1370             auto t = global.data.db.transaction;
1371             foreach (a; results) {
1372                 statusUpdate(a);
1373             }
1374             t.commit;
1375         });
1376 
1377         const left = spinSql!(() { return global.data.db.getWorklistCount; });
1378         logger.infof("%s mutants left to test. Estimated mutation score %.3s (error %.3s)", left,
1379                 global.estimate.value.get, global.estimate.error.get).collectException;
1380     }
1381 }
1382 
1383 private:
1384 
1385 /** A schemata must have at least this many mutants that have the status unknown
1386  * for it to be cost efficient to use schemata.
1387  *
1388  * The weights dynamically adjust with how many of the schemas that has failed
1389  * to compile.
1390  *
1391  * Params:
1392  *  checkSchemata = if the user has activated check_schemata that run all test cases before the schemata is used.
1393  */
1394 long schemataMutantsThreshold(const long minThreshold, const long invalidSchematas,
1395         const long totalSchematas) @safe pure nothrow @nogc {
1396     // "10" is a magic number that felt good but not too conservative. A future
1397     // improvement is to instead base it on the ratio between compilation time
1398     // and test suite execution time.
1399     if (totalSchematas > 0)
1400         return cast(long)(minThreshold + 10.0 * (
1401                 cast(double) invalidSchematas / cast(double) totalSchematas));
1402     return cast(long) minThreshold;
1403 }
1404 
1405 /** Compare the old test cases with those that have been found this run.
1406  *
1407  * TODO: the side effect that this function print to the console is NOT good.
1408  */
1409 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow {
1410     bool rval;
1411 
1412     auto new_tcs = found_tcs.setDifference(old_tcs);
1413     foreach (tc; new_tcs.toRange) {
1414         logger.info(!rval, "Found new test case(s):").collectException;
1415         logger.infof("%s", tc).collectException;
1416         rval = true;
1417     }
1418 
1419     return rval;
1420 }
1421 
1422 /** Compare old and new test cases to print those that have been removed.
1423  */
1424 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow {
1425     auto diff = old_tcs.setDifference(changed_tcs);
1426     auto removed = diff.toArray;
1427 
1428     logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException;
1429     foreach (tc; removed) {
1430         logger.infof("%s", tc).collectException;
1431     }
1432 }
1433 
1434 /// Returns: true if all tests cases have unique identifiers
1435 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow {
1436     Set!TestCase checked;
1437     bool conflict;
1438 
1439     foreach (tc; found_tcs) {
1440         if (checked.contains(tc)) {
1441             logger.info(!conflict,
1442                     "Found test cases that do not have global, unique identifiers")
1443                 .collectException;
1444             logger.info(!conflict,
1445                     "This make the report of test cases that has killed zero mutants unreliable")
1446                 .collectException;
1447             logger.info("%s", tc).collectException;
1448             conflict = true;
1449         }
1450     }
1451 }
1452 
1453 private:
1454 
1455 /**
1456 DESCRIPTION
1457 
1458      The getloadavg() function returns the number of processes in the system
1459      run queue averaged over various periods of time.  Up to nelem samples are
1460      retrieved and assigned to successive elements of loadavg[].  The system
1461      imposes a maximum of 3 samples, representing averages over the last 1, 5,
1462      and 15 minutes, respectively.
1463 
1464 
1465 DIAGNOSTICS
1466 
1467      If the load average was unobtainable, -1 is returned; otherwise, the num-
1468      ber of samples actually retrieved is returned.
1469  */
1470 extern (C) int getloadavg(double* loadavg, int nelem) nothrow;