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;
14 import std.array : empty, array, appender;
15 import std.conv : to;
16 import std.datetime : Duration;
17 import std.datetime.stopwatch : StopWatch, AutoStart;
18 import std.exception : collectException;
19 import std.format : format;
20 import std.typecons : Tuple;
21 
22 import proc : DrainElement;
23 import sumtype;
24 import blob_model;
25 
26 import my.fsm : Fsm, next, act, get, TypeDataMap;
27 import my.path;
28 import my.set;
29 static import my.fsm;
30 
31 import dextool.plugin.mutate.backend.database : MutationStatusId, Database,
32     spinSql, SchemataId, Schemata;
33 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
34 import dextool.plugin.mutate.backend.test_mutant.common;
35 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult;
36 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum;
37 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand, UserRuntime;
38 
39 @safe:
40 
41 struct SchemataTestDriver {
42     private {
43         /// True as long as the schemata driver is running.
44         bool isRunning_ = true;
45         bool hasFatalError_;
46         bool isInvalidSchema_;
47 
48         FilesysIO fio;
49 
50         Database* db;
51 
52         /// Runs the test commands.
53         TestRunner* runner;
54 
55         Mutation.Kind[] kinds;
56 
57         SchemataId schemataId;
58 
59         /// Result of testing the mutants.
60         MutationTestResult[] result_;
61 
62         /// Time it took to compile the schemata.
63         Duration compileTime;
64         StopWatch swCompile;
65 
66         // Write the instrumented source code to .cov.<ext> for separate
67         // inspection.
68         const bool log;
69 
70         ShellCommand buildCmd;
71         Duration buildCmdTimeout;
72 
73         /// The full schemata that is used..
74         Schemata schemata;
75 
76         AbsolutePath[] modifiedFiles;
77 
78         Set!AbsolutePath roots;
79 
80         TestStopCheck stopCheck;
81     }
82 
83     static struct None {
84     }
85 
86     static struct Initialize {
87         bool error;
88     }
89 
90     static struct InitializeRoots {
91         bool hasRoot;
92     }
93 
94     static struct InjectSchema {
95         bool error;
96     }
97 
98     static struct Compile {
99         bool error;
100     }
101 
102     static struct Done {
103     }
104 
105     static struct Restore {
106         bool error;
107     }
108 
109     static struct NextMutantData {
110         /// Mutants to test.
111         InjectIdResult mutants;
112     }
113 
114     static struct NextMutant {
115         bool done;
116         InjectIdResult.InjectId inject;
117     }
118 
119     static struct TestMutantData {
120         /// If the user has configured that the test cases should be analyzed.
121         bool hasTestCaseOutputAnalyzer;
122     }
123 
124     static struct TestMutant {
125         InjectIdResult.InjectId inject;
126 
127         MutationTestResult result;
128         bool hasTestOutput;
129         // if there are mutants status id's related to a file but the mutants
130         // have been removed.
131         bool mutantIdError;
132     }
133 
134     static struct TestCaseAnalyzeData {
135         TestCaseAnalyzer* testCaseAnalyzer;
136         DrainElement[] output;
137         TestCase[] testCmds;
138     }
139 
140     static struct TestCaseAnalyze {
141         MutationTestResult result;
142         bool unstableTests;
143     }
144 
145     static struct StoreResult {
146         MutationTestResult result;
147     }
148 
149     static struct OverloadCheck {
150         bool halt;
151         bool sleep;
152     }
153 
154     alias Fsm = my.fsm.Fsm!(None, Initialize, InitializeRoots, Done, NextMutant, TestMutant,
155             TestCaseAnalyze, StoreResult, InjectSchema, Compile, Restore, OverloadCheck);
156     alias LocalStateDataT = Tuple!(TestMutantData, TestCaseAnalyzeData, NextMutantData);
157 
158     private {
159         Fsm fsm;
160         TypeDataMap!(LocalStateDataT, TestMutant, TestCaseAnalyze, NextMutant) local;
161     }
162 
163     this(FilesysIO fio, TestRunner* runner, Database* db, TestCaseAnalyzer* testCaseAnalyzer,
164             UserRuntime[] userRuntimeCtrl, SchemataId id, TestStopCheck stopCheck,
165             Mutation.Kind[] kinds, ShellCommand buildCmd, Duration buildCmdTimeout, bool log) {
166         this.fio = fio;
167         this.runner = runner;
168         this.db = db;
169         this.schemataId = id;
170         this.stopCheck = stopCheck;
171         this.kinds = kinds;
172         this.buildCmd = buildCmd;
173         this.buildCmdTimeout = buildCmdTimeout;
174         this.log = log;
175 
176         this.local.get!TestCaseAnalyze.testCaseAnalyzer = testCaseAnalyzer;
177         this.local.get!TestMutant.hasTestCaseOutputAnalyzer = !testCaseAnalyzer.empty;
178 
179         foreach (a; userRuntimeCtrl) {
180             auto p = fio.toAbsoluteRoot(a.file);
181             roots.add(p);
182         }
183 
184         if (logger.globalLogLevel == logger.LogLevel.trace)
185             fsm.logger = (string s) { logger.trace(s); };
186     }
187 
188     static void execute_(ref SchemataTestDriver self) @trusted {
189         self.fsm.next!((None a) => fsm(Initialize.init), (Initialize a) {
190             if (a.error)
191                 return fsm(Done.init);
192             return fsm(InitializeRoots.init);
193         }, (InitializeRoots a) {
194             if (a.hasRoot)
195                 return fsm(InjectSchema.init);
196             return fsm(Done.init);
197         }, (InjectSchema a) {
198             if (a.error)
199                 return fsm(Restore.init);
200             return fsm(Compile.init);
201         }, (Compile a) {
202             if (a.error)
203                 return fsm(Restore.init);
204             return fsm(OverloadCheck.init);
205         }, (OverloadCheck a) {
206             if (a.halt)
207                 return fsm(Restore.init);
208             if (a.sleep)
209                 return fsm(OverloadCheck.init);
210             return fsm(NextMutant.init);
211         }, (NextMutant a) {
212             if (a.done)
213                 return fsm(Restore.init);
214             return fsm(TestMutant(a.inject));
215         }, (TestMutant a) {
216             if (a.mutantIdError)
217                 return fsm(OverloadCheck.init);
218             if (a.result.status == Mutation.Status.killed
219                 && self.local.get!TestMutant.hasTestCaseOutputAnalyzer && a.hasTestOutput) {
220                 return fsm(TestCaseAnalyze(a.result));
221             }
222             return fsm(StoreResult(a.result));
223         }, (TestCaseAnalyze a) {
224             if (a.unstableTests)
225                 return fsm(OverloadCheck.init);
226             return fsm(StoreResult(a.result));
227         }, (StoreResult a) => fsm(OverloadCheck.init), (Restore a) => Done.init, (Done a) => a);
228 
229         self.fsm.act!(self);
230     }
231 
232 nothrow:
233 
234     MutationTestResult[] popResult() {
235         auto tmp = result_;
236         result_ = null;
237         return tmp;
238     }
239 
240     void execute() {
241         try {
242             execute_(this);
243         } catch (Exception e) {
244             logger.warning(e.msg).collectException;
245         }
246     }
247 
248     bool hasFatalError() {
249         return hasFatalError_;
250     }
251 
252     bool isInvalidSchema() {
253         return isInvalidSchema_;
254     }
255 
256     bool isRunning() {
257         return isRunning_;
258     }
259 
260     void opCall(None data) {
261     }
262 
263     void opCall(ref Initialize data) {
264         swCompile = StopWatch(AutoStart.yes);
265 
266         InjectIdBuilder builder;
267         foreach (mutant; spinSql!(() => db.getSchemataMutants(schemataId, kinds))) {
268             auto cs = spinSql!(() { return db.getChecksum(mutant); });
269             if (!cs.isNull) {
270                 builder.put(mutant, cs.get);
271             }
272         }
273         debug logger.trace(builder).collectException;
274 
275         local.get!NextMutant.mutants = builder.finalize;
276 
277         schemata = spinSql!(() => db.getSchemata(schemataId)).get;
278 
279         try {
280             modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file))
281                 .toSet.toRange.array;
282         } catch (Exception e) {
283             logger.warning(e.msg).collectException;
284             hasFatalError_ = true;
285             data.error = true;
286         }
287     }
288 
289     void opCall(ref InitializeRoots data) {
290         if (roots.empty) {
291             auto allRoots = () {
292                 AbsolutePath[] tmp;
293                 try {
294                     tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get)
295                         .map!(a => fio.toAbsoluteRoot(a))
296                         .array;
297                     if (tmp.empty) {
298                         // no root found. Inject the runtime in all files and "hope for
299                         // the best". it will be less efficient but the weak symbol
300                         // should still mean that it link correctly.
301                         tmp = modifiedFiles;
302                     }
303                 } catch (Exception e) {
304                     logger.error(e.msg).collectException;
305                 }
306                 return tmp;
307             }();
308 
309             foreach (r; allRoots) {
310                 roots.add(r);
311             }
312         }
313 
314         auto mods = modifiedFiles.toSet;
315         foreach (r; roots.toRange) {
316             if (r !in mods)
317                 modifiedFiles ~= r;
318         }
319 
320         data.hasRoot = !roots.empty;
321 
322         if (roots.empty) {
323             logger.warning("No root file found to inject the schemata runtime in").collectException;
324         }
325     }
326 
327     void opCall(Done data) {
328         isRunning_ = false;
329     }
330 
331     void opCall(ref InjectSchema data) {
332         import std.path : extension, stripExtension;
333         import dextool.plugin.mutate.backend.database.type : SchemataFragment;
334 
335         scope (exit)
336             schemata = Schemata.init; // release the memory back to the GC
337 
338         Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) {
339             auto edits = appender!(Edit[])();
340             edits.put(extra);
341             foreach (a; fragments) {
342                 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text);
343             }
344             auto m = merge(original, edits.data);
345             return change(new Blob(original.uri, original.content), m.edits);
346         }
347 
348         SchemataFragment[] fragments(Path p) {
349             return schemata.fragments.filter!(a => a.file == p).array;
350         }
351 
352         try {
353             foreach (fname; modifiedFiles) {
354                 auto f = fio.makeInput(fname);
355                 auto extra = () {
356                     if (fname in roots) {
357                         logger.trace("Injecting schemata runtime in ", fname);
358                         return makeRootImpl(f.content.length);
359                     }
360                     return makeHdr;
361                 }();
362 
363                 logger.info("Injecting schema in ", fname);
364 
365                 // writing the schemata.
366                 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra);
367                 fio.makeOutput(fname).write(s);
368 
369                 if (log) {
370                     const ext = fname.toString.extension;
371                     fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension,
372                             schemataId.get, ext).Path)).write(s);
373 
374                     fio.makeOutput(AbsolutePath(format!"%s.%s.kinds.txt"(fname,
375                             schemataId.get).Path)).write(format("%s", kinds));
376                 }
377             }
378         } catch (Exception e) {
379             logger.warning(e.msg).collectException;
380             data.error = true;
381         }
382     }
383 
384     void opCall(ref Compile data) {
385         import colorlog;
386         import dextool.plugin.mutate.backend.test_mutant.common : compile;
387 
388         logger.infof("Compile schema %s", schemataId.get).collectException;
389 
390         compile(buildCmd, buildCmdTimeout, false).match!((Mutation.Status a) {
391             data.error = true;
392         }, (bool success) { data.error = !success; });
393 
394         if (data.error) {
395             isInvalidSchema_ = true;
396 
397             // run again but this time show the output to the user.  scheman
398             // shouldn't fail that often so this help finding out why a schema
399             // fail to compile.
400             compile(buildCmd, buildCmdTimeout, true);
401 
402             logger.info("Skipping schema because it failed to compile".color(Color.yellow))
403                 .collectException;
404             spinSql!(() { db.markUsed(schemataId); });
405             return;
406         }
407 
408         logger.info("Ok".color(Color.green)).collectException;
409 
410         try {
411             logger.info("Sanity check of the generated schemata");
412             auto res = runner.run;
413             data.error = res.status != TestResult.Status.passed;
414         } catch (Exception e) {
415             logger.warning(e.msg).collectException;
416         }
417 
418         if (data.error) {
419             logger.info("Skipping the schemata because the test suite failed".color(Color.yellow))
420                 .collectException;
421             spinSql!(() { db.markUsed(schemataId); });
422         } else {
423             logger.info("Ok".color(Color.green)).collectException;
424         }
425 
426         compileTime = swCompile.peek;
427     }
428 
429     void opCall(ref NextMutant data) {
430         data.done = local.get!NextMutant.mutants.empty;
431 
432         if (!data.done) {
433             data.inject = local.get!NextMutant.mutants.front;
434             local.get!NextMutant.mutants.popFront;
435         }
436     }
437 
438     void opCall(ref TestMutant data) {
439         import std.datetime.stopwatch : StopWatch, AutoStart;
440         import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey,
441             checksumToId;
442         import dextool.plugin.mutate.backend.generate_mutant : makeMutationText;
443 
444         auto sw = StopWatch(AutoStart.yes);
445 
446         data.result.id = data.inject.statusId;
447 
448         auto id = spinSql!(() { return db.getMutationId(data.inject.statusId); });
449         if (id.isNull) {
450             data.mutantIdError = true;
451             return;
452         }
453         auto entry_ = spinSql!(() { return db.getMutation(id.get); });
454         if (entry_.isNull) {
455             data.mutantIdError = true;
456             return;
457         }
458         auto entry = entry_.get;
459 
460         try {
461             const file = fio.toAbsoluteRoot(entry.file);
462             auto txt = makeMutationText(fio.makeInput(file), entry.mp.offset,
463                     entry.mp.mutations[0].kind, entry.lang);
464             debug logger.trace(entry);
465             logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original,
466                     txt.mutation, file, entry.sloc.line, entry.sloc.column);
467         } catch (Exception e) {
468             logger.info(e.msg).collectException;
469         }
470 
471         runner.env[schemataMutantEnvKey] = data.inject.injectId.to!string;
472         scope (exit)
473             runner.env.remove(schemataMutantEnvKey);
474 
475         auto res = runTester(*runner);
476         data.result.profile = MutantTimeProfile(compileTime, sw.peek);
477         // the first tested mutant also get the compile time of the schema.
478         compileTime = Duration.zero;
479 
480         data.result.mutId = id;
481         data.result.status = res.status;
482         data.result.exitStatus = res.exitStatus;
483         data.hasTestOutput = !res.output.empty;
484         local.get!TestCaseAnalyze.output = res.output;
485         local.get!TestCaseAnalyze.testCmds = res.testCmds.map!(
486                 a => TestCase(a.toShortString)).array;
487 
488         logger.infof("%s:%s (%s)", data.result.status,
489                 data.result.exitStatus.get, data.result.profile).collectException;
490         logger.tracef("%s %s injectId:%s", id, data.result.id,
491                 data.inject.injectId).collectException;
492     }
493 
494     void opCall(ref TestCaseAnalyze data) {
495         scope (exit)
496             () {
497             local.get!TestCaseAnalyze.output = null;
498             local.get!TestCaseAnalyze.testCmds = null;
499         }();
500 
501         try {
502             auto analyze = local.get!TestCaseAnalyze.testCaseAnalyzer.analyze(
503                     local.get!TestCaseAnalyze.output);
504 
505             analyze.match!((TestCaseAnalyzer.Success a) {
506                 data.result.testCases = a.failed ~ local.get!TestCaseAnalyze.testCmds;
507             }, (TestCaseAnalyzer.Unstable a) {
508                 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable);
509                 logger.info(
510                     "As configured the result is ignored which will force the mutant to be re-tested");
511                 data.unstableTests = true;
512             }, (TestCaseAnalyzer.Failed a) {
513                 logger.warning("The parser that analyze the output from test case(s) failed");
514             });
515 
516             logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`,
517                     data.result.testCases.sort.map!"a.name").collectException;
518         } catch (Exception e) {
519             logger.warning(e.msg).collectException;
520         }
521     }
522 
523     void opCall(StoreResult data) {
524         result_ ~= data.result;
525     }
526 
527     void opCall(ref OverloadCheck data) {
528         data.halt = stopCheck.isHalt != TestStopCheck.HaltReason.none;
529         data.sleep = stopCheck.isOverloaded;
530 
531         if (data.sleep) {
532             logger.info(stopCheck.overloadToString).collectException;
533             stopCheck.pause;
534         }
535     }
536 
537     void opCall(ref Restore data) {
538         try {
539             restoreFiles(modifiedFiles, fio);
540         } catch (Exception e) {
541             logger.error(e.msg).collectException;
542             data.error = true;
543             hasFatalError_ = true;
544         }
545     }
546 }
547 
548 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit).
549  *
550  * There is a possibility that an injection ID result in a collision because
551  * they are only 32 bit. If that happens the mutant is discarded as unfeasable
552  * to use for schemata.
553  *
554  * TODO: if this is changed to being order dependent then it can handle all
555  * mutants. But I can't see how that can be done easily both because of how the
556  * schemas are generated and how the database is setup.
557  */
558 struct InjectIdBuilder {
559     private {
560         alias InjectId = InjectIdResult.InjectId;
561 
562         InjectId[uint] result;
563         Set!uint collisions;
564     }
565 
566     void put(MutationStatusId id, Checksum cs) @safe pure nothrow {
567         import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId;
568 
569         const injectId = checksumToId(cs);
570         debug logger.tracef("%s %s %s", id, cs, injectId).collectException;
571 
572         if (injectId in collisions) {
573         } else if (injectId in result) {
574             collisions.add(injectId);
575             result.remove(injectId);
576         } else {
577             result[injectId] = InjectId(id, injectId);
578         }
579     }
580 
581     InjectIdResult finalize() @safe pure nothrow {
582         import std.array : array;
583 
584         return InjectIdResult(result.byValue.array);
585     }
586 }
587 
588 struct InjectIdResult {
589     alias InjectId = Tuple!(MutationStatusId, "statusId", uint, "injectId");
590     InjectId[] ids;
591 
592     InjectId front() @safe pure nothrow {
593         assert(!empty, "Can't get front of an empty range");
594         return ids[0];
595     }
596 
597     void popFront() @safe pure nothrow {
598         assert(!empty, "Can't pop front of an empty range");
599         ids = ids[1 .. $];
600     }
601 
602     bool empty() @safe pure nothrow const @nogc {
603         return ids.empty;
604     }
605 }
606 
607 @("shall detect a collision and make sure it is never part of the result")
608 unittest {
609     InjectIdBuilder builder;
610     builder.put(MutationStatusId(1), Checksum(1, 2));
611     builder.put(MutationStatusId(2), Checksum(3, 4));
612     builder.put(MutationStatusId(3), Checksum(1, 2));
613     auto r = builder.finalize;
614 
615     assert(r.front.statusId == MutationStatusId(2));
616     r.popFront;
617     assert(r.empty);
618 }
619 
620 Edit[] makeRootImpl(ulong end) {
621     import dextool.plugin.mutate.backend.resource : schemataImpl;
622 
623     return [
624         makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl)
625     ];
626 }
627 
628 Edit[] makeHdr() {
629     import dextool.plugin.mutate.backend.resource : schemataHeader;
630 
631     return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)];
632 }