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 Test a mutant by modifying the source code.
11 */
12 module dextool.plugin.mutate.backend.test_mutant.source_mutant;
13 
14 import core.time : Duration;
15 import logger = std.experimental.logger;
16 import std.algorithm : sort, map, filter, among;
17 import std.array : empty, array;
18 import std.exception : collectException;
19 import std.path : buildPath;
20 
21 import miniorm : spinSql;
22 import my.fsm : next, act, get, TypeDataMap;
23 import my.hash : Checksum64;
24 import my.named_type;
25 import my.optional;
26 import my.set;
27 import proc : DrainElement;
28 import sumtype;
29 
30 static import my.fsm;
31 
32 import dextool.plugin.mutate.backend.database : Database, MutationEntry, ChecksumTestCmdOriginal;
33 import dextool.plugin.mutate.backend.interface_ : FilesysIO, Blob;
34 import dextool.plugin.mutate.backend.test_mutant.common;
35 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, SkipTests;
36 import dextool.plugin.mutate.backend.type : Mutation, TestCase;
37 import dextool.plugin.mutate.config;
38 import dextool.plugin.mutate.type : ShellCommand;
39 import dextool.type : AbsolutePath, Path;
40 
41 @safe:
42 
43 /** Drive the control flow when testing **a** mutant.
44  */
45 struct MutationTestDriver {
46     import std.datetime.stopwatch : StopWatch;
47     import std.typecons : Tuple;
48 
49     // Hash of current test binaries
50     HashFile[string] testBinaryHashes;
51 
52     static struct Global {
53         FilesysIO fio;
54         Database* db;
55 
56         /// The mutant to apply.
57         MutationEntry mutp;
58 
59         /// Runs the test commands.
60         TestRunner* runner;
61 
62         TestBinaryDb* testBinaryDb;
63 
64         NamedType!(bool, Tag!"UseSkipMutant", bool.init, TagStringable) useSkipMutant;
65 
66         /// File to mutate.
67         AbsolutePath mutateFile;
68 
69         /// The original file.
70         Blob original;
71 
72         /// The result of running the test cases.
73         TestResult testResult;
74 
75         /// Test cases that killed the mutant.
76         TestCase[] testCases;
77 
78         /// How long it took to do the mutation testing.
79         StopWatch swCompile;
80         StopWatch swTest;
81     }
82 
83     static struct None {
84     }
85 
86     static struct Initialize {
87     }
88 
89     static struct MutateCode {
90         NamedType!(bool, Tag!"FilesysError", bool.init, TagStringable, ImplicitConvertable) filesysError;
91         NamedType!(bool, Tag!"MutationError", bool.init, TagStringable, ImplicitConvertable) mutationError;
92     }
93 
94     static struct TestMutantData {
95         /// If the user has configured that the test cases should be analyzed.
96         bool hasTestCaseOutputAnalyzer;
97         ShellCommand buildCmd;
98         Duration buildCmdTimeout;
99     }
100 
101     static struct TestMutant {
102         NamedType!(bool, Tag!"HasTestOutput", bool.init, TagStringable, ImplicitConvertable) hasTestOutput;
103         Optional!(Mutation.Status) calcStatus;
104     }
105 
106     // if checksums of test binaries is used to set the status.
107     static struct MarkCalcStatus {
108         Mutation.Status status;
109     }
110 
111     static struct RestoreCode {
112         NamedType!(bool, Tag!"FilesysError", bool.init, TagStringable, ImplicitConvertable) filesysError;
113     }
114 
115     static struct TestBinaryAnalyze {
116         NamedType!(bool, Tag!"HasTestOutput", bool.init, TagStringable, ImplicitConvertable) hasTestOutput;
117     }
118 
119     static struct TestCaseAnalyzeData {
120         TestCaseAnalyzer* testCaseAnalyzer;
121     }
122 
123     static struct TestCaseAnalyze {
124         bool unstableTests;
125     }
126 
127     static struct StoreResult {
128     }
129 
130     static struct Cover {
131     }
132 
133     static struct Done {
134     }
135 
136     static struct FilesysError {
137     }
138 
139     // happens when an error occurs during mutations testing but that do not
140     // prohibit testing of other mutants
141     static struct NoResultRestoreCode {
142     }
143 
144     static struct NoResult {
145     }
146 
147     alias Fsm = my.fsm.Fsm!(None, Initialize, MutateCode, TestMutant, RestoreCode, TestCaseAnalyze, StoreResult, Done,
148             FilesysError, NoResultRestoreCode, NoResult, MarkCalcStatus, TestBinaryAnalyze, Cover);
149     alias LocalStateDataT = Tuple!(TestMutantData, TestCaseAnalyzeData);
150 
151     private {
152         Fsm fsm;
153         Global global;
154         TypeDataMap!(LocalStateDataT, TestMutant, TestCaseAnalyze) local;
155         bool isRunning_ = true;
156         bool stopBecauseError_;
157     }
158 
159     MutationTestResult[] result;
160 
161     this(Global global, TestMutantData l1, TestCaseAnalyzeData l2) {
162         this.global = global;
163         this.local = LocalStateDataT(l1, l2);
164 
165         if (logger.globalLogLevel.among(logger.LogLevel.trace, logger.LogLevel.all))
166             fsm.logger = (string s) { logger.trace(s); };
167     }
168 
169     static void execute_(ref MutationTestDriver self) @trusted {
170         self.fsm.next!((None a) => fsm(Initialize.init),
171                 (Initialize a) => fsm(MutateCode.init), (MutateCode a) {
172             if (a.filesysError)
173                 return fsm(FilesysError.init);
174             else if (a.mutationError)
175                 return fsm(NoResultRestoreCode.init);
176             return fsm(TestMutant.init);
177         }, (TestMutant a) {
178             if (a.calcStatus.hasValue)
179                 return fsm(MarkCalcStatus(a.calcStatus.orElse(Mutation.Status.unknown)));
180             return fsm(TestBinaryAnalyze(a.hasTestOutput));
181         }, (TestBinaryAnalyze a) {
182             if (self.global.testResult.status == Mutation.Status.killed
183                 && self.local.get!TestMutant.hasTestCaseOutputAnalyzer && a.hasTestOutput) {
184                 return fsm(TestCaseAnalyze.init);
185             }
186             return fsm(RestoreCode.init);
187         }, (TestCaseAnalyze a) {
188             if (a.unstableTests)
189                 return fsm(NoResultRestoreCode.init);
190             return fsm(RestoreCode.init);
191         }, (MarkCalcStatus a) => RestoreCode.init, (RestoreCode a) {
192             if (a.filesysError)
193                 return fsm(FilesysError.init);
194             return fsm(StoreResult.init);
195         }, (StoreResult a) => Cover.init, (Cover a) => Done.init,
196                 (Done a) => fsm(a), (FilesysError a) => fsm(a),
197                 (NoResultRestoreCode a) => fsm(NoResult.init), (NoResult a) => fsm(a),);
198 
199         self.fsm.act!self;
200     }
201 
202 nothrow:
203 
204     void execute() {
205         try {
206             execute_(this);
207         } catch (Exception e) {
208             logger.warning(e.msg).collectException;
209         }
210     }
211 
212     /// Returns: true as long as the driver is processing a mutant.
213     bool isRunning() {
214         return isRunning_;
215     }
216 
217     bool stopBecauseError() {
218         return stopBecauseError_;
219     }
220 
221     void opCall(None data) {
222     }
223 
224     void opCall(Initialize data) {
225         global.swCompile.start;
226     }
227 
228     void opCall(Done data) {
229         isRunning_ = false;
230     }
231 
232     void opCall(FilesysError data) {
233         logger.warning("Filesystem error").collectException;
234         isRunning_ = false;
235         stopBecauseError_ = true;
236     }
237 
238     void opCall(NoResultRestoreCode data) {
239         RestoreCode tmp;
240         this.opCall(tmp);
241     }
242 
243     void opCall(NoResult data) {
244         isRunning_ = false;
245     }
246 
247     void opCall(ref MutateCode data) {
248         import dextool.plugin.mutate.backend.generate_mutant : generateMutant,
249             GenerateMutantResult, GenerateMutantStatus;
250 
251         try {
252             global.mutateFile = AbsolutePath(buildPath(global.fio.getOutputDir, global.mutp.file));
253             global.original = global.fio.makeInput(global.mutateFile);
254         } catch (Exception e) {
255             logger.error(e.msg).collectException;
256             logger.warning("Unable to read ", global.mutateFile).collectException;
257             data.filesysError.get = true;
258             return;
259         }
260 
261         // mutate
262         try {
263             auto fout = global.fio.makeOutput(global.mutateFile);
264             auto mut_res = generateMutant(*global.db, global.mutp, global.original, fout);
265 
266             final switch (mut_res.status) with (GenerateMutantStatus) {
267             case error:
268                 data.mutationError.get = true;
269                 break;
270             case filesysError:
271                 data.filesysError.get = true;
272                 break;
273             case databaseError:
274                 // such as when the database is locked
275                 data.mutationError.get = true;
276                 break;
277             case checksumError:
278                 data.filesysError.get = true;
279                 break;
280             case noMutation:
281                 data.mutationError.get = true;
282                 break;
283             case ok:
284                 try {
285                     logger.infof("from '%s' to '%s' in %s:%s:%s",
286                             cast(const(char)[]) mut_res.from, cast(const(char)[]) mut_res.to,
287                             global.mutateFile, global.mutp.sloc.line, global.mutp.sloc.column);
288                     logger.trace(global.mutp.id).collectException;
289                 } catch (Exception e) {
290                     logger.warningf("%s %s", global.mutp.id, e.msg);
291                 }
292                 break;
293             }
294         } catch (Exception e) {
295             logger.warning(e.msg).collectException;
296             data.mutationError.get = true;
297         }
298     }
299 
300     void opCall(ref TestMutant data) @trusted {
301         {
302             scope (exit)
303                 () { global.swCompile.stop; global.swTest.start; }();
304 
305             bool successCompile;
306             compile(local.get!TestMutant.buildCmd,
307                     local.get!TestMutant.buildCmdTimeout, PrintCompileOnFailure(false)).match!(
308                     (Mutation.Status a) { global.testResult.status = a; }, (bool success) {
309                 successCompile = success;
310             },);
311 
312             if (!successCompile)
313                 return;
314         }
315 
316         Set!string skipTests;
317         if (!global.testBinaryDb.empty) {
318             bool allOriginal = !global.testBinaryDb.original.empty;
319             bool allAlive = !global.testBinaryDb.mutated.empty;
320             bool anyKill;
321             bool loopRun;
322             try {
323                 foreach (f; global.runner.testCmds.map!(a => a.cmd.value[0]).hashFiles) {
324                     loopRun = true;
325 
326                     if (f.cs in global.testBinaryDb.original) {
327                         skipTests.add(f.file);
328                         logger.tracef("match original %s %s", f.file, f.cs);
329                     } else {
330                         allOriginal = false;
331                         testBinaryHashes[f.file] = f;
332                     }
333 
334                     if (auto v = f.cs in global.testBinaryDb.mutated) {
335                         logger.tracef("match mutated %s:%s %s", *v, f.file, f.cs);
336 
337                         allAlive = allAlive && *v == Mutation.Status.alive;
338                         anyKill = anyKill || *v == Mutation.Status.killed;
339 
340                         if ((*v).among(Mutation.Status.alive, Mutation.Status.killed))
341                             skipTests.add(f.file);
342                     } else {
343                         allAlive = false;
344                     }
345                 }
346             } catch (Exception e) {
347                 logger.warning(e.msg).collectException;
348             }
349 
350             if (!loopRun) {
351                 logger.trace("failed to checksum test_cmds: ",
352                         global.runner.testCmds.map!(a => a.cmd)).collectException;
353             } else if (allOriginal) {
354                 data.calcStatus = some(Mutation.Status.equivalent);
355             } else if (anyKill) {
356                 data.calcStatus = some(Mutation.Status.killed);
357             } else if (allAlive) {
358                 data.calcStatus = some(Mutation.Status.alive);
359             } else if (skipTests.length == global.testBinaryDb.original.length) {
360                 // happens when there is a mix of alive or original
361                 data.calcStatus = some(Mutation.Status.alive);
362             }
363 
364             // TODO: prefix with debug after 2021-10-23
365             logger.tracef("allOriginal:%s allAlive:%s anyKill:%s dbLen:%s", allOriginal, allAlive, anyKill,
366                     global.testBinaryDb.mutated.length + global.testBinaryDb.original.length)
367                 .collectException;
368         }
369 
370         if (data.calcStatus.hasValue) {
371             logger.info("Using mutant status from previous test executions").collectException;
372         } else if (!skipTests.empty && !global.testBinaryDb.empty) {
373             logger.infof("%s/%s test_cmd unaffected by mutant", skipTests.length,
374                     global.testBinaryDb.original.length).collectException;
375             logger.trace("skipped tests ", skipTests.toRange).collectException;
376         }
377 
378         if (!data.calcStatus.hasValue) {
379             global.testResult = runTester(*global.runner, SkipTests(skipTests));
380             data.hasTestOutput.get = !global.testResult.output.empty;
381         }
382     }
383 
384     void opCall(TestBinaryAnalyze data) {
385         scope (exit)
386             testBinaryHashes = null;
387 
388         // means that the user has configured that it should be used because
389         // then at least original is set.
390         if (!global.testBinaryDb.empty) {
391             final switch (global.testResult.status) with (Mutation) {
392             case Status.alive:
393                 foreach (a; testBinaryHashes.byKeyValue) {
394                     logger.tracef("save %s -> %s", a.key, Status.alive).collectException;
395                     global.testBinaryDb.add(a.value.cs, Status.alive);
396                 }
397                 break;
398             case Status.killed:
399                 foreach (a; global.testResult.output.byKey.map!(a => a.value[0])) {
400                     if (auto v = a in testBinaryHashes) {
401                         logger.tracef("save %s -> %s", a,
402                                 global.testResult.status).collectException;
403                         global.testBinaryDb.add(v.cs, global.testResult.status);
404                     }
405                 }
406                 break;
407             case Status.timeout:
408                 goto case;
409             case Status.noCoverage:
410                 goto case;
411             case Status.killedByCompiler:
412                 goto case;
413             case Status.equivalent:
414                 goto case;
415             case Status.skipped:
416                 goto case;
417             case Status.unknown:
418                 break;
419             }
420         }
421     }
422 
423     void opCall(ref TestCaseAnalyze data) {
424         scope (exit)
425             global.testResult.output = null;
426 
427         foreach (testCmd; global.testResult.output.byKeyValue) {
428             try {
429                 auto analyze = local.get!TestCaseAnalyze.testCaseAnalyzer.analyze(testCmd.key,
430                         testCmd.value);
431 
432                 analyze.match!((TestCaseAnalyzer.Success a) {
433                     global.testCases ~= a.failed ~ a.testCmd;
434                 }, (TestCaseAnalyzer.Unstable a) {
435                     logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable);
436                     logger.info(
437                         "As configured the result is ignored which will force the mutant to be re-tested");
438                     data.unstableTests = true;
439                 }, (TestCaseAnalyzer.Failed a) {
440                     logger.warning("The parser that analyze the output from test case(s) failed");
441                 });
442             } catch (Exception e) {
443                 logger.warning(e.msg).collectException;
444             }
445         }
446     }
447 
448     void opCall(MarkCalcStatus data) {
449         global.testResult.output = null;
450         global.testResult.status = data.status;
451     }
452 
453     void opCall(StoreResult data) {
454         const statusId = spinSql!(() => global.db.mutantApi.getMutationStatusId(global.mutp.id));
455 
456         global.swTest.stop;
457         auto profile = MutantTimeProfile(global.swCompile.peek, global.swTest.peek);
458 
459         if (statusId.isNull) {
460             logger.trace("No MutationStatusId for ", global.mutp.id.get).collectException;
461             return;
462         }
463 
464         result = [
465             MutationTestResult(global.mutp.id, statusId.get, global.testResult.status,
466                     profile, global.testCases, global.testResult.exitStatus)
467         ];
468 
469         logger.infof("%s:%s (%s)", global.testResult.status,
470                 global.testResult.exitStatus.get, profile).collectException;
471         logger.infof(!global.testCases.empty, `killed by [%-(%s, %)]`,
472                 global.testCases.sort.map!"a.name").collectException;
473     }
474 
475     void opCall(Cover data) {
476         import std.algorithm : canFind;
477         import dextool.plugin.mutate.backend.mutation_type.cover : covers, Cover;
478 
479         // only SDL mutants are supported for propgatation for now because a
480         // surviving SDL is a strong indication that all internal mutants will
481         // survive. The SDL mutant have basically deleted the code so. Note
482         // though that there are probably corner cases wherein this assumption
483         // isn't true.
484 
485         if (!global.useSkipMutant.get || result.empty || global.mutp.mp.mutations.empty)
486             return;
487 
488         const(Mutation.Kind)[] cover;
489         if (auto v = Cover(global.mutp.mp.mutations[0].kind, result[0].status) in covers) {
490             cover = *v;
491         } else {
492             logger.tracef("no cover for %s:%s", global.mutp.mp.mutations[0].kind,
493                     result[0].status).collectException;
494             return;
495         }
496 
497         if (cover.empty)
498             return;
499 
500         logger.trace("Performing cover").collectException;
501         logger.tracef("cover for %s:%s", global.mutp.mp.mutations[0].kind,
502                 result[0].status).collectException;
503 
504         void propagate() {
505             const fid = global.db.getFileId(global.mutp.file);
506             if (fid.isNull)
507                 return;
508 
509             foreach (const stId; global.db.mutantApi.mutantsInRegion(fid.get,
510                     global.mutp.mp.offset, Mutation.Status.unknown, cover).filter!(
511                     a => a != result[0].id)) {
512                 const mutId = global.db.mutantApi.getMutationId(stId);
513                 if (mutId.isNull)
514                     return;
515                 result ~= MutationTestResult(mutId.get, stId, Mutation.Status.skipped,
516                         MutantTimeProfile.init, null, ExitStatus(0));
517             }
518 
519             logger.tracef("Marked %s as skipped", result.length - 1).collectException;
520         }
521 
522         spinSql!(() @trusted {
523             auto t = global.db.transaction;
524             propagate;
525             t.commit;
526         });
527     }
528 
529     void opCall(ref RestoreCode data) {
530         // restore the original file.
531         try {
532             global.fio.makeOutput(global.mutateFile).write(global.original.content);
533         } catch (Exception e) {
534             logger.error(e.msg).collectException;
535             // fatal error because being unable to restore a file prohibit
536             // future mutations.
537             data.filesysError.get = true;
538         }
539     }
540 }