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