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