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 }