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, Nullable;
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, FileId;
37 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
38 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ, SchemaSizeQ, SchemaStatus;
39 import dextool.plugin.mutate.backend.test_mutant.common;
40 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor;
41 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult;
42 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig;
43 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum;
44 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand,
45     UserRuntime, SchemaRuntime;
46 import dextool.plugin.mutate.config : ConfigSchema;
47 
48 @safe:
49 
50 private {
51     struct Init {
52     }
53 
54     struct GenSchema {
55     }
56 
57     struct RunSchema {
58     }
59 
60     struct UpdateWorkList {
61     }
62 
63     struct MarkMsg {
64     }
65 
66     struct InjectAndCompile {
67     }
68 
69     struct ScheduleTestMsg {
70     }
71 
72     struct RestoreMsg {
73     }
74 
75     struct StartTestMsg {
76     }
77 
78     struct CheckStopCondMsg {
79     }
80 
81     struct Stop {
82     }
83 }
84 
85 struct IsDone {
86 }
87 
88 struct GetDoneStatus {
89 }
90 
91 struct FinalResult {
92     enum Status {
93         fatalError,
94         invalidSchema,
95         ok
96     }
97 
98     Status status;
99     int alive;
100 }
101 
102 struct ConfTesters {
103 }
104 
105 // dfmt off
106 alias SchemaActor = typedActor!(
107     void function(Init, AbsolutePath database, ShellCommand, Duration),
108     /// Generate a schema, if possible
109     void function(GenSchema),
110     void function(RunSchema, SchemataBuilder.ET, InjectIdResult),
111     /// Quary the schema actor to see if it is done
112     bool function(IsDone),
113     /// Update the list of mutants that are still in the worklist.
114     void function(UpdateWorkList, bool),
115     FinalResult function(GetDoneStatus),
116     /// Save the result of running the schema to the DB.
117     void function(SchemaTestResult),
118     void function(MarkMsg, FinalResult.Status),
119     /// Inject the schema in the source code and compile it.
120     void function(InjectAndCompile),
121     /// Restore the source code.
122     void function(RestoreMsg),
123     /// Start running the schema.
124     void function(StartTestMsg),
125     void function(ScheduleTestMsg),
126     void function(CheckStopCondMsg),
127     void function(ConfTesters),
128     // Queue up a msg that set isRunning to false. Convenient to ensure that a
129     // RestoreMsg has been processed before setting to false.
130     void function(Stop),
131     );
132 // dfmt on
133 
134 auto spawnSchema(SchemaActor.Impl self, FilesysIO fio, ref TestRunner runner,
135         AbsolutePath dbPath, TestCaseAnalyzer testCaseAnalyzer,
136         ConfigSchema conf, TestStopCheck stopCheck, ShellCommand buildCmd, Duration buildCmdTimeout,
137         DbSaveActor.Address dbSave, StatActor.Address stat, TimeoutConfig timeoutConf) @trusted {
138 
139     static struct State {
140         TestStopCheck stopCheck;
141         DbSaveActor.Address dbSave;
142         StatActor.Address stat;
143         TimeoutConfig timeoutConf;
144         FilesysIO fio;
145         TestRunner runner;
146         TestCaseAnalyzer analyzer;
147         ConfigSchema conf;
148 
149         Database db;
150 
151         GenSchemaActor.Address genSchema;
152         SchemaSizeQUpdateActor.Address sizeQUpdater;
153 
154         ShellCommand buildCmd;
155         Duration buildCmdTimeout;
156 
157         SchemataBuilder.ET activeSchema;
158         enum ActiveSchemaCheck {
159             noMutantTested,
160             testing,
161             triggerRestoreOnce,
162         }
163         // used to detect a corner case which is that no mutant in the schema is in the whitelist.
164         ActiveSchemaCheck activeSchemaCheck;
165         Set!Checksum usedScheman;
166 
167         AbsolutePath[] modifiedFiles;
168 
169         InjectIdResult injectIds;
170 
171         ScheduleTest scheduler;
172 
173         Set!MutationStatusId whiteList;
174 
175         Duration compileTime;
176 
177         int alive;
178 
179         bool hasFatalError;
180 
181         bool isRunning;
182     }
183 
184     auto st = tuple!("self", "state")(self, refCounted(State(stopCheck, dbSave,
185             stat, timeoutConf, fio.dup, runner.dup, testCaseAnalyzer, conf)));
186     alias Ctx = typeof(st);
187 
188     static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath,
189             ShellCommand buildCmd, Duration buildCmdTimeout) nothrow {
190         import dextool.plugin.mutate.backend.database : dbOpenTimeout;
191 
192         try {
193             ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout);
194             ctx.state.get.buildCmd = buildCmd;
195             ctx.state.get.buildCmdTimeout = buildCmdTimeout;
196 
197             ctx.state.get.timeoutConf.timeoutScaleFactor = ctx.state.get.conf.timeoutScaleFactor;
198             logger.tracef("Timeout Scale Factor: %s", ctx.state.get.timeoutConf.timeoutScaleFactor);
199 
200             ctx.state.get.scheduler = () {
201                 TestMutantActor.Address[] testers;
202                 foreach (_0; 0 .. ctx.state.get.conf.parallelMutants) {
203                     auto a = ctx.self.homeSystem.spawn(&spawnTestMutant,
204                             ctx.state.get.runner.dup, ctx.state.get.analyzer);
205                     a.linkTo(ctx.self.address);
206                     testers ~= a;
207                 }
208                 return ScheduleTest(testers);
209             }();
210 
211             ctx.state.get.sizeQUpdater = ctx.self.homeSystem.spawn(&spawnSchemaSizeQ,
212                     getSchemaSizeQ(ctx.state.get.db, ctx.state.get.conf.mutantsPerSchema.get,
213                         ctx.state.get.conf.minMutantsPerSchema.get), ctx.state.get.dbSave);
214             linkTo(ctx.self, ctx.state.get.sizeQUpdater);
215 
216             ctx.state.get.genSchema = ctx.self.homeSystem.spawn(&spawnGenSchema,
217                     dbPath, ctx.state.get.conf, ctx.state.get.sizeQUpdater);
218             linkTo(ctx.self, ctx.state.get.genSchema);
219 
220             send(ctx.self, UpdateWorkList.init, true);
221             send(ctx.self, CheckStopCondMsg.init);
222             send(ctx.self, GenSchema.init);
223             ctx.state.get.isRunning = true;
224         } catch (Exception e) {
225             ctx.state.get.hasFatalError = true;
226             logger.error(e.msg).collectException;
227         }
228     }
229 
230     static void generateSchema(ref Ctx ctx, GenSchema _) @trusted nothrow {
231         try {
232             ctx.state.get.activeSchema = typeof(ctx.state.get.activeSchema).init;
233             ctx.state.get.injectIds = typeof(ctx.state.get.injectIds).init;
234 
235             ctx.self.request(ctx.state.get.genSchema, infTimeout)
236                 .send(GenSchema.init).capture(ctx).then((ref Ctx ctx, GenSchemaResult result) nothrow{
237                     if (result.noMoreScheman) {
238                         ctx.state.get.isRunning = false;
239                     } else {
240                         try {
241                             send(ctx.self, RunSchema.init, result.schema, result.injectIds);
242                         } catch (Exception e) {
243                             ctx.state.get.isRunning = false;
244                             logger.error(e.msg).collectException;
245                         }
246                     }
247                 });
248         } catch (Exception e) {
249             logger.warning(e.msg).collectException;
250         }
251     }
252 
253     static void runSchema(ref Ctx ctx, RunSchema _, SchemataBuilder.ET schema,
254             InjectIdResult injectIds) @safe nothrow {
255         try {
256             if (!ctx.state.get.isRunning) {
257                 return;
258             }
259             if (schema.checksum.value in ctx.state.get.usedScheman) {
260                 // discard, already used
261                 send(ctx.self, GenSchema.init);
262                 return;
263             }
264 
265             ctx.state.get.usedScheman.add(schema.checksum.value);
266 
267             logger.trace("schema generated ", schema.checksum);
268             logger.trace(schema.fragments.map!"a.file");
269             logger.trace(schema.mutants);
270             logger.trace(injectIds);
271 
272             if (injectIds.empty || injectIds.length < ctx.state.get.conf.minMutantsPerSchema.get) {
273                 send(ctx.self, GenSchema.init);
274             } else {
275                 ctx.state.get.activeSchema = schema;
276                 ctx.state.get.injectIds = injectIds;
277                 send(ctx.self, InjectAndCompile.init);
278             }
279         } catch (Exception e) {
280             logger.error(e.msg).collectException;
281         }
282     }
283 
284     static void confTesters(ref Ctx ctx, ConfTesters _) {
285         foreach (a; ctx.state.get.scheduler.testers) {
286             send(a, ctx.state.get.timeoutConf);
287         }
288     }
289 
290     static bool isDone(ref Ctx ctx, IsDone _) {
291         return !ctx.state.get.isRunning;
292     }
293 
294     static void mark(ref Ctx ctx, MarkMsg _, FinalResult.Status status) @safe nothrow {
295         import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ;
296 
297         static void updateSchemaQ(ref SchemaQ sq, ref SchemataBuilder.ET schema,
298                 const SchemaStatus status) @trusted nothrow {
299             import my.hash : Checksum64;
300             import my.set;
301 
302             auto paths = schema.fragments.map!"a.file".toSet.toRange.array;
303             Set!Checksum64 latestFiles;
304 
305             foreach (path; paths) {
306                 scope getPath = (SchemaStatus s) {
307                     return (s == status) ? schema.mutants.map!"a.mut.kind".toSet.toRange.array
308                         : null;
309                 };
310                 try {
311                     sq.update(path, getPath);
312                 } catch (Exception e) {
313                     logger.warning(e.msg).collectException;
314                 }
315                 latestFiles.add(sq.pathCache[path]);
316                 debug logger.tracef("updating %s %s", path, sq.pathCache[path]);
317             }
318 
319             // TODO: remove prob for non-existing files
320             sq.scatterTick;
321         }
322 
323         SchemaStatus schemaStatus = () {
324             final switch (status) with (FinalResult.Status) {
325             case fatalError:
326                 goto case;
327             case invalidSchema:
328                 return SchemaStatus.broken;
329             case ok:
330                 // TODO: remove SchemaStatus.allKilled
331                 return SchemaStatus.ok;
332             }
333         }();
334 
335         try {
336             auto schemaQ = spinSql!(() => SchemaQ(ctx.state.get.db.schemaApi.getMutantProbability));
337             updateSchemaQ(schemaQ, ctx.state.get.activeSchema, schemaStatus);
338             send(ctx.state.get.dbSave, schemaQ);
339 
340             send(ctx.state.get.sizeQUpdater, SchemaGenStatusMsg.init,
341                     schemaStatus, cast(long) ctx.state.get.activeSchema.mutants.length);
342         } catch (Exception e) {
343             logger.trace(e.msg).collectException;
344         }
345     }
346 
347     static void updateWlist(ref Ctx ctx, UpdateWorkList _, bool repeat) @safe nothrow {
348         if (!ctx.state.get.isRunning)
349             return;
350 
351         try {
352             if (repeat)
353                 delayedSend(ctx.self, 1.dur!"minutes".delay, UpdateWorkList.init, true);
354 
355             ctx.state.get.whiteList = spinSql!(() => ctx.state.get.db.worklistApi.getAll)
356                 .map!"a.id".toSet;
357             send(ctx.state.get.genSchema, ctx.state.get.whiteList.toArray);
358 
359             logger.trace("update schema worklist: ", ctx.state.get.whiteList.length);
360             debug logger.trace("update schema worklist: ", ctx.state.get.whiteList.toRange);
361         } catch (Exception e) {
362             logger.trace(e.msg).collectException;
363         }
364     }
365 
366     static FinalResult doneStatus(ref Ctx ctx, GetDoneStatus _) @safe nothrow {
367         FinalResult.Status status = () {
368             if (ctx.state.get.hasFatalError)
369                 return FinalResult.Status.fatalError;
370             return FinalResult.Status.ok;
371         }();
372 
373         return FinalResult(status, ctx.state.get.alive);
374     }
375 
376     static void save(ref Ctx ctx, SchemaTestResult data) {
377         import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft,
378             UnknownMutantTested;
379 
380         void update(MutationTestResult a) {
381             final switch (a.status) with (Mutation.Status) {
382             case skipped:
383                 goto case;
384             case unknown:
385                 goto case;
386             case equivalent:
387                 goto case;
388             case noCoverage:
389                 goto case;
390             case alive:
391                 ctx.state.get.alive++;
392                 ctx.state.get.stopCheck.incrAliveMutants(1);
393                 return;
394             case killed:
395                 goto case;
396             case timeout:
397                 goto case;
398             case memOverload:
399                 goto case;
400             case killedByCompiler:
401                 break;
402             }
403         }
404 
405         debug logger.trace(data);
406 
407         if (!data.unstable.empty) {
408             logger.warningf("Unstable test cases found: [%-(%s, %)]", data.unstable);
409             logger.info(
410                     "As configured the result is ignored which will force the mutant to be re-tested");
411             return;
412         }
413 
414         update(data.result);
415 
416         auto result = data.result;
417         result.profile = MutantTimeProfile(ctx.state.get.compileTime, data.testTime);
418         ctx.state.get.compileTime = Duration.zero;
419 
420         logger.infof("%s:%s (%s)", data.result.status,
421                 data.result.exitStatus.get, result.profile).collectException;
422         logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`,
423                 data.result.testCases.sort.map!"a.name").collectException;
424 
425         send(ctx.state.get.dbSave, result, ctx.state.get.timeoutConf.iter);
426         send(ctx.state.get.stat, UnknownMutantTested.init, 1L);
427 
428         // an error handler is required because the stat actor can be held up
429         // for more than a minute.
430         ctx.self.request(ctx.state.get.stat, delay(5.dur!"seconds"))
431             .send(GetMutantsLeft.init).then((long x) {
432                 logger.infof("%s mutants left to test.", x);
433             }, (ref Actor self, ErrorMsg) {});
434 
435         if (ctx.state.get.injectIds.empty && ctx.state.get.scheduler.full) {
436             logger.trace("done saving result for schema ",
437                     ctx.state.get.activeSchema.checksum).collectException;
438             send(ctx.self, MarkMsg.init, FinalResult.Status.ok);
439             send(ctx.self, UpdateWorkList.init, false);
440             send(ctx.self, RestoreMsg.init).collectException;
441         }
442     }
443 
444     static void injectAndCompile(ref Ctx ctx, InjectAndCompile _) @safe nothrow {
445         try {
446             auto sw = StopWatch(AutoStart.yes);
447             scope (exit)
448                 ctx.state.get.compileTime = sw.peek;
449 
450             logger.infof("Using schema with %s mutants", ctx.state.get.injectIds.length);
451 
452             auto codeInject = CodeInject(ctx.state.get.fio, ctx.state.get.conf);
453             ctx.state.get.modifiedFiles = codeInject.inject(ctx.state.get.db,
454                     ctx.state.get.activeSchema);
455             codeInject.compile(ctx.state.get.buildCmd, ctx.state.get.buildCmdTimeout);
456 
457             if (ctx.state.get.conf.sanityCheckSchemata) {
458                 logger.info("Sanity check of the generated schemata");
459                 const sanity = sanityCheck(ctx.state.get.runner);
460                 if (sanity.isOk) {
461                     if (ctx.state.get.timeoutConf.base < sanity.runtime) {
462                         ctx.state.get.timeoutConf.set(sanity.runtime);
463                         send(ctx.self, ConfTesters.init);
464                     }
465 
466                     logger.info("Ok".color(Color.green), ". Using test suite timeout ",
467                             ctx.state.get.timeoutConf.value).collectException;
468                     send(ctx.self, StartTestMsg.init);
469                 } else {
470                     logger.info("Skipping the schemata because the test suite failed".color(Color.yellow)
471                             .toString);
472                     send(ctx.self, MarkMsg.init, FinalResult.Status.invalidSchema);
473                     send(ctx.self, RestoreMsg.init).collectException;
474                 }
475             } else {
476                 send(ctx.self, StartTestMsg.init);
477             }
478         } catch (Exception e) {
479             send(ctx.self, MarkMsg.init, FinalResult.Status.invalidSchema).collectException;
480             send(ctx.self, RestoreMsg.init).collectException;
481             logger.warning(e.msg).collectException;
482         }
483     }
484 
485     static void restore(ref Ctx ctx, RestoreMsg _) @safe nothrow {
486         import dextool.plugin.mutate.backend.test_mutant.common : restoreFiles;
487 
488         try {
489             logger.trace("restore ", ctx.state.get.modifiedFiles);
490             restoreFiles(ctx.state.get.modifiedFiles, ctx.state.get.fio);
491             ctx.state.get.modifiedFiles = null;
492             send(ctx.self, GenSchema.init);
493         } catch (Exception e) {
494             ctx.state.get.hasFatalError = true;
495             ctx.state.get.isRunning = false;
496             logger.error(e.msg).collectException;
497         }
498     }
499 
500     static void startTest(ref Ctx ctx, StartTestMsg _) @safe nothrow {
501         ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.noMutantTested;
502 
503         try {
504             foreach (_0; 0 .. ctx.state.get.scheduler.testers.length)
505                 send(ctx.self, ScheduleTestMsg.init);
506             logger.tracef("sent %s ScheduleTestMsg", ctx.state.get.scheduler.testers.length);
507         } catch (Exception e) {
508             ctx.state.get.hasFatalError = true;
509             ctx.state.get.isRunning = false;
510             logger.error(e.msg).collectException;
511         }
512     }
513 
514     static void test(ref Ctx ctx, ScheduleTestMsg _) nothrow {
515         // TODO: move this printer to another thread because it perform
516         // significant DB lookup and can potentially slow down the testing.
517         void print(MutationStatusId statusId) {
518             import dextool.plugin.mutate.backend.generate_mutant : makeMutationText;
519 
520             auto entry_ = spinSql!(() => ctx.state.get.db.mutantApi.getMutation(statusId));
521             if (entry_.isNull)
522                 return;
523             auto entry = entry_.get;
524 
525             try {
526                 const file = ctx.state.get.fio.toAbsoluteRoot(entry.file);
527                 auto txt = makeMutationText(ctx.state.get.fio.makeInput(file),
528                         entry.mp.offset, entry.mp.mutations[0].kind, entry.lang);
529                 debug logger.trace(entry);
530                 logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original,
531                         txt.mutation, file, entry.sloc.line, entry.sloc.column);
532             } catch (Exception e) {
533                 logger.info(e.msg).collectException;
534             }
535         }
536 
537         if (!ctx.state.get.isRunning)
538             return;
539 
540         try {
541             if (ctx.state.get.injectIds.empty) {
542                 logger.trace("no mutants left to test ", ctx.state.get.scheduler.free.length);
543                 if (ctx.state.get.activeSchemaCheck == State.ActiveSchemaCheck.noMutantTested
544                         && ctx.state.get.scheduler.full) {
545                     // no mutant has been tested in the schema thus the restore in save is never triggered.
546                     send(ctx.self, RestoreMsg.init);
547                     ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.triggerRestoreOnce;
548                 }
549                 return;
550             }
551 
552             if (ctx.state.get.scheduler.empty) {
553                 logger.trace("no free worker");
554                 delayedSend(ctx.self, 1.dur!"seconds".delay, ScheduleTestMsg.init);
555                 return;
556             }
557 
558             if (ctx.state.get.stopCheck.isOverloaded) {
559                 logger.info(ctx.state.get.stopCheck.overloadToString).collectException;
560                 delayedSend(ctx.self, 30.dur!"seconds".delay, ScheduleTestMsg.init);
561                 return;
562             }
563 
564             auto m = ctx.state.get.injectIds.front;
565             ctx.state.get.injectIds.popFront;
566 
567             if (m.statusId in ctx.state.get.whiteList) {
568                 ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.testing;
569                 auto testerId = ctx.state.get.scheduler.pop;
570                 auto tester = ctx.state.get.scheduler.get(testerId);
571                 print(m.statusId);
572                 ctx.self.request(tester, infTimeout).send(m).capture(ctx,
573                         testerId).then((ref Capture!(Ctx, size_t) ctx, SchemaTestResult x) {
574                     ctx[0].state.get.scheduler.put(ctx[1]);
575                     send(ctx[0].self, x);
576                     send(ctx[0].self, ScheduleTestMsg.init);
577                 });
578             } else {
579                 debug logger.tracef("%s not in whitelist. Skipping", m);
580                 send(ctx.self, ScheduleTestMsg.init);
581             }
582         } catch (Exception e) {
583             ctx.state.get.hasFatalError = true;
584             ctx.state.get.isRunning = false;
585             logger.error(e.msg).collectException;
586         }
587     }
588 
589     static void checkHaltCond(ref Ctx ctx, CheckStopCondMsg _) @safe nothrow {
590         if (!ctx.state.get.isRunning)
591             return;
592 
593         try {
594             delayedSend(ctx.self, 5.dur!"seconds".delay, CheckStopCondMsg.init).collectException;
595 
596             const halt = ctx.state.get.stopCheck.isHalt;
597             if (halt == TestStopCheck.HaltReason.overloaded)
598                 ctx.state.get.stopCheck.startBgShutdown;
599 
600             if (halt != TestStopCheck.HaltReason.none) {
601                 send(ctx.self, RestoreMsg.init);
602                 send(ctx.self, Stop.init);
603                 logger.info(ctx.state.get.stopCheck.overloadToString).collectException;
604             }
605         } catch (Exception e) {
606             ctx.state.get.isRunning = false;
607             logger.error(e.msg).collectException;
608         }
609     }
610 
611     static void stop(ref Ctx ctx, Stop _) @safe nothrow {
612         ctx.state.get.isRunning = false;
613     }
614 
615     import std.functional : toDelegate;
616 
617     self.name = "schemaDriver";
618     self.exceptionHandler = toDelegate(&logExceptionHandler);
619     try {
620         send(self, Init.init, dbPath, buildCmd, buildCmdTimeout);
621     } catch (Exception e) {
622         logger.error(e.msg).collectException;
623         self.shutdown;
624     }
625 
626     return impl(self, &init_, st, &isDone, st, &updateWlist, st,
627             &doneStatus, st, &save, st, &mark, st, &injectAndCompile, st,
628             &restore, st, &startTest, st, &test, st, &checkHaltCond, st,
629             &confTesters, st, &generateSchema, st, &runSchema, st, &stop, st);
630 }
631 
632 private SchemaSizeQ getSchemaSizeQ(ref Database db, const long userInit, const long minSize) @trusted nothrow {
633     // 1.1 is a magic number. it feels good. the purpose is to be a little
634     // leniant with the size to demonstrate to the user that it is OK to
635     // raise the max size. At the same time it shouldn't be too much
636     // because the user may have configured it to a low value for a reason.
637     auto sq = SchemaSizeQ.make(minSize, cast(long)(userInit * 1.1));
638     sq.updateSize(spinSql!(() => db.schemaApi.getSchemaSize(userInit)));
639     sq.testMutantsSize = spinSql!(() => db.worklistApi.getCount);
640     return sq;
641 }
642 
643 private {
644     struct GenSchemaResult {
645         bool noMoreScheman;
646         SchemataBuilder.ET schema;
647         InjectIdResult injectIds;
648     }
649 }
650 
651 // dfmt off
652 alias GenSchemaActor = typedActor!(
653     void function(Init, AbsolutePath database, ConfigSchema conf),
654     GenSchemaResult function(GenSchema),
655     void function(MutationStatusId[] whiteList),
656     );
657 // dfmt on
658 
659 private auto spawnGenSchema(GenSchemaActor.Impl self, AbsolutePath dbPath,
660         ConfigSchema conf, SchemaSizeQUpdateActor.Address sizeQUpdater) @trusted {
661     static struct State {
662         ConfigSchema conf;
663         SchemaSizeQUpdateActor.Address sizeQUpdater;
664         Database db;
665         SchemaBuildState schemaBuild;
666         Set!MutationStatusId whiteList;
667     }
668 
669     auto st = tuple!("self", "state")(self, refCounted(State(conf, sizeQUpdater)));
670     alias Ctx = typeof(st);
671 
672     static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath, ConfigSchema conf) nothrow {
673         import dextool.plugin.mutate.backend.database : dbOpenTimeout;
674 
675         try {
676             ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout);
677 
678             ctx.state.get.schemaBuild.minMutantsPerSchema = ctx.state.get.conf.minMutantsPerSchema;
679             ctx.state.get.schemaBuild.mutantsPerSchema.get = ctx.state.get.conf
680                 .mutantsPerSchema.get;
681             ctx.state.get.schemaBuild.initFiles(
682                     spinSql!(() => ctx.state.get.db.fileApi.getFileIds));
683         } catch (Exception e) {
684             logger.error(e.msg).collectException;
685             // TODO: should terminate?
686         }
687     }
688 
689     static void updateWhiteList(ref Ctx ctx, MutationStatusId[] whiteList) @trusted nothrow {
690         try {
691             ctx.state.get.whiteList = whiteList.toSet;
692             send(ctx.state.get.sizeQUpdater, MutantsToTestMsg.init,
693                     cast(long) ctx.state.get.whiteList.length);
694 
695             ctx.state.get.schemaBuild.builder.schemaQ = spinSql!(
696                     () => SchemaQ(ctx.state.get.db.schemaApi.getMutantProbability));
697 
698             ctx.self.request(ctx.state.get.sizeQUpdater, infTimeout)
699                 .send(GetSchemaSizeMsg.init).capture(ctx).then((ref Ctx ctx, long sz) nothrow{
700                     ctx.state.get.schemaBuild.mutantsPerSchema.get = sz;
701                 });
702         } catch (Exception e) {
703         }
704     }
705 
706     static GenSchemaResult genSchema(ref Ctx ctx, GenSchema _) nothrow {
707         static void process(ref Ctx ctx, ref GenSchemaResult result,
708                 ref Set!MutationStatusId whiteList) @safe {
709             auto value = ctx.state.get.schemaBuild.process;
710             value.match!((Some!(SchemataBuilder.ET) a) {
711                 result.schema = a;
712                 result.injectIds = mutantsFromSchema(a, whiteList);
713             }, (None a) {});
714         }
715 
716         static void processFile(ref Ctx ctx, ref Set!MutationStatusId whiteList) @trusted nothrow {
717             if (ctx.state.get.schemaBuild.files.isDone)
718                 return;
719 
720             size_t frags;
721             while (frags == 0 && ctx.state.get.schemaBuild.files.filesLeft != 0) {
722                 logger.trace("Files left ",
723                         ctx.state.get.schemaBuild.files.filesLeft).collectException;
724                 frags = spinSql!(() {
725                     auto trans = ctx.state.get.db.transaction;
726                     return ctx.state.get.schemaBuild.updateFiles(whiteList,
727                         (FileId id) => spinSql!(() => ctx.state.get.db.schemaApi.getFragments(id)),
728                         (FileId id) => spinSql!(() => ctx.state.get.db.getFile(id)),
729                         (MutationStatusId id) => spinSql!(
730                         () => ctx.state.get.db.mutantApi.getKind(id)));
731                 });
732             }
733         }
734 
735         GenSchemaResult result;
736 
737         logger.trace("Generate schema").collectException;
738         while (ctx.state.get.schemaBuild.st != SchemaBuildState.State.done) {
739             ctx.state.get.schemaBuild.tick;
740 
741             final switch (ctx.state.get.schemaBuild.st) {
742             case SchemaBuildState.State.none:
743                 break;
744             case SchemaBuildState.State.processFiles:
745                 try {
746                     processFile(ctx, ctx.state.get.whiteList);
747                     process(ctx, result, ctx.state.get.whiteList);
748                 } catch (Exception e) {
749                     logger.trace(e.msg).collectException;
750                     return GenSchemaResult(true);
751                 }
752                 break;
753             case SchemaBuildState.State.prepareReduction:
754                 send(ctx.state.get.sizeQUpdater,
755                         FullSchemaGenDoneMsg.init).collectException;
756                 goto case;
757             case SchemaBuildState.State.prepareFinalize:
758                 try {
759                     ctx.state.get.schemaBuild.files.reset;
760                 } catch (Exception e) {
761                     logger.trace(e.msg).collectException;
762                     return GenSchemaResult(true);
763                 }
764                 break;
765             case SchemaBuildState.State.reduction:
766                 goto case;
767             case SchemaBuildState.State.finalize1:
768                 goto case;
769             case SchemaBuildState.State.finalize2:
770                 try {
771                     processFile(ctx, ctx.state.get.whiteList);
772                     process(ctx, result, ctx.state.get.whiteList);
773                 } catch (Exception e) {
774                     logger.trace(e.msg).collectException;
775                     return GenSchemaResult(true);
776                 }
777                 break;
778             case SchemaBuildState.State.done:
779                 ctx.state.get.schemaBuild.files.clear;
780                 return GenSchemaResult(true);
781             }
782 
783             if (!result.injectIds.empty)
784                 return result;
785         }
786 
787         return GenSchemaResult(true);
788     }
789 
790     self.name = "generateSchema";
791 
792     try {
793         send(self, Init.init, dbPath, conf);
794     } catch (Exception e) {
795         logger.error(e.msg).collectException;
796         self.shutdown;
797     }
798 
799     return impl(self, &init_, st, &genSchema, st, &updateWhiteList, st);
800 }
801 
802 private {
803     struct FullSchemaGenDoneMsg {
804     }
805 
806     struct MutantsToTestMsg {
807     }
808 
809     struct SchemaGenStatusMsg {
810     }
811 
812     struct GetSchemaSizeMsg {
813     }
814 
815     struct SaveSizeQMsg {
816     }
817 }
818 
819 // dfmt off
820 alias SchemaSizeQUpdateActor = typedActor!(
821     // Signal that no more full scheman are generated.
822     void function(FullSchemaGenDoneMsg),
823     // mutants to test when the scheman where generated
824     void function(MutantsToTestMsg, long number),
825     // if the generation where successfull
826     void function(SchemaGenStatusMsg, SchemaStatus, long mutantsInSchema),
827     /// The currently state of the size to use for scheman.
828     long function(GetSchemaSizeMsg),
829     );
830 // dfmt on
831 
832 private auto spawnSchemaSizeQ(SchemaSizeQUpdateActor.Impl self,
833         SchemaSizeQ sizeQ, DbSaveActor.Address dbSave) @trusted {
834     static struct State {
835         DbSaveActor.Address dbSave;
836         // state of the sizeq algorithm.
837         SchemaSizeQ sizeQ;
838         // number of scheman that has been generated.
839         long genCount;
840     }
841 
842     auto st = tuple!("self", "state")(self, refCounted(State(dbSave, sizeQ)));
843     alias Ctx = typeof(st);
844 
845     static void updateMutantsNumber(ref Ctx ctx, MutantsToTestMsg _, long number) @safe nothrow {
846         ctx.state.get.sizeQ.testMutantsSize = number;
847     }
848 
849     static void genStatus(ref Ctx ctx, SchemaGenStatusMsg,
850             SchemaStatus status, long mutantsInSchema) @safe nothrow {
851         ctx.state.get.genCount++;
852         try {
853             ctx.state.get.sizeQ.update(status, mutantsInSchema);
854             send(ctx.state.get.dbSave, ctx.state.get.sizeQ);
855             logger.trace(ctx.state.get.sizeQ);
856         } catch (Exception e) {
857             logger.info(e.msg).collectException;
858         }
859     }
860 
861     static void fullGenDone(ref Ctx ctx, FullSchemaGenDoneMsg _) @safe nothrow {
862         if (ctx.state.get.genCount == 0) {
863             try {
864                 ctx.state.get.sizeQ.noCurrentSize;
865                 send(ctx.state.get.dbSave, ctx.state.get.sizeQ);
866                 logger.trace(ctx.state.get.sizeQ);
867             } catch (Exception e) {
868                 logger.info(e.msg).collectException;
869             }
870         }
871     }
872 
873     static long getSize(ref Ctx ctx, GetSchemaSizeMsg _) @safe nothrow {
874         return ctx.state.get.sizeQ.currentSize;
875     }
876 
877     self.name = "schemaSizeQUpdater";
878 
879     return impl(self, &updateMutantsNumber, st, &getSize, st, &genStatus,
880             st, &fullGenDone, st);
881 }
882 
883 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit).
884  *
885  * There is a possibility that an injection ID result in a collision because
886  * they are only 32 bit. If that happens the mutant is discarded as unfeasable
887  * to use for schemata.
888  *
889  * TODO: if this is changed to being order dependent then it can handle all
890  * mutants. But I can't see how that can be done easily both because of how the
891  * schemas are generated and how the database is setup.
892  */
893 struct InjectIdBuilder {
894     private {
895         alias InjectId = InjectIdResult.InjectId;
896 
897         InjectId[uint] result;
898         Set!uint collisions;
899     }
900 
901     void put(MutationStatusId id, Checksum cs) @safe pure nothrow {
902         import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId;
903 
904         const injectId = checksumToId(cs);
905         debug logger.tracef("%s %s %s", id, cs, injectId).collectException;
906 
907         if (injectId in collisions) {
908         } else if (injectId in result) {
909             collisions.add(injectId);
910             result.remove(injectId);
911         } else {
912             result[injectId] = InjectId(id, injectId);
913         }
914     }
915 
916     InjectIdResult finalize() @safe nothrow {
917         import std.array : array;
918         import std.random : randomCover;
919 
920         return InjectIdResult(result.byValue.array.randomCover.array);
921     }
922 }
923 
924 struct InjectIdResult {
925     struct InjectId {
926         MutationStatusId statusId;
927         uint injectId;
928     }
929 
930     InjectId[] ids;
931 
932     InjectId front() @safe pure nothrow {
933         assert(!empty, "Can't get front of an empty range");
934         return ids[0];
935     }
936 
937     void popFront() @safe pure nothrow {
938         assert(!empty, "Can't pop front of an empty range");
939         ids = ids[1 .. $];
940     }
941 
942     bool empty() @safe pure nothrow const @nogc {
943         return ids.empty;
944     }
945 
946     size_t length() @safe pure nothrow const @nogc scope {
947         return ids.length;
948     }
949 }
950 
951 /// Extract the mutants that are part of the schema.
952 InjectIdResult mutantsFromSchema(ref SchemataBuilder.ET schema, ref Set!MutationStatusId whiteList) {
953     import dextool.plugin.mutate.backend.database.type : toMutationStatusId;
954 
955     InjectIdBuilder builder;
956     foreach (mutant; schema.mutants.filter!(a => a.id.toMutationStatusId in whiteList)) {
957         builder.put(mutant.id.toMutationStatusId, mutant.id);
958     }
959 
960     return builder.finalize;
961 }
962 
963 @("shall detect a collision and make sure it is never part of the result")
964 unittest {
965     InjectIdBuilder builder;
966     builder.put(MutationStatusId(1), Checksum(1));
967     builder.put(MutationStatusId(2), Checksum(2));
968     builder.put(MutationStatusId(3), Checksum(1));
969     auto r = builder.finalize;
970 
971     assert(r.front.statusId == MutationStatusId(2));
972     r.popFront;
973     assert(r.empty);
974 }
975 
976 Edit[] makeRootImpl(ulong end) {
977     import dextool.plugin.mutate.backend.resource : schemataImpl;
978 
979     return [
980         makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl)
981     ];
982 }
983 
984 Edit[] makeHdr() {
985     import dextool.plugin.mutate.backend.resource : schemataHeader;
986 
987     return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)];
988 }
989 
990 /** Injects the schema and runtime.
991  *
992  * Uses exceptions to signal failure.
993  */
994 struct CodeInject {
995     FilesysIO fio;
996 
997     Set!AbsolutePath roots;
998 
999     /// Unique checksum for the schema.
1000     Checksum checksum;
1001 
1002     bool logSchema;
1003 
1004     this(FilesysIO fio, ConfigSchema conf) {
1005         this.fio = fio;
1006         this.logSchema = conf.log;
1007 
1008         foreach (a; conf.userRuntimeCtrl) {
1009             auto p = fio.toAbsoluteRoot(a.file);
1010             roots.add(p);
1011         }
1012     }
1013 
1014     /// Throws an error on failure.
1015     /// Returns: modified files.
1016     AbsolutePath[] inject(ref Database db, SchemataBuilder.ET schemata) {
1017         checksum = schemata.checksum.value;
1018         auto modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file))
1019             .toSet.toRange.array;
1020 
1021         void initRoots(ref Database db) {
1022             if (roots.empty) {
1023                 auto allRoots = () {
1024                     AbsolutePath[] tmp;
1025                     try {
1026                         tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get)
1027                             .map!(a => fio.toAbsoluteRoot(a))
1028                             .array;
1029                         if (tmp.empty) {
1030                             // no root found. Inject the runtime in all files and "hope for
1031                             // the best". it will be less efficient but the weak symbol
1032                             // should still mean that it link correctly.
1033                             tmp = modifiedFiles;
1034                         }
1035                     } catch (Exception e) {
1036                         logger.error(e.msg).collectException;
1037                     }
1038                     return tmp;
1039                 }();
1040 
1041                 foreach (r; allRoots) {
1042                     roots.add(r);
1043                 }
1044             }
1045 
1046             auto mods = modifiedFiles.toSet;
1047             foreach (r; roots.toRange) {
1048                 if (r !in mods)
1049                     modifiedFiles ~= r;
1050             }
1051 
1052             if (roots.empty)
1053                 throw new Exception("No root file found to inject the schemata runtime in");
1054         }
1055 
1056         void injectCode() {
1057             import std.path : extension, stripExtension;
1058 
1059             alias SchemataFragment = SchemataBuilder.SchemataFragment;
1060 
1061             Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) {
1062                 auto edits = appender!(Edit[])();
1063                 edits.put(extra);
1064                 foreach (a; fragments) {
1065                     edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text);
1066                 }
1067                 auto m = merge(original, edits.data);
1068                 return change(new Blob(original.uri, original.content), m.edits);
1069             }
1070 
1071             SchemataFragment[] fragments(Path p) {
1072                 return schemata.fragments.filter!(a => a.file == p).array;
1073             }
1074 
1075             foreach (fname; modifiedFiles) {
1076                 auto f = fio.makeInput(fname);
1077                 auto extra = () {
1078                     if (fname in roots) {
1079                         logger.trace("Injecting schemata runtime in ", fname);
1080                         return makeRootImpl(f.content.length);
1081                     }
1082                     return makeHdr;
1083                 }();
1084 
1085                 logger.info("Injecting schema in ", fname);
1086 
1087                 // writing the schemata.
1088                 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra);
1089                 fio.makeOutput(fname).write(s);
1090 
1091                 if (logSchema) {
1092                     const ext = fname.toString.extension;
1093                     fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension,
1094                             checksum.c0, ext).Path)).write(s);
1095                 }
1096             }
1097         }
1098 
1099         initRoots(db);
1100         injectCode;
1101 
1102         return modifiedFiles;
1103     }
1104 
1105     void compile(ShellCommand buildCmd, Duration buildCmdTimeout) {
1106         import dextool.plugin.mutate.backend.test_mutant.common : compile;
1107 
1108         logger.infof("Compile schema %s", checksum.c0).collectException;
1109 
1110         compile(buildCmd, buildCmdTimeout, PrintCompileOnFailure(true)).match!((Mutation.Status a) {
1111             throw new Exception("Skipping schema because it failed to compile".color(Color.yellow)
1112                 .toString);
1113         }, (bool success) {
1114             if (!success) {
1115                 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow)
1116                     .toString);
1117             }
1118         });
1119 
1120         logger.info("Ok".color(Color.green)).collectException;
1121     }
1122 }
1123 
1124 // Check that the test suite successfully execute "passed".
1125 // Returns: true on success.
1126 Tuple!(bool, "isOk", Duration, "runtime") sanityCheck(ref TestRunner runner) {
1127     auto sw = StopWatch(AutoStart.yes);
1128     auto res = runner.run;
1129     return typeof(return)(res.status == TestResult.Status.passed, sw.peek);
1130 }
1131 
1132 /// Round robin scheduling of mutants for testing from the worker pool.
1133 struct ScheduleTest {
1134     TestMutantActor.Address[] testers;
1135     Vector!size_t free;
1136 
1137     this(TestMutantActor.Address[] testers) {
1138         this.testers = testers;
1139         foreach (size_t i; 0 .. testers.length)
1140             free.put(i);
1141     }
1142 
1143     /// Returns: if the tester is full, no worker used.
1144     bool full() @safe pure nothrow const @nogc {
1145         return testers.length == free.length;
1146     }
1147 
1148     bool empty() @safe pure nothrow const @nogc {
1149         return free.empty;
1150     }
1151 
1152     size_t pop()
1153     in (free.length <= testers.length) {
1154         scope (exit)
1155             free.popFront();
1156         return free.front;
1157     }
1158 
1159     void put(size_t x)
1160     in (x < testers.length)
1161     out (; free.length <= testers.length)
1162     do {
1163         free.put(x);
1164     }
1165 
1166     TestMutantActor.Address get(size_t x)
1167     in (free.length <= testers.length)
1168     in (x < testers.length) {
1169         return testers[x];
1170     }
1171 }
1172 
1173 struct SchemaTestResult {
1174     MutationTestResult result;
1175     Duration testTime;
1176     TestCase[] unstable;
1177 }
1178 
1179 alias TestMutantActor = typedActor!(
1180         SchemaTestResult function(InjectIdResult.InjectId id), void function(TimeoutConfig));
1181 
1182 auto spawnTestMutant(TestMutantActor.Impl self, TestRunner runner, TestCaseAnalyzer analyzer) {
1183     static struct State {
1184         TestRunner runner;
1185         TestCaseAnalyzer analyzer;
1186     }
1187 
1188     auto st = tuple!("self", "state")(self, refCounted(State(runner, analyzer)));
1189     alias Ctx = typeof(st);
1190 
1191     static SchemaTestResult run(ref Ctx ctx, InjectIdResult.InjectId id) @safe nothrow {
1192         import std.datetime.stopwatch : StopWatch, AutoStart;
1193         import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey;
1194 
1195         SchemaTestResult analyzeForTestCase(SchemaTestResult rval,
1196                 ref DrainElement[][ShellCommand] output) @safe nothrow {
1197             foreach (testCmd; output.byKeyValue) {
1198                 try {
1199                     auto analyze = ctx.state.get.analyzer.analyze(testCmd.key, testCmd.value);
1200 
1201                     analyze.match!((TestCaseAnalyzer.Success a) {
1202                         rval.result.testCases ~= a.failed ~ a.testCmd;
1203                     }, (TestCaseAnalyzer.Unstable a) {
1204                         rval.unstable ~= a.unstable;
1205                         // must re-test the mutant
1206                         rval.result.status = Mutation.Status.unknown;
1207                     }, (TestCaseAnalyzer.Failed a) {
1208                         logger.tracef("The parsers that analyze the output from %s failed",
1209                             testCmd.key);
1210                     });
1211                 } catch (Exception e) {
1212                     logger.warning(e.msg).collectException;
1213                 }
1214             }
1215             return rval;
1216         }
1217 
1218         auto sw = StopWatch(AutoStart.yes);
1219 
1220         SchemaTestResult rval;
1221 
1222         rval.result.id = id.statusId;
1223 
1224         auto env = ctx.state.get.runner.getDefaultEnv;
1225         env[schemataMutantEnvKey] = id.injectId.to!string;
1226 
1227         auto res = runTester(ctx.state.get.runner, env);
1228         rval.result.status = res.status;
1229         rval.result.exitStatus = res.exitStatus;
1230         rval.result.testCmds = res.output.byKey.array;
1231 
1232         if (!ctx.state.get.analyzer.empty)
1233             rval = analyzeForTestCase(rval, res.output);
1234 
1235         rval.testTime = sw.peek;
1236         return rval;
1237     }
1238 
1239     static void doConf(ref Ctx ctx, TimeoutConfig conf) @safe nothrow {
1240         ctx.state.get.runner.timeout = conf.value;
1241     }
1242 
1243     self.name = "testMutant";
1244     return impl(self, &run, st, &doConf, st);
1245 }
1246 
1247 // private:
1248 
1249 import std.algorithm : sum;
1250 import std.format : formattedWrite, format;
1251 
1252 import dextool.plugin.mutate.backend.database.type : SchemataFragment;
1253 import dextool.plugin.mutate.backend.type : Language, SourceLoc, Offset,
1254     SourceLocRange, CodeMutant, SchemataChecksum;
1255 import dextool.plugin.mutate.backend.analyze.utility;
1256 
1257 /// Language generic schemata result.
1258 class SchemataResult {
1259     static struct Fragment {
1260         Offset offset;
1261         const(ubyte)[] text;
1262         CodeMutant[] mutants;
1263     }
1264 
1265     static struct Fragments {
1266         // TODO: change to using appender
1267         Fragment[] fragments;
1268     }
1269 
1270     private {
1271         Fragments[AbsolutePath] fragments;
1272     }
1273 
1274     /// Returns: all fragments containing mutants per file.
1275     Fragments[AbsolutePath] getFragments() @safe {
1276         return fragments;
1277     }
1278 
1279     /// Assuming that all fragments for a file should be merged to one huge.
1280     private void putFragment(AbsolutePath file, Fragment sf) {
1281         fragments.update(file, () => Fragments([sf]), (ref Fragments a) {
1282             a.fragments ~= sf;
1283         });
1284     }
1285 
1286     override string toString() @safe {
1287         import std.range : put;
1288         import std.utf : byUTF;
1289 
1290         auto w = appender!string();
1291 
1292         void toBuf(Fragments s) {
1293             foreach (f; s.fragments) {
1294                 formattedWrite(w, "  %s: %s\n", f.offset,
1295                         (cast(const(char)[]) f.text).byUTF!(const(char)));
1296                 formattedWrite(w, "%(    %s\n%)\n", f.mutants);
1297             }
1298         }
1299 
1300         foreach (k; fragments.byKey.array.sort) {
1301             try {
1302                 formattedWrite(w, "%s:\n", k);
1303                 toBuf(fragments[k]);
1304             } catch (Exception e) {
1305             }
1306         }
1307 
1308         return w.data;
1309     }
1310 }
1311 
1312 /** Build scheman from the fragments.
1313  *
1314  * TODO: optimize the implementation. A lot of redundant memory allocations
1315  * etc.
1316  *
1317  * Conservative to only allow up to <user defined> mutants per schemata but it
1318  * reduces the chance that one failing schemata is "fatal", loosing too many
1319  * muntats.
1320  */
1321 struct SchemataBuilder {
1322     import std.algorithm : any, all;
1323     import my.container.vector;
1324     import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ;
1325     import dextool.plugin.mutate.backend.database.type : SchemaFragmentV2;
1326 
1327     static struct SchemataFragment {
1328         Path file;
1329         Offset offset;
1330         const(ubyte)[] text;
1331     }
1332 
1333     static struct Fragment {
1334         SchemataFragment fragment;
1335         CodeMutant[] mutants;
1336     }
1337 
1338     static struct ET {
1339         SchemataFragment[] fragments;
1340         CodeMutant[] mutants;
1341         SchemataChecksum checksum;
1342     }
1343 
1344     // TODO: remove SchemataChecksum?
1345 
1346     /// Controls the probability that a mutant is part of the currently generating schema.
1347     SchemaQ schemaQ;
1348 
1349     /// use probability for if a mutant is injected or not
1350     bool useProbability;
1351 
1352     /// if the probability should also influence if the scheam is smaller.
1353     bool useProbablitySmallSize;
1354 
1355     // if fragments that are part of scheman that didn't reach the min
1356     // threshold should be discarded.
1357     bool discardMinScheman;
1358 
1359     /// The threshold start at this value.
1360     double thresholdStartValue = 0.0;
1361 
1362     /// Max mutants per schema.
1363     long mutantsPerSchema = 1000;
1364 
1365     /// Minimal mutants that a schema must contain for it to be valid.
1366     long minMutantsPerSchema = 3;
1367 
1368     Vector!Fragment current;
1369     Vector!Fragment rest;
1370 
1371     /// Size in bytes of the cache of fragments.
1372     size_t cacheSize;
1373 
1374     /** Merge analyze fragments into larger schemata fragments. If a schemata
1375      * fragment is large enough it is converted to a schemata. Otherwise kept
1376      * for pass2.
1377      *
1378      * Schematan from this pass only contain one kind and only affect one file.
1379      */
1380     void put(Fragment[] fragments) {
1381         foreach (a; fragments) {
1382             current.put(a);
1383             incrCache(a.fragment);
1384         }
1385     }
1386 
1387     private void incrCache(ref SchemataFragment a) @safe pure nothrow @nogc {
1388         cacheSize += a.text.length + (cast(const(ubyte)[]) a.file.toString).length + typeof(a)
1389             .sizeof;
1390     }
1391 
1392     bool empty() @safe pure nothrow const @nogc {
1393         return current.length == 0 && rest.length == 0;
1394     }
1395 
1396     auto stats() @safe pure nothrow const {
1397         static struct Stats {
1398             double cacheSizeMb;
1399             size_t current;
1400             size_t rest;
1401         }
1402 
1403         return Stats(cast(double) cacheSize / (1024 * 1024), current.length, rest.length);
1404     }
1405 
1406     /** Merge schemata fragments to schemas. A schemata from this pass may may
1407      * contain multiple mutation kinds and span over multiple files.
1408      */
1409     Optional!ET next() {
1410         import std.algorithm : max;
1411 
1412         Index!Path index;
1413         auto app = appender!(Fragment[])();
1414         Set!CodeMutant local;
1415         auto threshold = () => max(thresholdStartValue,
1416                 cast(double) local.length / cast(double) mutantsPerSchema);
1417 
1418         while (!current.empty) {
1419             if (local.length >= mutantsPerSchema) {
1420                 // done now so woop
1421                 break;
1422             }
1423 
1424             auto a = current.front;
1425             current.popFront;
1426 
1427             if (a.mutants.empty)
1428                 continue;
1429 
1430             if (index.intersect(a.fragment.file, a.fragment.offset)) {
1431                 rest.put(a);
1432                 continue;
1433             }
1434 
1435             // if any of the mutants in the schema has already been included.
1436             if (any!(a => a in local)(a.mutants)) {
1437                 rest.put(a);
1438                 continue;
1439             }
1440 
1441             // if any of the mutants fail the probability to be included
1442             if (useProbability && any!(b => !schemaQ.use(a.fragment.file,
1443                     b.mut.kind, threshold()))(a.mutants)) {
1444                 // TODO: remove this line of code in the future. used for now,
1445                 // ugly, to see that it behavies as expected.
1446                 //log.tracef("probability postpone fragment with mutants %s %s",
1447                 //        a.mutants.length, a.mutants.map!(a => a.mut.kind));
1448                 rest.put(a);
1449                 continue;
1450             }
1451 
1452             // no use in using a mutant that has zero probability because then, it will always fail.
1453             if (any!(b => schemaQ.isZero(a.fragment.file, b.mut.kind))(a.mutants)) {
1454                 continue;
1455             }
1456 
1457             app.put(a);
1458             local.add(a.mutants);
1459             index.put(a.fragment.file, a.fragment.offset);
1460 
1461             if (useProbablitySmallSize && local.length > minMutantsPerSchema
1462                     && any!(b => !schemaQ.use(a.fragment.file, b.mut.kind, threshold()))(a.mutants)) {
1463                 break;
1464             }
1465         }
1466 
1467         if (local.length == 0 || local.length < minMutantsPerSchema) {
1468             if (discardMinScheman) {
1469                 logger.tracef("discarding %s fragments with %s mutants",
1470                         app.data.length, app.data.map!(a => a.mutants.length).sum);
1471             } else {
1472                 rest.put(app.data);
1473             }
1474             return none!ET;
1475         }
1476 
1477         ET v;
1478         v.fragments = app.data.map!(a => a.fragment).array;
1479         v.mutants = local.toArray;
1480         v.checksum = toSchemataChecksum(v.mutants);
1481 
1482         return some(v);
1483     }
1484 
1485     bool isDone() @safe pure nothrow const @nogc {
1486         return current.empty;
1487     }
1488 
1489     void restart() @safe pure nothrow @nogc {
1490         current = rest;
1491         rest.clear;
1492 
1493         cacheSize = 0;
1494         foreach (a; current[])
1495             incrCache(a.fragment);
1496     }
1497 }
1498 
1499 /** A schema is uniquely identified by the mutants it contains.
1500  *
1501  * The order of the mutants are irrelevant because they are always sorted by
1502  * their value before the checksum is calculated.
1503  */
1504 SchemataChecksum toSchemataChecksum(CodeMutant[] mutants) {
1505     import dextool.plugin.mutate.backend.utility : BuildChecksum, toChecksum, toBytes;
1506 
1507     BuildChecksum h;
1508     foreach (a; mutants.sort!((a, b) => a.id.value < b.id.value)
1509             .map!(a => a.id.value)) {
1510         h.put(a.c0.toBytes);
1511     }
1512 
1513     return SchemataChecksum(toChecksum(h));
1514 }
1515 
1516 /** The total state for building schemas in runtime.
1517  *
1518  * The intention isn't to perfectly travers and handle all mutants in the
1519  * worklist if the worklist is manipulated while the schema generation is
1520  * running. It is just "good enough" to generate schemas for those mutants when
1521  * it was started.
1522  */
1523 struct SchemaBuildState {
1524     import sumtype;
1525     import my.optional;
1526     import dextool.plugin.mutate.backend.database.type : FileId, SchemaFragmentV2;
1527 
1528     enum State : ubyte {
1529         none,
1530         processFiles,
1531         prepareReduction,
1532         reduction,
1533         prepareFinalize,
1534         finalize1,
1535         finalize2,
1536         done,
1537     }
1538 
1539     static struct ProcessFiles {
1540         FileId[] files;
1541         size_t idx;
1542 
1543         FileId pop() @safe pure nothrow scope {
1544             if (idx == files.length)
1545                 return FileId.init;
1546             return files[idx++];
1547         }
1548 
1549         bool isDone() @safe pure nothrow const @nogc scope {
1550             return idx == files.length;
1551         }
1552 
1553         size_t filesLeft() @safe pure nothrow const @nogc scope {
1554             return files.length - idx;
1555         }
1556 
1557         void reset() @safe pure nothrow @nogc scope {
1558             idx = 0;
1559         }
1560 
1561         void clear() @safe pure nothrow @nogc scope {
1562             files = null;
1563             reset;
1564         }
1565     }
1566 
1567     // State of the schema building
1568     State st;
1569     private int reducedTicks;
1570 
1571     // Files to use when generating schemas.
1572     ProcessFiles files;
1573 
1574     SchemataBuilder builder;
1575 
1576     // User configuration.
1577     typeof(ConfigSchema.minMutantsPerSchema) minMutantsPerSchema = 3;
1578     typeof(ConfigSchema.mutantsPerSchema) mutantsPerSchema = 1000;
1579 
1580     void initFiles(FileId[] files) @safe nothrow {
1581         import std.random : randomCover;
1582 
1583         try {
1584             // improve the schemas non-determinism between each `test` run.
1585             this.files.files = files.randomCover.array;
1586         } catch (Exception e) {
1587             this.files.files = files;
1588         }
1589     }
1590 
1591     /// Step through the schema building.
1592     void tick() @safe nothrow {
1593         logger.tracef("state_pre: %s %s", st, builder.stats).collectException;
1594         final switch (st) {
1595         case State.none:
1596             st = State.processFiles;
1597             try {
1598                 setIntermediate;
1599             } catch (Exception e) {
1600                 st = State.done;
1601             }
1602             break;
1603         case State.processFiles:
1604             if (files.isDone)
1605                 st = State.prepareReduction;
1606             try {
1607                 setIntermediate;
1608             } catch (Exception e) {
1609                 st = State.done;
1610             }
1611             break;
1612         case State.prepareReduction:
1613             st = State.reduction;
1614             break;
1615         case State.reduction:
1616             immutable magic = 10; // reduce the size until it is 1/10 of the original
1617             immutable magic2 = 5; // if it goes <95% then it is too high probability to fail
1618 
1619             if (builder.empty)
1620                 st = State.prepareFinalize;
1621             else if (++reducedTicks > (magic * magic2))
1622                 st = State.prepareFinalize;
1623 
1624             try {
1625                 setReducedIntermediate(1 + reducedTicks / magic, reducedTicks % magic2);
1626             } catch (Exception e) {
1627                 st = State.done;
1628             }
1629             break;
1630         case State.prepareFinalize:
1631             st = State.finalize1;
1632             break;
1633         case State.finalize1:
1634             st = State.finalize2;
1635             try {
1636                 finalize;
1637             } catch (Exception e) {
1638                 st = State.done;
1639             }
1640             break;
1641         case State.finalize2:
1642             if (builder.isDone)
1643                 st = State.done;
1644             break;
1645         case State.done:
1646             break;
1647         }
1648         logger.trace("state_post: ", st).collectException;
1649     }
1650 
1651     /// Add all fragments from one of the files to process to those to be
1652     /// incorporated into future schemas.
1653     /// Returns: number of fragments added.
1654     size_t updateFiles(ref Set!MutationStatusId whiteList, scope SchemaFragmentV2[]delegate(
1655             FileId) @safe fragmentsFn, scope Nullable!Path delegate(FileId) @safe fnameFn,
1656             scope Mutation.Kind delegate(MutationStatusId) @safe kindFn) @safe nothrow {
1657         import dextool.plugin.mutate.backend.type : CodeChecksum, Mutation;
1658         import dextool.plugin.mutate.backend.database : toChecksum;
1659 
1660         if (files.isDone)
1661             return 0;
1662         auto id = files.pop;
1663         try {
1664             const fname = fnameFn(id);
1665             if (fname.isNull)
1666                 return 0;
1667 
1668             auto app = appender!(SchemataBuilder.Fragment[])();
1669             auto frags = fragmentsFn(id);
1670             foreach (a; frags) {
1671                 auto cm = a.mutants
1672                     .filter!(a => a in whiteList)
1673                     .map!(a => CodeMutant(CodeChecksum(a.toChecksum),
1674                             Mutation(kindFn(a), Mutation.Status.unknown)))
1675                     .array;
1676                 if (!cm.empty) {
1677                     app.put(SchemataBuilder.Fragment(SchemataBuilder.SchemataFragment(fname.get,
1678                             a.offset, a.text), cm));
1679                 }
1680             }
1681 
1682             builder.put(app.data);
1683             return app.data.length;
1684         } catch (Exception e) {
1685             logger.trace(e.msg).collectException;
1686         }
1687         return 0;
1688     }
1689 
1690     Optional!(SchemataBuilder.ET) process() {
1691         auto rval = builder.next;
1692         builder.restart;
1693         return rval;
1694     }
1695 
1696     void setMinMutants(long desiredValue) {
1697         // seems like 200 Mbyte is large enough to generate scheman with >1000
1698         // mutants easily when running on LLVM.
1699         enum MaxCache = 200 * 1024 * 1024;
1700         if (builder.cacheSize > MaxCache) {
1701             // panic mode, just empty it as fast as possible.
1702             logger.infof(
1703                     "Schema cache is %s bytes (limit %s). Producing as many schemas as possible to flush the cache.",
1704                     builder.cacheSize, MaxCache);
1705             builder.minMutantsPerSchema = minMutantsPerSchema.get;
1706         } else {
1707             builder.minMutantsPerSchema = desiredValue;
1708         }
1709     }
1710 
1711     void setIntermediate() {
1712         logger.trace("schema generator phase: intermediate");
1713         builder.discardMinScheman = false;
1714         builder.useProbability = true;
1715         builder.useProbablitySmallSize = false;
1716         builder.mutantsPerSchema = mutantsPerSchema.get;
1717         builder.thresholdStartValue = 1.0;
1718 
1719         setMinMutants(mutantsPerSchema.get);
1720     }
1721 
1722     void setReducedIntermediate(long sizeDiv, long threshold) {
1723         import std.algorithm : max;
1724 
1725         logger.tracef("schema generator phase: reduced size:%s threshold:%s", sizeDiv, threshold);
1726         builder.discardMinScheman = false;
1727         builder.useProbability = true;
1728         builder.useProbablitySmallSize = false;
1729         builder.mutantsPerSchema = mutantsPerSchema.get;
1730         // TODO: interresting effect. this need to be studied. I think this
1731         // is the behavior that is "best".
1732         builder.thresholdStartValue = 1.0 - (cast(double) threshold / 100.0);
1733 
1734         setMinMutants(max(minMutantsPerSchema.get, mutantsPerSchema.get / sizeDiv));
1735     }
1736 
1737     /// Consume all fragments or discard.
1738     void finalize() {
1739         logger.trace("schema generator phase: finalize");
1740         builder.discardMinScheman = true;
1741         builder.useProbability = false;
1742         builder.useProbablitySmallSize = true;
1743         builder.mutantsPerSchema = mutantsPerSchema.get;
1744         builder.minMutantsPerSchema = minMutantsPerSchema.get;
1745         builder.thresholdStartValue = 0;
1746     }
1747 }