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