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 }