1 /**
2 Copyright: Copyright (c) 2020, 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.schemata;
11 
12 import logger = std.experimental.logger;
13 import std.algorithm : sort, map, filter, among;
14 import std.array : empty, array, appender;
15 import std.conv : to;
16 import std.datetime : Duration, dur, Clock, SysTime;
17 import std.datetime.stopwatch : StopWatch, AutoStart;
18 import std.exception : collectException;
19 import std.format : format;
20 import std.typecons : Tuple, tuple;
21 
22 import blob_model;
23 import colorlog;
24 import miniorm : spinSql, silentLog;
25 import my.actor;
26 import my.gc.refc;
27 import my.optional;
28 import my.container.vector;
29 import proc : DrainElement;
30 import sumtype;
31 
32 import my.path;
33 import my.set;
34 
35 import dextool.plugin.mutate.backend.database : MutationStatusId, Database,
36     spinSql, SchemataId, Schemata;
37 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
38 import dextool.plugin.mutate.backend.test_mutant.common;
39 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor;
40 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult;
41 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig;
42 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum;
43 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand,
44     UserRuntime, SchemaRuntime;
45 import dextool.plugin.mutate.config : ConfigSchema;
46 
47 @safe:
48 
49 private {
50     struct Init {
51     }
52 
53     struct UpdateWorkList {
54     }
55 
56     struct Mark {
57     }
58 
59     struct InjectAndCompile {
60     }
61 
62     struct ScheduleTestMsg {
63     }
64 
65     struct RestoreMsg {
66     }
67 
68     struct StartTestMsg {
69     }
70 
71     struct CheckStopCondMsg {
72     }
73 }
74 
75 struct IsDone {
76 }
77 
78 struct GetDoneStatus {
79 }
80 
81 struct FinalResult {
82     enum Status {
83         fatalError,
84         invalidSchema,
85         ok
86     }
87 
88     Status status;
89     int alive;
90 }
91 
92 alias SchemaActor = typedActor!(void function(Init, AbsolutePath, ShellCommand, Duration),
93         bool function(IsDone), void function(UpdateWorkList), FinalResult function(GetDoneStatus),
94         void function(SchemaTestResult), void function(Mark, FinalResult.Status), void function(InjectAndCompile,
95             ShellCommand, Duration), void function(RestoreMsg), void function(StartTestMsg),
96         void function(ScheduleTestMsg), void function(CheckStopCondMsg));
97 
98 auto spawnSchema(SchemaActor.Impl self, FilesysIO fio, ref TestRunner runner, AbsolutePath dbPath,
99         TestCaseAnalyzer testCaseAnalyzer, ConfigSchema conf, SchemataId id,
100         TestStopCheck stopCheck, Mutation.Kind[] kinds,
101         ShellCommand buildCmd, Duration buildCmdTimeout, DbSaveActor.Address dbSave,
102         StatActor.Address stat, TimeoutConfig timeoutConf) @trusted {
103 
104     static struct State {
105         SchemataId id;
106         Mutation.Kind[] kinds;
107         TestStopCheck stopCheck;
108         DbSaveActor.Address dbSave;
109         StatActor.Address stat;
110         TimeoutConfig timeoutConf;
111         FilesysIO fio;
112         TestRunner runner;
113         TestCaseAnalyzer analyzer;
114         ConfigSchema conf;
115 
116         Database db;
117 
118         AbsolutePath[] modifiedFiles;
119 
120         InjectIdResult injectIds;
121 
122         ScheduleTest scheduler;
123 
124         Set!MutationStatusId whiteList;
125 
126         Duration compileTime;
127 
128         int alive;
129 
130         bool hasFatalError;
131         bool isInvalidSchema;
132 
133         bool isRunning;
134     }
135 
136     auto st = tuple!("self", "state")(self, refCounted(State(id, kinds, stopCheck,
137             dbSave, stat, timeoutConf, fio.dup, runner.dup, testCaseAnalyzer, conf)));
138     alias Ctx = typeof(st);
139 
140     static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath,
141             ShellCommand buildCmd, Duration buildCmdTimeout) nothrow {
142         import dextool.plugin.mutate.backend.database : dbOpenTimeout;
143 
144         try {
145             ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout);
146             ctx.state.get.scheduler = () {
147                 TestMutantActor.Address[] testers;
148                 foreach (_0; 0 .. ctx.state.get.conf.parallelMutants) {
149                     auto a = ctx.self.homeSystem.spawn(&spawnTestMutant,
150                             ctx.state.get.runner.dup, ctx.state.get.analyzer);
151                     a.linkTo(ctx.self.address);
152                     testers ~= a;
153                 }
154                 return ScheduleTest(testers);
155             }();
156 
157             ctx.state.get.injectIds = mutantsFromSchema(ctx.state.get.db,
158                     ctx.state.get.id, ctx.state.get.kinds);
159 
160             if (!ctx.state.get.injectIds.empty) {
161                 send(ctx.self, UpdateWorkList.init);
162                 send(ctx.self, InjectAndCompile.init, buildCmd, buildCmdTimeout);
163                 send(ctx.self, CheckStopCondMsg.init);
164 
165                 ctx.state.get.isRunning = true;
166             }
167         } catch (Exception e) {
168             ctx.state.get.hasFatalError = true;
169             logger.error(e.msg).collectException;
170         }
171     }
172 
173     static bool isDone(ref Ctx ctx, IsDone _) {
174         return !ctx.state.get.isRunning;
175     }
176 
177     static void mark(ref Ctx ctx, Mark _, FinalResult.Status status) {
178         import std.traits : EnumMembers;
179         import dextool.plugin.mutate.backend.database : SchemaStatus;
180 
181         SchemaStatus schemaStatus;
182         final switch (status) with (FinalResult.Status) {
183         case fatalError:
184             break;
185         case invalidSchema:
186             schemaStatus = SchemaStatus.broken;
187             break;
188         case ok:
189             const total = spinSql!(() => ctx.state.get.db.schemaApi.countMutants(ctx.state.get.id,
190                     ctx.state.get.kinds, [EnumMembers!(Mutation.Status)]));
191             const killed = spinSql!(() => ctx.state.get.db.schemaApi.countMutants(ctx.state.get.id,
192                     ctx.state.get.kinds, [
193                         Mutation.Status.killed, Mutation.Status.timeout,
194                         Mutation.Status.memOverload
195                     ]));
196             schemaStatus = (total == killed) ? SchemaStatus.allKilled : SchemaStatus.ok;
197             break;
198         }
199 
200         spinSql!(() => ctx.state.get.db.schemaApi.markUsed(ctx.state.get.id, schemaStatus));
201     }
202 
203     static void updateWlist(ref Ctx ctx, UpdateWorkList _) @safe nothrow {
204         if (!ctx.state.get.isRunning)
205             return;
206 
207         delayedSend(ctx.self, 1.dur!"minutes".delay, UpdateWorkList.init).collectException;
208         // TODO: should injectIds be updated too?
209 
210         try {
211             ctx.state.get.whiteList = spinSql!(
212                     () => ctx.state.get.db.schemaApi.getSchemataMutants(ctx.state.get.id,
213                     ctx.state.get.kinds)).toSet;
214             logger.trace("update schema worklist mutants: ", ctx.state.get.whiteList.length);
215             debug logger.trace("update schema worklist: ", ctx.state.get.whiteList.toRange);
216         } catch (Exception e) {
217             logger.trace(e.msg).collectException;
218         }
219     }
220 
221     static FinalResult doneStatus(ref Ctx ctx, GetDoneStatus _) @safe nothrow {
222         FinalResult.Status status = () {
223             if (ctx.state.get.hasFatalError)
224                 return FinalResult.Status.fatalError;
225             if (ctx.state.get.isInvalidSchema)
226                 return FinalResult.Status.invalidSchema;
227             return FinalResult.Status.ok;
228         }();
229 
230         if (!ctx.state.get.isRunning)
231             send(ctx.self, Mark.init, status).collectException;
232 
233         return FinalResult(status, ctx.state.get.alive);
234     }
235 
236     static void save(ref Ctx ctx, SchemaTestResult data) {
237         import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft,
238             UnknownMutantTested;
239 
240         void update(MutationTestResult a) {
241             final switch (a.status) with (Mutation.Status) {
242             case skipped:
243                 goto case;
244             case unknown:
245                 goto case;
246             case equivalent:
247                 goto case;
248             case noCoverage:
249                 goto case;
250             case alive:
251                 ctx.state.get.alive++;
252                 ctx.state.get.stopCheck.incrAliveMutants(1);
253                 return;
254             case killed:
255                 goto case;
256             case timeout:
257                 goto case;
258             case memOverload:
259                 goto case;
260             case killedByCompiler:
261                 break;
262             }
263         }
264 
265         debug logger.trace(data);
266 
267         if (!data.unstable.empty) {
268             logger.warningf("Unstable test cases found: [%-(%s, %)]", data.unstable);
269             logger.info(
270                     "As configured the result is ignored which will force the mutant to be re-tested");
271             return;
272         }
273 
274         update(data.result);
275 
276         auto result = data.result;
277         result.profile = MutantTimeProfile(ctx.state.get.compileTime, data.testTime);
278         ctx.state.get.compileTime = Duration.zero;
279 
280         logger.infof("%s:%s (%s)", data.result.status,
281                 data.result.exitStatus.get, result.profile).collectException;
282         logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`,
283                 data.result.testCases.sort.map!"a.name").collectException;
284 
285         send(ctx.state.get.dbSave, result, ctx.state.get.timeoutConf.iter);
286         send(ctx.state.get.stat, UnknownMutantTested.init, 1L);
287 
288         // an error handler is required because the stat actor can be held up
289         // for more than a minute.
290         ctx.self.request(ctx.state.get.stat, delay(5.dur!"seconds"))
291             .send(GetMutantsLeft.init).then((long x) {
292                 logger.infof("%s mutants left to test.", x);
293             }, (ref Actor self, ErrorMsg) {});
294 
295         if (ctx.state.get.injectIds.empty)
296             send(ctx.self, RestoreMsg.init).collectException;
297     }
298 
299     static void injectAndCompile(ref Ctx ctx, InjectAndCompile _,
300             ShellCommand buildCmd, Duration buildCmdTimeout) @safe nothrow {
301         try {
302             auto sw = StopWatch(AutoStart.yes);
303             scope (exit)
304                 ctx.state.get.compileTime = sw.peek;
305 
306             auto codeInject = CodeInject(ctx.state.get.fio, ctx.state.get.conf, ctx.state.get.id);
307             ctx.state.get.modifiedFiles = codeInject.inject(ctx.state.get.db);
308             codeInject.compile(buildCmd, buildCmdTimeout);
309 
310             if (ctx.state.get.conf.sanityCheckSchemata) {
311                 logger.info("Sanity check of the generated schemata");
312                 const sanity = sanityCheck(ctx.state.get.runner);
313                 if (sanity.isOk) {
314                     if (ctx.state.get.timeoutConf.base < sanity.runtime) {
315                         ctx.state.get.timeoutConf.set(sanity.runtime);
316                         ctx.state.get.runner.timeout = ctx.state.get.timeoutConf.value;
317                     }
318 
319                     logger.info("Ok".color(Color.green), ". Using test suite timeout ",
320                             ctx.state.get.timeoutConf.value).collectException;
321                     send(ctx.self, StartTestMsg.init);
322                 } else {
323                     logger.info("Skipping the schemata because the test suite failed".color(Color.yellow)
324                             .toString);
325                     ctx.state.get.isInvalidSchema = true;
326                     send(ctx.self, RestoreMsg.init).collectException;
327                 }
328             } else {
329                 send(ctx.self, StartTestMsg.init);
330             }
331         } catch (Exception e) {
332             ctx.state.get.isInvalidSchema = true;
333             send(ctx.self, RestoreMsg.init).collectException;
334             logger.warning(e.msg).collectException;
335         }
336     }
337 
338     static void restore(ref Ctx ctx, RestoreMsg _) @safe nothrow {
339         try {
340             restoreFiles(ctx.state.get.modifiedFiles, ctx.state.get.fio);
341             ctx.state.get.isRunning = false;
342         } catch (Exception e) {
343             ctx.state.get.hasFatalError = true;
344             logger.error(e.msg).collectException;
345         }
346     }
347 
348     static void startTest(ref Ctx ctx, StartTestMsg _) @safe nothrow {
349         try {
350             foreach (_0; 0 .. ctx.state.get.scheduler.testers.length)
351                 send(ctx.self, ScheduleTestMsg.init);
352 
353             logger.tracef("sent %s ScheduleTestMsg", ctx.state.get.scheduler.testers.length);
354         } catch (Exception e) {
355             ctx.state.get.hasFatalError = true;
356             ctx.state.get.isRunning = false;
357             logger.error(e.msg).collectException;
358         }
359     }
360 
361     static void test(ref Ctx ctx, ScheduleTestMsg _) nothrow {
362         // TODO: move this printer to another thread because it perform
363         // significant DB lookup and can potentially slow down the testing.
364         void print(MutationStatusId statusId) {
365             import dextool.plugin.mutate.backend.generate_mutant : makeMutationText;
366 
367             auto id = spinSql!(() => ctx.state.get.db.mutantApi.getMutationId(statusId));
368             if (id.isNull)
369                 return;
370             auto entry_ = spinSql!(() => ctx.state.get.db.mutantApi.getMutation(id.get));
371             if (entry_.isNull)
372                 return;
373             auto entry = entry_.get;
374 
375             try {
376                 const file = ctx.state.get.fio.toAbsoluteRoot(entry.file);
377                 auto txt = makeMutationText(ctx.state.get.fio.makeInput(file),
378                         entry.mp.offset, entry.mp.mutations[0].kind, entry.lang);
379                 debug logger.trace(entry);
380                 logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original,
381                         txt.mutation, file, entry.sloc.line, entry.sloc.column);
382             } catch (Exception e) {
383                 logger.info(e.msg).collectException;
384             }
385         }
386 
387         try {
388             if (!ctx.state.get.isRunning)
389                 return;
390 
391             if (ctx.state.get.injectIds.empty) {
392                 logger.trace("no mutants left to test");
393                 return;
394             }
395 
396             if (ctx.state.get.scheduler.empty) {
397                 logger.trace("no free worker");
398                 delayedSend(ctx.self, 1.dur!"seconds".delay, ScheduleTestMsg.init);
399                 return;
400             }
401 
402             if (ctx.state.get.stopCheck.isOverloaded) {
403                 logger.info(ctx.state.get.stopCheck.overloadToString).collectException;
404                 delayedSend(ctx.self, 30.dur!"seconds".delay, ScheduleTestMsg.init);
405                 ctx.state.get.stopCheck.pause;
406                 return;
407             }
408 
409             auto m = ctx.state.get.injectIds.front;
410             ctx.state.get.injectIds.popFront;
411 
412             if (m.statusId in ctx.state.get.whiteList) {
413                 auto testerId = ctx.state.get.scheduler.pop;
414                 auto tester = ctx.state.get.scheduler.get(testerId);
415                 print(m.statusId);
416                 ctx.self.request(tester, infTimeout).send(m).capture(ctx,
417                         testerId).then((ref Capture!(Ctx, size_t) ctx, SchemaTestResult x) {
418                     ctx[0].state.get.scheduler.put(ctx[1]);
419                     send(ctx[0].self, x);
420                     send(ctx[0].self, ScheduleTestMsg.init);
421                 });
422             } else {
423                 debug logger.tracef("%s not in whitelist. Skipping", m);
424                 send(ctx.self, ScheduleTestMsg.init);
425             }
426         } catch (Exception e) {
427             ctx.state.get.hasFatalError = true;
428             ctx.state.get.isRunning = false;
429             logger.error(e.msg).collectException;
430         }
431     }
432 
433     static void checkHaltCond(ref Ctx ctx, CheckStopCondMsg _) @safe nothrow {
434         if (!ctx.state.get.isRunning)
435             return;
436         try {
437             delayedSend(ctx.self, 5.dur!"seconds".delay, CheckStopCondMsg.init).collectException;
438 
439             if (ctx.state.get.stopCheck.isHalt != TestStopCheck.HaltReason.none) {
440                 send(ctx.self, RestoreMsg.init);
441                 logger.info(ctx.state.get.stopCheck.overloadToString).collectException;
442             }
443         } catch (Exception e) {
444         }
445     }
446 
447     import std.functional : toDelegate;
448 
449     self.name = "schemaDriver";
450     self.exceptionHandler = toDelegate(&logExceptionHandler);
451     try {
452         send(self, Init.init, dbPath, buildCmd, buildCmdTimeout);
453     } catch (Exception e) {
454         logger.error(e.msg).collectException;
455         self.shutdown;
456     }
457 
458     return impl(self, &init_, st, &isDone, st, &updateWlist, st,
459             &doneStatus, st, &save, st, &mark, st, &injectAndCompile, st,
460             &restore, st, &startTest, st, &test, st, &checkHaltCond, st);
461 }
462 
463 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit).
464  *
465  * There is a possibility that an injection ID result in a collision because
466  * they are only 32 bit. If that happens the mutant is discarded as unfeasable
467  * to use for schemata.
468  *
469  * TODO: if this is changed to being order dependent then it can handle all
470  * mutants. But I can't see how that can be done easily both because of how the
471  * schemas are generated and how the database is setup.
472  */
473 struct InjectIdBuilder {
474     private {
475         alias InjectId = InjectIdResult.InjectId;
476 
477         InjectId[uint] result;
478         Set!uint collisions;
479     }
480 
481     void put(MutationStatusId id, Checksum cs) @safe pure nothrow {
482         import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId;
483 
484         const injectId = checksumToId(cs);
485         debug logger.tracef("%s %s %s", id, cs, injectId).collectException;
486 
487         if (injectId in collisions) {
488         } else if (injectId in result) {
489             collisions.add(injectId);
490             result.remove(injectId);
491         } else {
492             result[injectId] = InjectId(id, injectId);
493         }
494     }
495 
496     InjectIdResult finalize() @safe nothrow {
497         import std.array : array;
498         import std.random : randomCover;
499 
500         return InjectIdResult(result.byValue.array.randomCover.array);
501     }
502 }
503 
504 struct InjectIdResult {
505     struct InjectId {
506         MutationStatusId statusId;
507         uint injectId;
508     }
509 
510     InjectId[] ids;
511 
512     InjectId front() @safe pure nothrow {
513         assert(!empty, "Can't get front of an empty range");
514         return ids[0];
515     }
516 
517     void popFront() @safe pure nothrow {
518         assert(!empty, "Can't pop front of an empty range");
519         ids = ids[1 .. $];
520     }
521 
522     bool empty() @safe pure nothrow const @nogc {
523         return ids.empty;
524     }
525 }
526 
527 /// Extract the mutants that are part of the schema.
528 InjectIdResult mutantsFromSchema(ref Database db, const SchemataId id, const Mutation.Kind[] kinds) {
529     InjectIdBuilder builder;
530     foreach (mutant; spinSql!(() => db.schemaApi.getSchemataMutants(id, kinds))) {
531         auto cs = spinSql!(() => db.mutantApi.getChecksum(mutant));
532         if (!cs.isNull)
533             builder.put(mutant, cs.get);
534     }
535     debug logger.trace(builder);
536 
537     return builder.finalize;
538 }
539 
540 @("shall detect a collision and make sure it is never part of the result")
541 unittest {
542     InjectIdBuilder builder;
543     builder.put(MutationStatusId(1), Checksum(1, 2));
544     builder.put(MutationStatusId(2), Checksum(3, 4));
545     builder.put(MutationStatusId(3), Checksum(1, 2));
546     auto r = builder.finalize;
547 
548     assert(r.front.statusId == MutationStatusId(2));
549     r.popFront;
550     assert(r.empty);
551 }
552 
553 Edit[] makeRootImpl(ulong end) {
554     import dextool.plugin.mutate.backend.resource : schemataImpl;
555 
556     return [
557         makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl)
558     ];
559 }
560 
561 Edit[] makeHdr() {
562     import dextool.plugin.mutate.backend.resource : schemataHeader;
563 
564     return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)];
565 }
566 
567 /** Injects the schema and runtime.
568  *
569  * Uses exceptions to signal failure.
570  */
571 struct CodeInject {
572     FilesysIO fio;
573 
574     SchemataId schemataId;
575 
576     Set!AbsolutePath roots;
577 
578     bool logSchema;
579 
580     this(FilesysIO fio, ConfigSchema conf, SchemataId id) {
581         this.fio = fio;
582         this.schemataId = id;
583         this.logSchema = conf.log;
584 
585         foreach (a; conf.userRuntimeCtrl) {
586             auto p = fio.toAbsoluteRoot(a.file);
587             roots.add(p);
588         }
589     }
590 
591     /// Throws an error on failure.
592     /// Returns: modified files.
593     AbsolutePath[] inject(ref Database db) {
594         auto schemata = spinSql!(() => db.schemaApi.getSchemata(schemataId)).get;
595         auto modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file))
596             .toSet.toRange.array;
597 
598         void initRoots(ref Database db) {
599             if (roots.empty) {
600                 auto allRoots = () {
601                     AbsolutePath[] tmp;
602                     try {
603                         tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get)
604                             .map!(a => fio.toAbsoluteRoot(a))
605                             .array;
606                         if (tmp.empty) {
607                             // no root found. Inject the runtime in all files and "hope for
608                             // the best". it will be less efficient but the weak symbol
609                             // should still mean that it link correctly.
610                             tmp = modifiedFiles;
611                         }
612                     } catch (Exception e) {
613                         logger.error(e.msg).collectException;
614                     }
615                     return tmp;
616                 }();
617 
618                 foreach (r; allRoots) {
619                     roots.add(r);
620                 }
621             }
622 
623             auto mods = modifiedFiles.toSet;
624             foreach (r; roots.toRange) {
625                 if (r !in mods)
626                     modifiedFiles ~= r;
627             }
628 
629             if (roots.empty)
630                 throw new Exception("No root file found to inject the schemata runtime in");
631         }
632 
633         void injectCode() {
634             import std.path : extension, stripExtension;
635             import dextool.plugin.mutate.backend.database.type : SchemataFragment;
636 
637             Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) {
638                 auto edits = appender!(Edit[])();
639                 edits.put(extra);
640                 foreach (a; fragments) {
641                     edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text);
642                 }
643                 auto m = merge(original, edits.data);
644                 return change(new Blob(original.uri, original.content), m.edits);
645             }
646 
647             SchemataFragment[] fragments(Path p) {
648                 return schemata.fragments.filter!(a => a.file == p).array;
649             }
650 
651             foreach (fname; modifiedFiles) {
652                 auto f = fio.makeInput(fname);
653                 auto extra = () {
654                     if (fname in roots) {
655                         logger.trace("Injecting schemata runtime in ", fname);
656                         return makeRootImpl(f.content.length);
657                     }
658                     return makeHdr;
659                 }();
660 
661                 logger.info("Injecting schema in ", fname);
662 
663                 // writing the schemata.
664                 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra);
665                 fio.makeOutput(fname).write(s);
666 
667                 if (logSchema) {
668                     const ext = fname.toString.extension;
669                     fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension,
670                             schemataId.get, ext).Path)).write(s);
671                 }
672             }
673         }
674 
675         initRoots(db);
676         injectCode;
677 
678         return modifiedFiles;
679     }
680 
681     void compile(ShellCommand buildCmd, Duration buildCmdTimeout) {
682         import dextool.plugin.mutate.backend.test_mutant.common : compile;
683 
684         logger.infof("Compile schema %s", schemataId.get).collectException;
685 
686         compile(buildCmd, buildCmdTimeout, PrintCompileOnFailure(true)).match!((Mutation.Status a) {
687             throw new Exception("Skipping schema because it failed to compile".color(Color.yellow)
688                 .toString);
689         }, (bool success) {
690             if (!success) {
691                 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow)
692                     .toString);
693             }
694         });
695 
696         logger.info("Ok".color(Color.green)).collectException;
697     }
698 }
699 
700 // Check that the test suite successfully execute "passed".
701 // Returns: true on success.
702 Tuple!(bool, "isOk", Duration, "runtime") sanityCheck(ref TestRunner runner) {
703     auto sw = StopWatch(AutoStart.yes);
704     auto res = runner.run;
705     return typeof(return)(res.status == TestResult.Status.passed, sw.peek);
706 }
707 
708 /// Round robin scheduling of mutants for testing from the worker pool.
709 struct ScheduleTest {
710     TestMutantActor.Address[] testers;
711     Vector!size_t free;
712 
713     this(TestMutantActor.Address[] testers) {
714         this.testers = testers;
715         foreach (size_t i; 0 .. testers.length)
716             free.put(i);
717     }
718 
719     bool empty() @safe pure nothrow const @nogc {
720         return free.empty;
721     }
722 
723     size_t pop()
724     in (free.length <= testers.length) {
725         scope (exit)
726             free.popFront();
727         return free.front;
728     }
729 
730     void put(size_t x)
731     in (x < testers.length)
732     out (; free.length <= testers.length)do {
733         free.put(x);
734     }
735 
736     TestMutantActor.Address get(size_t x)
737     in (free.length <= testers.length)
738     in (x < testers.length) {
739         return testers[x];
740     }
741 }
742 
743 struct SchemaTestResult {
744     MutationTestResult result;
745     Duration testTime;
746     TestCase[] unstable;
747 }
748 
749 alias TestMutantActor = typedActor!(SchemaTestResult function(InjectIdResult.InjectId id));
750 
751 auto spawnTestMutant(TestMutantActor.Impl self, TestRunner runner, TestCaseAnalyzer analyzer) {
752     static struct State {
753         TestRunner runner;
754         TestCaseAnalyzer analyzer;
755     }
756 
757     auto st = tuple!("self", "state")(self, refCounted(State(runner, analyzer)));
758     alias Ctx = typeof(st);
759 
760     static SchemaTestResult run(ref Ctx ctx, InjectIdResult.InjectId id) @safe nothrow {
761         import std.datetime.stopwatch : StopWatch, AutoStart;
762         import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey;
763 
764         SchemaTestResult analyzeForTestCase(SchemaTestResult rval,
765                 ref DrainElement[][ShellCommand] output) @safe nothrow {
766             foreach (testCmd; output.byKeyValue) {
767                 try {
768                     auto analyze = ctx.state.get.analyzer.analyze(testCmd.key, testCmd.value);
769 
770                     analyze.match!((TestCaseAnalyzer.Success a) {
771                         rval.result.testCases ~= a.failed ~ a.testCmd;
772                     }, (TestCaseAnalyzer.Unstable a) {
773                         rval.unstable ~= a.unstable;
774                         // must re-test the mutant
775                         rval.result.status = Mutation.Status.unknown;
776                     }, (TestCaseAnalyzer.Failed a) {
777                         logger.tracef("The parsers that analyze the output from %s failed",
778                             testCmd.key);
779                     });
780                 } catch (Exception e) {
781                     logger.warning(e.msg).collectException;
782                 }
783             }
784             return rval;
785         }
786 
787         auto sw = StopWatch(AutoStart.yes);
788 
789         SchemaTestResult rval;
790 
791         rval.result.id = id.statusId;
792 
793         auto env = ctx.state.get.runner.getDefaultEnv;
794         env[schemataMutantEnvKey] = id.injectId.to!string;
795 
796         auto res = runTester(ctx.state.get.runner, env);
797         rval.result.status = res.status;
798         rval.result.exitStatus = res.exitStatus;
799 
800         if (!ctx.state.get.analyzer.empty)
801             rval = analyzeForTestCase(rval, res.output);
802 
803         rval.testTime = sw.peek;
804         return rval;
805     }
806 
807     self.name = "testMutant";
808     return impl(self, &run, st);
809 }