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