1 /** 2 Copyright: Copyright (c) 2017, 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; 11 12 import core.thread : Thread; 13 import core.time : Duration, dur; 14 import logger = std.experimental.logger; 15 import std.algorithm : sort, map, splitter, filter; 16 import std.array : empty, array, appender; 17 import std.datetime : SysTime, Clock; 18 import std.exception : collectException; 19 import std.path : buildPath; 20 import std.typecons : Nullable, NullableRef, nullableRef, Tuple; 21 22 import blob_model : Blob, Uri; 23 import process : DrainElement; 24 import sumtype; 25 26 import dextool.fsm : Fsm, next, act, get, TypeDataMap; 27 import dextool.plugin.mutate.backend.database : Database, MutationEntry, 28 NextMutationEntry, spinSql, MutantTimeoutCtx, MutationId; 29 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 30 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport; 31 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner; 32 import dextool.plugin.mutate.backend.type : Mutation, TestCase; 33 import dextool.plugin.mutate.config; 34 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand; 35 import dextool.set; 36 import dextool.type : AbsolutePath, ExitStatusType, FileName, DirName, Path; 37 38 @safe: 39 40 auto makeTestMutant() { 41 return BuildTestMutant(); 42 } 43 44 private: 45 46 struct BuildTestMutant { 47 @safe: 48 nothrow: 49 50 import dextool.plugin.mutate.type : MutationKind; 51 52 private struct InternalData { 53 Mutation.Kind[] mut_kinds; 54 FilesysIO filesys_io; 55 ConfigMutationTest config; 56 } 57 58 private InternalData data; 59 60 auto config(ConfigMutationTest c) { 61 data.config = c; 62 return this; 63 } 64 65 auto mutations(MutationKind[] v) { 66 import dextool.plugin.mutate.backend.utility : toInternal; 67 68 data.mut_kinds = toInternal(v); 69 return this; 70 } 71 72 ExitStatusType run(ref Database db, FilesysIO fio) nothrow { 73 // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope 74 auto db_ref = () @trusted { return nullableRef(&db); }(); 75 76 auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config); 77 78 try { 79 auto test_driver = TestDriver(driver_data); 80 81 while (test_driver.isRunning) { 82 test_driver.execute; 83 } 84 85 return test_driver.status; 86 } catch (Exception e) { 87 logger.error(e.msg).collectException; 88 } 89 90 return ExitStatusType.Errors; 91 } 92 } 93 94 struct DriverData { 95 NullableRef!Database db; 96 FilesysIO filesysIO; 97 Mutation.Kind[] mutKind; 98 AutoCleanup autoCleanup; 99 ConfigMutationTest conf; 100 } 101 102 /** Run the test suite to verify a mutation. 103 * 104 * Params: 105 * compile_p = compile command 106 * tester_p = test command 107 * timeout = kill the test command and mark mutant as timeout if the runtime exceed this value. 108 * fio = i/o 109 * 110 * Returns: the result of testing the mutant. 111 */ 112 auto runTester(ShellCommand compile_p, ref TestRunner runner) nothrow { 113 import process; 114 115 struct Rval { 116 Mutation.Status status; 117 DrainElement[] output; 118 } 119 120 try { 121 auto p = pipeProcess(compile_p.value).sandbox.drainToNull.raii; 122 if (p.wait != 0) { 123 return Rval(Mutation.Status.killedByCompiler); 124 } 125 } catch (Exception e) { 126 logger.warning("Unknown error when executing the build command").collectException; 127 logger.warning(e.msg).collectException; 128 return Rval(Mutation.Status.unknown); 129 } 130 131 Rval rval; 132 try { 133 auto res = runner.run; 134 rval.output = res.output; 135 136 final switch (res.status) with (TestResult.Status) { 137 case passed: 138 rval.status = Mutation.Status.alive; 139 break; 140 case failed: 141 rval.status = Mutation.Status.killed; 142 break; 143 case timeout: 144 rval.status = Mutation.Status.timeout; 145 break; 146 case error: 147 rval.status = Mutation.Status.unknown; 148 break; 149 } 150 } catch (Exception e) { 151 // unable to for example execute the test suite 152 logger.warning(e.msg).collectException; 153 rval.status = Mutation.Status.unknown; 154 } 155 156 return rval; 157 } 158 159 struct MeasureTestDurationResult { 160 bool ok; 161 Duration runtime; 162 } 163 164 /** Measure the time it takes to run the test command. 165 * 166 * The runtime is the lowest of three executions. Anything else is assumed to 167 * be variations in the system. 168 * 169 * If the tests fail (exit code isn't 0) any time then they are too unreliable 170 * to use for mutation testing. 171 * 172 * Params: 173 * cmd = test command to measure 174 */ 175 MeasureTestDurationResult measureTestCommand(ref TestRunner runner) @safe nothrow { 176 import std.algorithm : min; 177 import std.datetime.stopwatch : StopWatch, AutoStart; 178 import std.stdio : writeln; 179 import process; 180 181 if (runner.empty) { 182 collectException(logger.error("No test command(s) specified (--test-cmd)")); 183 return MeasureTestDurationResult(false); 184 } 185 186 static struct Rval { 187 TestResult result; 188 Duration runtime; 189 } 190 191 auto runTest() @safe { 192 auto sw = StopWatch(AutoStart.yes); 193 auto res = runner.run; 194 return Rval(res, sw.peek); 195 } 196 197 static void print(DrainElement[] data) { 198 foreach (l; data) { 199 writeln(l.byUTF8); 200 } 201 } 202 203 auto runtime = Duration.max; 204 bool failed; 205 for (int i; i < 3 && !failed; ++i) { 206 try { 207 auto res = runTest; 208 final switch (res.result.status) with (TestResult) { 209 case Status.passed: 210 runtime = min(runtime, res.runtime); 211 break; 212 case Status.failed: 213 goto case; 214 case Status.timeout: 215 goto case; 216 case Status.error: 217 failed = true; 218 print(res.result.output); 219 break; 220 } 221 } catch (Exception e) { 222 logger.error(e.msg).collectException; 223 failed = true; 224 } 225 } 226 227 return MeasureTestDurationResult(!failed, runtime); 228 } 229 230 /** Drive the control flow when testing **a** mutant. 231 */ 232 struct MutationTestDriver { 233 import std.datetime.stopwatch : StopWatch; 234 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 235 236 static struct Global { 237 FilesysIO fio; 238 NullableRef!Database db; 239 240 /// Files that should be automatically removed after the testing is done is added here. 241 AutoCleanup auto_cleanup; 242 243 /// The mutant to apply. 244 MutationEntry mutp; 245 246 /// Runs the test commands. 247 TestRunner* runner; 248 249 /// File to mutate. 250 AbsolutePath mut_file; 251 /// The original file. 252 Blob original; 253 254 /// The result of running the test cases. 255 Mutation.Status mut_status; 256 257 /// Test cases that killed the mutant. 258 GatherTestCase test_cases; 259 260 /// How long it took to do the mutation testing. 261 StopWatch sw; 262 } 263 264 static struct TestMutantData { 265 /// If the user has configured that the test cases should be analyzed. 266 bool hasTestCaseOutputAnalyzer; 267 ShellCommand compile_cmd; 268 } 269 270 static struct TestCaseAnalyzeData { 271 //TODO: change to a ShellCommand 272 ShellCommand test_case_cmd; 273 const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin; 274 DrainElement[] output; 275 } 276 277 static struct None { 278 } 279 280 static struct Initialize { 281 } 282 283 static struct MutateCode { 284 bool next; 285 bool filesysError; 286 bool mutationError; 287 } 288 289 static struct TestMutant { 290 bool mutationError; 291 } 292 293 static struct RestoreCode { 294 bool next; 295 bool filesysError; 296 } 297 298 static struct TestCaseAnalyze { 299 bool mutationError; 300 bool unstableTests; 301 } 302 303 static struct StoreResult { 304 } 305 306 static struct Done { 307 } 308 309 static struct FilesysError { 310 } 311 312 // happens when an error occurs during mutations testing but that do not 313 // prohibit testing of other mutants 314 static struct NoResultRestoreCode { 315 } 316 317 static struct NoResult { 318 } 319 320 alias Fsm = dextool.fsm.Fsm!(None, Initialize, MutateCode, TestMutant, RestoreCode, 321 TestCaseAnalyze, StoreResult, Done, FilesysError, NoResultRestoreCode, NoResult); 322 Fsm fsm; 323 324 Global global; 325 MutationTestResult result; 326 327 alias LocalStateDataT = Tuple!(TestMutantData, TestCaseAnalyzeData); 328 TypeDataMap!(LocalStateDataT, TestMutant, TestCaseAnalyze) local; 329 330 this(Global global, TestMutantData l1, TestCaseAnalyzeData l2) { 331 this.global = global; 332 this.local = LocalStateDataT(l1, l2); 333 } 334 335 static void execute_(ref MutationTestDriver self) @trusted { 336 self.fsm.next!((None a) => fsm(Initialize.init), 337 (Initialize a) => fsm(MutateCode.init), (MutateCode a) { 338 if (a.next) 339 return fsm(TestMutant.init); 340 else if (a.filesysError) 341 return fsm(FilesysError.init); 342 else if (a.mutationError) 343 return fsm(NoResultRestoreCode.init); 344 return fsm(a); 345 }, (TestMutant a) { 346 if (a.mutationError) 347 return fsm(NoResultRestoreCode.init); 348 else if (self.global.mut_status == Mutation.Status.killed 349 && self.local.get!TestMutant.hasTestCaseOutputAnalyzer 350 && !self.local.get!TestCaseAnalyze.output.empty) 351 return fsm(TestCaseAnalyze.init); 352 return fsm(RestoreCode.init); 353 }, (TestCaseAnalyze a) { 354 if (a.mutationError || a.unstableTests) 355 return fsm(NoResultRestoreCode.init); 356 return fsm(RestoreCode.init); 357 }, (RestoreCode a) { 358 if (a.next) 359 return fsm(StoreResult.init); 360 else if (a.filesysError) 361 return fsm(FilesysError.init); 362 return fsm(a); 363 }, (StoreResult a) { return fsm(Done.init); }, (Done a) => fsm(a), 364 (FilesysError a) => fsm(a), 365 (NoResultRestoreCode a) => fsm(NoResult.init), (NoResult a) => fsm(a),); 366 367 self.fsm.act!(self); 368 } 369 370 nothrow: 371 372 void execute() { 373 try { 374 execute_(this); 375 } catch (Exception e) { 376 logger.warning(e.msg).collectException; 377 } 378 } 379 380 /// Returns: true as long as the driver is processing a mutant. 381 bool isRunning() { 382 return !fsm.isState!(Done, NoResult, FilesysError); 383 } 384 385 bool stopBecauseError() { 386 return fsm.isState!(FilesysError); 387 } 388 389 void opCall(None data) { 390 } 391 392 void opCall(Initialize data) { 393 global.sw.start; 394 } 395 396 void opCall(Done data) { 397 } 398 399 void opCall(FilesysError data) { 400 logger.warning("Filesystem error").collectException; 401 } 402 403 void opCall(NoResultRestoreCode data) { 404 RestoreCode tmp; 405 this.opCall(tmp); 406 } 407 408 void opCall(NoResult data) { 409 } 410 411 void opCall(ref MutateCode data) { 412 import std.random : uniform; 413 import dextool.plugin.mutate.backend.generate_mutant : generateMutant, 414 GenerateMutantResult, GenerateMutantStatus; 415 416 try { 417 global.mut_file = AbsolutePath(FileName(global.mutp.file), 418 DirName(global.fio.getOutputDir)); 419 global.original = global.fio.makeInput(global.mut_file); 420 } catch (Exception e) { 421 logger.error(e.msg).collectException; 422 logger.warning("Unable to read ", global.mut_file).collectException; 423 data.filesysError = true; 424 return; 425 } 426 427 // mutate 428 try { 429 auto fout = global.fio.makeOutput(global.mut_file); 430 auto mut_res = generateMutant(global.db.get, global.mutp, global.original, fout); 431 432 final switch (mut_res.status) with (GenerateMutantStatus) { 433 case error: 434 data.mutationError = true; 435 break; 436 case filesysError: 437 data.filesysError = true; 438 break; 439 case databaseError: 440 // such as when the database is locked 441 data.mutationError = true; 442 break; 443 case checksumError: 444 data.filesysError = true; 445 break; 446 case noMutation: 447 data.mutationError = true; 448 break; 449 case ok: 450 data.next = true; 451 try { 452 logger.infof("%s from '%s' to '%s' in %s:%s:%s", global.mutp.id, 453 cast(const(char)[]) mut_res.from, cast(const(char)[]) mut_res.to, 454 global.mut_file, global.mutp.sloc.line, global.mutp.sloc.column); 455 456 } catch (Exception e) { 457 logger.warning("Mutation ID", e.msg); 458 } 459 break; 460 } 461 } catch (Exception e) { 462 logger.warning(e.msg).collectException; 463 data.mutationError = true; 464 } 465 } 466 467 void opCall(ref TestMutant data) { 468 try { 469 auto res = runTester(local.get!TestMutant.compile_cmd, *global.runner); 470 global.mut_status = res.status; 471 local.get!TestCaseAnalyze.output = res.output; 472 } catch (Exception e) { 473 logger.warning(e.msg).collectException; 474 data.mutationError = true; 475 } 476 } 477 478 void opCall(ref TestCaseAnalyze data) { 479 try { 480 auto gather_tc = new GatherTestCase; 481 482 // the post processer must succeeed for the data to be stored. if 483 // is considered a major error that may corrupt existing data if it 484 // fails. 485 bool success = true; 486 487 if (!local.get!TestCaseAnalyze.test_case_cmd.empty) { 488 success = success && externalProgram(local.get!TestCaseAnalyze.test_case_cmd, 489 local.get!TestCaseAnalyze.output, gather_tc, global.auto_cleanup); 490 } 491 if (!local.get!TestCaseAnalyze.tc_analyze_builtin.empty) { 492 success = success && builtin(local.get!TestCaseAnalyze.output, 493 local.get!TestCaseAnalyze.tc_analyze_builtin, gather_tc); 494 } 495 496 if (!gather_tc.unstable.empty) { 497 logger.warningf("Unstable test cases found: [%-(%s, %)]", 498 gather_tc.unstableAsArray); 499 logger.info( 500 "As configured the result is ignored which will force the mutant to be re-tested"); 501 data.unstableTests = true; 502 } else if (success) { 503 global.test_cases = gather_tc; 504 } 505 } catch (Exception e) { 506 logger.warning(e.msg).collectException; 507 } 508 } 509 510 void opCall(StoreResult data) { 511 global.sw.stop; 512 auto failedTestCases = () { 513 if (global.test_cases is null) { 514 return null; 515 } 516 return global.test_cases.failedAsArray; 517 }(); 518 result = MutationTestResult.StatusUpdate(global.mutp.id, 519 global.mut_status, global.sw.peek, failedTestCases); 520 } 521 522 void opCall(ref RestoreCode data) { 523 // restore the original file. 524 try { 525 global.fio.makeOutput(global.mut_file).write(global.original.content); 526 } catch (Exception e) { 527 logger.error(e.msg).collectException; 528 // fatal error because being unable to restore a file prohibit 529 // future mutations. 530 data.filesysError = true; 531 return; 532 } 533 534 data.next = true; 535 } 536 } 537 538 struct TestDriver { 539 import std.datetime : SysTime; 540 import std.typecons : Unique; 541 import dextool.plugin.mutate.backend.test_mutant.timeout : calculateTimeout, TimeoutFsm; 542 543 /// Runs the test commands. 544 TestRunner runner; 545 546 static struct Global { 547 DriverData data; 548 Unique!MutationTestDriver mut_driver; 549 550 TimeoutFsm timeoutFsm; 551 /// The time it takes to execute the test suite when no mutant is injected. 552 Duration testSuiteRuntime; 553 554 /// the next mutant to test, if there are any. 555 MutationEntry nextMutant; 556 557 // when the user manually configure the timeout it means that the 558 // timeout algorithm should not be used. 559 bool hardcodedTimeout; 560 561 /// Max time to run the mutation testing for. 562 SysTime maxRuntime; 563 } 564 565 static struct UpdateTimeoutData { 566 long lastTimeoutIter; 567 } 568 569 static struct None { 570 } 571 572 static struct Initialize { 573 } 574 575 static struct PullRequest { 576 } 577 578 static struct PullRequestData { 579 import dextool.plugin.mutate.type : TestConstraint; 580 581 TestConstraint constraint; 582 } 583 584 static struct SanityCheck { 585 bool sanityCheckFailed; 586 } 587 588 static struct AnalyzeTestCmdForTestCase { 589 TestCase[] foundTestCases; 590 } 591 592 static struct UpdateAndResetAliveMutants { 593 TestCase[] foundTestCases; 594 } 595 596 static struct ResetOldMutant { 597 bool doneTestingOldMutants; 598 } 599 600 static struct ResetOldMutantData { 601 /// Number of mutants that where reset. 602 long resetCount; 603 long maxReset; 604 } 605 606 static struct CleanupTempDirs { 607 } 608 609 static struct CheckMutantsLeft { 610 bool allMutantsTested; 611 } 612 613 static struct ParseStdin { 614 } 615 616 static struct PreCompileSut { 617 bool compilationError; 618 } 619 620 static struct MeasureTestSuite { 621 bool unreliableTestSuite; 622 } 623 624 static struct PreMutationTest { 625 } 626 627 static struct MutationTest { 628 bool next; 629 bool mutationError; 630 MutationTestResult result; 631 } 632 633 static struct CheckTimeout { 634 bool timeoutUnchanged; 635 } 636 637 static struct Done { 638 } 639 640 static struct Error { 641 } 642 643 static struct UpdateTimeout { 644 } 645 646 static struct NextPullRequestMutant { 647 bool noUnknownMutantsLeft; 648 } 649 650 static struct NextPullRequestMutantData { 651 import dextool.plugin.mutate.backend.database : MutationStatusId; 652 653 MutationStatusId[] mutants; 654 655 /// If set then stop after this many alive are found. 656 Nullable!int maxAlive; 657 /// number of alive mutants that has been found. 658 int alive; 659 } 660 661 static struct NextMutant { 662 bool noUnknownMutantsLeft; 663 } 664 665 static struct HandleTestResult { 666 MutationTestResult result; 667 } 668 669 static struct CheckRuntime { 670 bool reachedMax; 671 } 672 673 static struct SetMaxRuntime { 674 } 675 676 alias Fsm = dextool.fsm.Fsm!(None, Initialize, SanityCheck, 677 AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, ResetOldMutant, 678 CleanupTempDirs, CheckMutantsLeft, PreCompileSut, MeasureTestSuite, 679 PreMutationTest, NextMutant, MutationTest, HandleTestResult, 680 CheckTimeout, Done, Error, UpdateTimeout, CheckRuntime, 681 SetMaxRuntime, PullRequest, NextPullRequestMutant, ParseStdin); 682 683 Fsm fsm; 684 685 Global global; 686 687 alias LocalStateDataT = Tuple!(UpdateTimeoutData, 688 NextPullRequestMutantData, PullRequestData, ResetOldMutantData); 689 TypeDataMap!(LocalStateDataT, UpdateTimeout, NextPullRequestMutant, 690 PullRequest, ResetOldMutant) local; 691 692 this(DriverData data) { 693 this.global = Global(data); 694 this.global.timeoutFsm = TimeoutFsm(data.mutKind); 695 this.global.hardcodedTimeout = !global.data.conf.mutationTesterRuntime.isNull; 696 local.get!PullRequest.constraint = global.data.conf.constraint; 697 local.get!NextPullRequestMutant.maxAlive = global.data.conf.maxAlive; 698 local.get!ResetOldMutant.maxReset = global.data.conf.oldMutantsNr; 699 700 this.runner = TestRunner.make; 701 // using an unreasonable timeout to make it possible to analyze for 702 // test cases and measure the test suite. 703 this.runner.timeout = 999.dur!"hours"; 704 this.runner.put(data.conf.mutationTester); 705 } 706 707 static void execute_(ref TestDriver self) @trusted { 708 // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a 709 // graphical view of the state machine. 710 711 self.fsm.next!((None a) => fsm(Initialize.init), 712 (Initialize a) => fsm(SanityCheck.init), (SanityCheck a) { 713 if (a.sanityCheckFailed) 714 return fsm(Error.init); 715 if (self.global.data.conf.unifiedDiffFromStdin) 716 return fsm(ParseStdin.init); 717 return fsm(PreCompileSut.init); 718 }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) => fsm( 719 UpdateAndResetAliveMutants(a.foundTestCases)), 720 (UpdateAndResetAliveMutants a) => fsm(CheckMutantsLeft.init), (ResetOldMutant a) { 721 if (a.doneTestingOldMutants) 722 return fsm(Done.init); 723 return fsm(UpdateTimeout.init); 724 }, (CleanupTempDirs a) { 725 if (self.local.get!PullRequest.constraint.empty) 726 return fsm(NextMutant.init); 727 return fsm(NextPullRequestMutant.init); 728 }, (CheckMutantsLeft a) { 729 if (a.allMutantsTested 730 && self.global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) 731 return fsm(Done.init); 732 return fsm(MeasureTestSuite.init); 733 }, (PreCompileSut a) { 734 if (a.compilationError) 735 return fsm(Error.init); 736 if (!self.local.get!PullRequest.constraint.empty) 737 return fsm(PullRequest.init); 738 if (!self.global.data.conf.mutationTestCaseAnalyze.empty 739 || !self.global.data.conf.mutationTestCaseBuiltin.empty) 740 return fsm(AnalyzeTestCmdForTestCase.init); 741 return fsm(CheckMutantsLeft.init); 742 }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) { 743 if (a.unreliableTestSuite) 744 return fsm(Error.init); 745 return fsm(SetMaxRuntime.init); 746 }, (SetMaxRuntime a) => fsm(UpdateTimeout.init), (NextPullRequestMutant a) { 747 if (a.noUnknownMutantsLeft) 748 return fsm(Done.init); 749 return fsm(PreMutationTest.init); 750 }, (NextMutant a) { 751 if (a.noUnknownMutantsLeft) 752 return fsm(CheckTimeout.init); 753 return fsm(PreMutationTest.init); 754 }, (PreMutationTest a) => fsm(MutationTest.init), 755 (UpdateTimeout a) => fsm(CleanupTempDirs.init), (MutationTest a) { 756 if (a.next) 757 return fsm(HandleTestResult(a.result)); 758 else if (a.mutationError) 759 return fsm(Error.init); 760 return fsm(a); 761 }, (HandleTestResult a) => fsm(CheckRuntime.init), (CheckRuntime a) { 762 if (a.reachedMax) 763 return fsm(Done.init); 764 return fsm(UpdateTimeout.init); 765 }, (CheckTimeout a) { 766 if (a.timeoutUnchanged) 767 return fsm(ResetOldMutant.init); 768 return fsm(UpdateTimeout.init); 769 }, (Done a) => fsm(a), (Error a) => fsm(a),); 770 771 self.fsm.act!(self); 772 } 773 774 nothrow: 775 void execute() { 776 try { 777 execute_(this); 778 } catch (Exception e) { 779 logger.warning(e.msg).collectException; 780 } 781 } 782 783 bool isRunning() { 784 return !fsm.isState!(Done, Error); 785 } 786 787 ExitStatusType status() { 788 if (fsm.isState!Done) 789 return ExitStatusType.Ok; 790 return ExitStatusType.Errors; 791 } 792 793 void opCall(None data) { 794 } 795 796 void opCall(Initialize data) { 797 } 798 799 void opCall(Done data) { 800 global.data.autoCleanup.cleanup; 801 802 logger.info("Done!").collectException; 803 } 804 805 void opCall(Error data) { 806 global.data.autoCleanup.cleanup; 807 } 808 809 void opCall(ref SanityCheck data) { 810 // #SPC-sanity_check_db_vs_filesys 811 import colorlog : color, Color; 812 import dextool.plugin.mutate.backend.utility : checksum, Checksum; 813 814 logger.info("Checking that the file(s) on the filesystem match the database") 815 .collectException; 816 817 auto failed = appender!(string[])(); 818 foreach (file; spinSql!(() { return global.data.db.getFiles; })) { 819 auto db_checksum = spinSql!(() { 820 return global.data.db.getFileChecksum(file); 821 }); 822 823 try { 824 auto abs_f = AbsolutePath(FileName(file), 825 DirName(cast(string) global.data.filesysIO.getOutputDir)); 826 auto f_checksum = checksum(global.data.filesysIO.makeInput(abs_f).content[]); 827 if (db_checksum != f_checksum) { 828 failed.put(abs_f); 829 } 830 } catch (Exception e) { 831 // assume it is a problem reading the file or something like that. 832 failed.put(file); 833 logger.warningf("%s: %s", file, e.msg).collectException; 834 } 835 } 836 837 data.sanityCheckFailed = failed.data.length != 0; 838 839 if (data.sanityCheckFailed) { 840 logger.error("Detected that file(s) has changed since last analyze where done") 841 .collectException; 842 logger.error("Either restore the file(s) or rerun the analyze").collectException; 843 foreach (f; failed.data) { 844 logger.info(f).collectException; 845 } 846 } else { 847 logger.info("Ok".color(Color.green)).collectException; 848 } 849 } 850 851 void opCall(ParseStdin data) { 852 import dextool.plugin.mutate.backend.diff_parser : diffFromStdin; 853 import dextool.plugin.mutate.type : Line; 854 855 try { 856 auto constraint = local.get!PullRequest.constraint; 857 foreach (pkv; diffFromStdin.toRange(global.data.filesysIO.getOutputDir)) { 858 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array; 859 } 860 local.get!PullRequest.constraint = constraint; 861 } catch (Exception e) { 862 logger.warning(e.msg).collectException; 863 } 864 } 865 866 void opCall(ref AnalyzeTestCmdForTestCase data) { 867 import std.datetime.stopwatch : StopWatch; 868 import dextool.plugin.mutate.backend.type : TestCase; 869 870 TestCase[] all_found_tc; 871 872 try { 873 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 874 875 auto res = runTester(global.data.conf.mutationCompile, runner); 876 877 auto gather_tc = new GatherTestCase; 878 879 if (!global.data.conf.mutationTestCaseAnalyze.empty) { 880 externalProgram(global.data.conf.mutationTestCaseAnalyze, 881 res.output, gather_tc, global.data.autoCleanup); 882 logger.warningf(gather_tc.unstable.length != 0, 883 "Unstable test cases found: [%-(%s, %)]", gather_tc.unstableAsArray); 884 } 885 if (!global.data.conf.mutationTestCaseBuiltin.empty) { 886 builtin(res.output, global.data.conf.mutationTestCaseBuiltin, gather_tc); 887 } 888 889 all_found_tc = gather_tc.foundAsArray; 890 } catch (Exception e) { 891 logger.warning(e.msg).collectException; 892 } 893 894 warnIfConflictingTestCaseIdentifiers(all_found_tc); 895 896 data.foundTestCases = all_found_tc; 897 } 898 899 void opCall(UpdateAndResetAliveMutants data) { 900 import std.traits : EnumMembers; 901 902 // the test cases before anything has potentially changed. 903 auto old_tcs = spinSql!(() { 904 Set!string old_tcs; 905 foreach (tc; global.data.db.getDetectedTestCases) { 906 old_tcs.add(tc.name); 907 } 908 return old_tcs; 909 }); 910 911 void transaction() @safe { 912 final switch (global.data.conf.onRemovedTestCases) with ( 913 ConfigMutationTest.RemovedTestCases) { 914 case doNothing: 915 global.data.db.addDetectedTestCases(data.foundTestCases); 916 break; 917 case remove: 918 foreach (id; global.data.db.setDetectedTestCases(data.foundTestCases)) { 919 global.data.db.updateMutationStatus(id, Mutation.Status.unknown); 920 } 921 break; 922 } 923 } 924 925 auto found_tcs = spinSql!(() @trusted { 926 auto tr = global.data.db.transaction; 927 transaction(); 928 929 Set!string found_tcs; 930 foreach (tc; global.data.db.getDetectedTestCases) { 931 found_tcs.add(tc.name); 932 } 933 934 tr.commit; 935 return found_tcs; 936 }); 937 938 printDroppedTestCases(old_tcs, found_tcs); 939 940 if (hasNewTestCases(old_tcs, found_tcs) 941 && global.data.conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) { 942 logger.info("Resetting alive mutants").collectException; 943 // there is no use in trying to limit the mutants to reset to those 944 // that are part of "this" execution because new test cases can 945 // only mean one thing: re-test all alive mutants. 946 spinSql!(() { 947 global.data.db.resetMutant([EnumMembers!(Mutation.Kind)], 948 Mutation.Status.alive, Mutation.Status.unknown); 949 }); 950 } 951 } 952 953 void opCall(ref ResetOldMutant data) { 954 import dextool.plugin.mutate.backend.database.type; 955 956 if (global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) { 957 data.doneTestingOldMutants = true; 958 return; 959 } 960 if (Clock.currTime > global.maxRuntime) { 961 data.doneTestingOldMutants = true; 962 return; 963 } 964 if (local.get!ResetOldMutant.resetCount >= local.get!ResetOldMutant.maxReset) { 965 data.doneTestingOldMutants = true; 966 return; 967 } 968 969 local.get!ResetOldMutant.resetCount++; 970 971 logger.infof("Resetting an old mutant (%s/%s)", local.get!ResetOldMutant.resetCount, 972 local.get!ResetOldMutant.maxReset).collectException; 973 auto oldest = spinSql!(() { 974 return global.data.db.getOldestMutants(global.data.mutKind, 1); 975 }); 976 977 foreach (const old; oldest) { 978 logger.info("Last updated ", old.updated).collectException; 979 spinSql!(() { 980 global.data.db.updateMutationStatus(old.id, Mutation.Status.unknown); 981 }); 982 } 983 } 984 985 void opCall(CleanupTempDirs data) { 986 global.data.autoCleanup.cleanup; 987 } 988 989 void opCall(ref CheckMutantsLeft data) { 990 spinSql!(() { global.timeoutFsm.execute(global.data.db); }); 991 992 data.allMutantsTested = global.timeoutFsm.output.done; 993 994 if (global.timeoutFsm.output.done) { 995 logger.info("All mutants are tested").collectException; 996 } 997 } 998 999 void opCall(ref PreCompileSut data) { 1000 import std.stdio : write; 1001 import colorlog : color, Color; 1002 import process; 1003 1004 logger.info("Checking the build command").collectException; 1005 try { 1006 auto output = appender!(DrainElement[])(); 1007 auto p = pipeProcess(global.data.conf.mutationCompile.value).sandbox.drain(output).raii; 1008 if (p.wait == 0) { 1009 logger.info("Ok".color(Color.green)); 1010 return; 1011 } 1012 1013 logger.error("Build commman failed"); 1014 foreach (l; output.data) { 1015 write(l.byUTF8); 1016 } 1017 } catch (Exception e) { 1018 // unable to for example execute the compiler 1019 logger.error(e.msg).collectException; 1020 } 1021 1022 data.compilationError = true; 1023 } 1024 1025 void opCall(PullRequest data) { 1026 import std.array : appender; 1027 import dextool.plugin.mutate.backend.database : MutationStatusId; 1028 import dextool.plugin.mutate.backend.type : SourceLoc; 1029 import dextool.set; 1030 1031 Set!MutationStatusId mut_ids; 1032 1033 foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) { 1034 const file_id = spinSql!(() => global.data.db.getFileId(kv.key)); 1035 if (file_id.isNull) { 1036 logger.infof("The file %s do not exist in the database. Skipping...", 1037 kv.key).collectException; 1038 continue; 1039 } 1040 1041 foreach (l; kv.value) { 1042 auto mutants = spinSql!(() { 1043 return global.data.db.getMutationsOnLine(global.data.mutKind, 1044 file_id.get, SourceLoc(l.value, 0)); 1045 }); 1046 1047 const pre_cnt = mut_ids.length; 1048 foreach (v; mutants) 1049 mut_ids.add(v); 1050 1051 logger.infof(mut_ids.length - pre_cnt > 0, "Found %s mutant(s) to test (%s:%s)", 1052 mut_ids.length - pre_cnt, kv.key, l.value).collectException; 1053 } 1054 } 1055 1056 logger.infof(!mut_ids.empty, "Found %s mutants in the diff", 1057 mut_ids.length).collectException; 1058 1059 local.get!NextPullRequestMutant.mutants = mut_ids.toArray; 1060 logger.trace(local.get!NextPullRequestMutant.mutants.sort).collectException; 1061 1062 if (mut_ids.empty) { 1063 logger.warning("None of the locations specified with -L exists").collectException; 1064 logger.info("Available files are:").collectException; 1065 foreach (f; spinSql!(() => global.data.db.getFiles)) 1066 logger.info(f).collectException; 1067 } 1068 } 1069 1070 void opCall(ref MeasureTestSuite data) { 1071 if (!global.data.conf.mutationTesterRuntime.isNull) { 1072 global.testSuiteRuntime = global.data.conf.mutationTesterRuntime.get; 1073 return; 1074 } 1075 1076 logger.info("Measuring the runtime of the test command: ", 1077 global.data.conf.mutationTester).collectException; 1078 const tester = measureTestCommand(runner); 1079 if (tester.ok) { 1080 // The sampling of the test suite become too unreliable when the timeout is <1s. 1081 // This is a quick and dirty fix. 1082 // A proper fix requires an update of the sampler in runTester. 1083 auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime; 1084 logger.info("Test command runtime: ", t).collectException; 1085 global.testSuiteRuntime = t; 1086 } else { 1087 data.unreliableTestSuite = true; 1088 logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected") 1089 .collectException; 1090 } 1091 } 1092 1093 void opCall(PreMutationTest) { 1094 auto factory(DriverData d, MutationEntry mutp, TestRunner* runner) @safe nothrow { 1095 import std.typecons : Unique; 1096 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 1097 1098 try { 1099 auto global = MutationTestDriver.Global(d.filesysIO, d.db, 1100 d.autoCleanup, mutp, runner); 1101 return Unique!MutationTestDriver(new MutationTestDriver(global, 1102 MutationTestDriver.TestMutantData(!(d.conf.mutationTestCaseAnalyze.empty 1103 && d.conf.mutationTestCaseBuiltin.empty), d.conf.mutationCompile,), 1104 MutationTestDriver.TestCaseAnalyzeData(d.conf.mutationTestCaseAnalyze, 1105 d.conf.mutationTestCaseBuiltin))); 1106 } catch (Exception e) { 1107 logger.error(e.msg).collectException; 1108 } 1109 assert(0, "should not happen"); 1110 } 1111 1112 runner.timeout = calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime); 1113 global.mut_driver = factory(global.data, global.nextMutant, () @trusted { 1114 return &runner; 1115 }()); 1116 } 1117 1118 void opCall(ref MutationTest data) { 1119 if (global.mut_driver.isRunning) { 1120 global.mut_driver.execute(); 1121 } else if (global.mut_driver.stopBecauseError) { 1122 data.mutationError = true; 1123 } else { 1124 data.result = global.mut_driver.result; 1125 data.next = true; 1126 } 1127 } 1128 1129 void opCall(ref CheckTimeout data) { 1130 data.timeoutUnchanged = global.hardcodedTimeout || global.timeoutFsm.output.done; 1131 } 1132 1133 void opCall(UpdateTimeout) { 1134 spinSql!(() { global.timeoutFsm.execute(global.data.db); }); 1135 1136 const lastIter = local.get!UpdateTimeout.lastTimeoutIter; 1137 1138 if (lastIter != global.timeoutFsm.output.iter) { 1139 logger.infof("Changed the timeout from %s to %s (iteration %s)", 1140 calculateTimeout(lastIter, global.testSuiteRuntime), 1141 calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime), 1142 global.timeoutFsm.output.iter).collectException; 1143 local.get!UpdateTimeout.lastTimeoutIter = global.timeoutFsm.output.iter; 1144 } 1145 } 1146 1147 void opCall(ref NextPullRequestMutant data) { 1148 global.nextMutant = MutationEntry.init; 1149 data.noUnknownMutantsLeft = true; 1150 1151 while (!local.get!NextPullRequestMutant.mutants.empty) { 1152 const id = local.get!NextPullRequestMutant.mutants[$ - 1]; 1153 const status = spinSql!(() => global.data.db.getMutationStatus(id)); 1154 1155 if (status.isNull) 1156 continue; 1157 1158 if (status.get == Mutation.Status.alive) { 1159 local.get!NextPullRequestMutant.alive++; 1160 } 1161 1162 if (status.get != Mutation.Status.unknown) { 1163 local.get!NextPullRequestMutant.mutants 1164 = local.get!NextPullRequestMutant.mutants[0 .. $ - 1]; 1165 continue; 1166 } 1167 1168 const info = spinSql!(() => global.data.db.getMutantsInfo(global.data.mutKind, [ 1169 id 1170 ])); 1171 if (info.empty) 1172 continue; 1173 1174 global.nextMutant = spinSql!(() => global.data.db.getMutation(info[0].id)); 1175 data.noUnknownMutantsLeft = false; 1176 break; 1177 } 1178 1179 if (!local.get!NextPullRequestMutant.maxAlive.isNull) { 1180 const alive = local.get!NextPullRequestMutant.alive; 1181 const maxAlive = local.get!NextPullRequestMutant.maxAlive.get; 1182 logger.infof(alive > 0, "Found %s/%s alive mutants", alive, maxAlive).collectException; 1183 if (alive >= maxAlive) { 1184 data.noUnknownMutantsLeft = true; 1185 } 1186 } 1187 } 1188 1189 void opCall(ref NextMutant data) { 1190 global.nextMutant = MutationEntry.init; 1191 1192 auto next = spinSql!(() { 1193 return global.data.db.nextMutation(global.data.mutKind); 1194 }); 1195 1196 data.noUnknownMutantsLeft = next.st == NextMutationEntry.Status.done; 1197 1198 if (!next.entry.isNull) { 1199 global.nextMutant = next.entry.get; 1200 } 1201 } 1202 1203 void opCall(HandleTestResult data) { 1204 void statusUpdate(MutationTestResult.StatusUpdate result) { 1205 import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus; 1206 1207 const cnt_action = () { 1208 if (result.status == Mutation.Status.alive) 1209 return Database.CntAction.incr; 1210 return Database.CntAction.reset; 1211 }(); 1212 1213 auto statusId = spinSql!(() { 1214 return global.data.db.getMutationStatusId(result.id); 1215 }); 1216 if (statusId.isNull) 1217 return; 1218 1219 spinSql!(() @trusted { 1220 auto t = global.data.db.transaction; 1221 updateMutantStatus(global.data.db, statusId.get, result.status, 1222 global.timeoutFsm.output.iter); 1223 global.data.db.updateMutation(statusId.get, cnt_action); 1224 global.data.db.updateMutation(statusId.get, result.testTime); 1225 global.data.db.updateMutationTestCases(statusId.get, result.testCases); 1226 t.commit; 1227 }); 1228 1229 logger.infof("%s %s (%s)", result.id, result.status, result.testTime).collectException; 1230 logger.infof(!result.testCases.empty, `%s killed by [%-(%s, %)]`, 1231 result.id, result.testCases.sort.map!"a.name").collectException; 1232 } 1233 1234 data.result.value.match!((MutationTestResult.NoResult a) {}, 1235 (MutationTestResult.StatusUpdate a) => statusUpdate(a)); 1236 } 1237 1238 void opCall(SetMaxRuntime) { 1239 global.maxRuntime = Clock.currTime + global.data.conf.maxRuntime; 1240 } 1241 1242 void opCall(ref CheckRuntime data) { 1243 data.reachedMax = Clock.currTime > global.maxRuntime; 1244 if (data.reachedMax) { 1245 logger.infof("Max runtime of %s reached at %s", 1246 global.data.conf.maxRuntime, global.maxRuntime).collectException; 1247 } 1248 } 1249 } 1250 1251 private: 1252 1253 /** Run an external program that analyze the output from the test suite for 1254 * test cases that failed. 1255 * 1256 * Params: 1257 * cmd = user analyze command to execute on the output 1258 * output = output from the test command to be passed on to the analyze command 1259 * report = the result is stored in the report 1260 * 1261 * Returns: True if it successfully analyzed the output 1262 */ 1263 bool externalProgram(ShellCommand cmd, DrainElement[] output, 1264 TestCaseReport report, AutoCleanup cleanup) @safe nothrow { 1265 import std.algorithm : copy; 1266 import std.ascii : newline; 1267 import std.string : strip, startsWith; 1268 import process; 1269 1270 immutable passed = "passed:"; 1271 immutable failed = "failed:"; 1272 immutable unstable = "unstable:"; 1273 1274 auto tmpdir = createTmpDir(); 1275 if (tmpdir.empty) { 1276 return false; 1277 } 1278 1279 ShellCommand writeOutput(ShellCommand cmd) @safe { 1280 import std.stdio : File; 1281 1282 const stdoutPath = buildPath(tmpdir, "stdout.log"); 1283 const stderrPath = buildPath(tmpdir, "stderr.log"); 1284 auto stdout = File(stdoutPath, "w"); 1285 auto stderr = File(stderrPath, "w"); 1286 1287 foreach (a; output) { 1288 final switch (a.type) { 1289 case DrainElement.Type.stdout: 1290 stdout.write(a.data); 1291 break; 1292 case DrainElement.Type.stderr: 1293 stderr.write(a.data); 1294 break; 1295 } 1296 } 1297 1298 cmd.value ~= [stdoutPath, stderrPath]; 1299 return cmd; 1300 } 1301 1302 try { 1303 cleanup.add(tmpdir.Path.AbsolutePath); 1304 cmd = writeOutput(cmd); 1305 auto p = pipeProcess(cmd.value).sandbox.raii; 1306 foreach (l; p.drainByLineCopy 1307 .map!(a => a.strip) 1308 .filter!(a => !a.empty)) { 1309 if (l.startsWith(passed)) 1310 report.reportFound(TestCase(l[passed.length .. $].strip.idup)); 1311 else if (l.startsWith(failed)) 1312 report.reportFailed(TestCase(l[failed.length .. $].strip.idup)); 1313 else if (l.startsWith(unstable)) 1314 report.reportUnstable(TestCase(l[unstable.length .. $].strip.idup)); 1315 } 1316 1317 if (p.wait == 0) { 1318 return true; 1319 } 1320 1321 logger.warningf("Failed to analyze the test case output with command '%-(%s %)'", cmd); 1322 } catch (Exception e) { 1323 logger.warning(e.msg).collectException; 1324 } 1325 1326 return false; 1327 } 1328 1329 /** Analyze the output from the test suite with one of the builtin analyzers. 1330 */ 1331 bool builtin(DrainElement[] output, 1332 const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @safe nothrow { 1333 import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze; 1334 import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze; 1335 import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze; 1336 1337 GtestParser gtest; 1338 CtestParser ctest; 1339 MakefileParser makefile; 1340 1341 void analyzeLine(const(char)[] line) { 1342 // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this? 1343 immutable magic_nr = 2048; 1344 if (line.length > magic_nr) { 1345 // The byLine split may fail and thus result in one huge line. 1346 // The result of this is that regex's that use backtracking become really slow. 1347 // By skipping these lines dextool at list doesn't hang. 1348 logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...", 1349 line.length, magic_nr); 1350 return; 1351 } 1352 1353 foreach (const p; tc_analyze_builtin) { 1354 final switch (p) { 1355 case TestCaseAnalyzeBuiltin.gtest: 1356 gtest.process(line, app); 1357 break; 1358 case TestCaseAnalyzeBuiltin.ctest: 1359 ctest.process(line, app); 1360 break; 1361 case TestCaseAnalyzeBuiltin.makefile: 1362 makefile.process(line, app); 1363 break; 1364 } 1365 } 1366 } 1367 1368 const(char)[] buf; 1369 void parseLine() { 1370 import std.algorithm : countUntil; 1371 1372 try { 1373 const idx = buf.countUntil('\n'); 1374 if (idx != -1) { 1375 analyzeLine(buf[0 .. idx]); 1376 if (idx < buf.length) { 1377 buf = buf[idx + 1 .. $]; 1378 } else { 1379 buf = null; 1380 } 1381 } 1382 } catch (Exception e) { 1383 logger.warning("A error encountered when trying to analyze the output from the test suite. Dumping the rest of the buffer") 1384 .collectException; 1385 1386 logger.warning(e.msg).collectException; 1387 buf = null; 1388 } 1389 } 1390 1391 foreach (d; output.map!(a => a.byUTF8.array)) { 1392 buf ~= d; 1393 parseLine; 1394 } 1395 while (!buf.empty) { 1396 parseLine; 1397 } 1398 1399 return true; 1400 } 1401 1402 /// Returns: path to a tmp directory or null on failure. 1403 string createTmpDir() @safe nothrow { 1404 import std.random : uniform; 1405 import std.format : format; 1406 import std.file : mkdir; 1407 1408 string test_tmp_output; 1409 1410 // try 5 times or bailout 1411 foreach (const _; 0 .. 5) { 1412 try { 1413 auto tmp = format!"dextool_tmp_id_%s"(uniform!ulong); 1414 mkdir(tmp); 1415 test_tmp_output = AbsolutePath(FileName(tmp)); 1416 break; 1417 } catch (Exception e) { 1418 logger.warning(e.msg).collectException; 1419 } 1420 } 1421 1422 if (test_tmp_output.length == 0) { 1423 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 1424 .collectException; 1425 } 1426 1427 return test_tmp_output; 1428 } 1429 1430 /** Compare the old test cases with those that have been found this run. 1431 * 1432 * TODO: the side effect that this function print to the console is NOT good. 1433 */ 1434 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow { 1435 bool rval; 1436 1437 auto new_tcs = found_tcs.setDifference(old_tcs); 1438 foreach (tc; new_tcs.toRange) { 1439 logger.info(!rval, "Found new test case(s):").collectException; 1440 logger.infof("%s", tc).collectException; 1441 rval = true; 1442 } 1443 1444 return rval; 1445 } 1446 1447 /** Compare old and new test cases to print those that have been removed. 1448 */ 1449 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow { 1450 auto diff = old_tcs.setDifference(changed_tcs); 1451 auto removed = diff.toArray; 1452 1453 logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException; 1454 foreach (tc; removed) { 1455 logger.infof("%s", tc).collectException; 1456 } 1457 } 1458 1459 /// Returns: true if all tests cases have unique identifiers 1460 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow { 1461 Set!TestCase checked; 1462 bool conflict; 1463 1464 foreach (tc; found_tcs) { 1465 if (checked.contains(tc)) { 1466 logger.info(!conflict, 1467 "Found test cases that do not have global, unique identifiers") 1468 .collectException; 1469 logger.info(!conflict, 1470 "This make the report of test cases that has killed zero mutants unreliable") 1471 .collectException; 1472 logger.info("%s", tc).collectException; 1473 conflict = true; 1474 } 1475 } 1476 } 1477 1478 /** Paths stored will be removed automatically either when manually called or goes out of scope. 1479 */ 1480 class AutoCleanup { 1481 private string[] remove_dirs; 1482 1483 void add(AbsolutePath p) @safe nothrow { 1484 remove_dirs ~= cast(string) p; 1485 } 1486 1487 // trusted: the paths are forced to be valid paths. 1488 void cleanup() @trusted nothrow { 1489 import std.file : rmdirRecurse, exists; 1490 1491 foreach (ref p; remove_dirs.filter!(a => !a.empty)) { 1492 try { 1493 if (exists(p)) 1494 rmdirRecurse(p); 1495 if (!exists(p)) 1496 p = null; 1497 } catch (Exception e) { 1498 logger.info(e.msg).collectException; 1499 } 1500 } 1501 1502 remove_dirs = remove_dirs.filter!(a => !a.empty).array; 1503 } 1504 } 1505 1506 /// The result of testing a mutant. 1507 struct MutationTestResult { 1508 import process : DrainElement; 1509 1510 static struct NoResult { 1511 } 1512 1513 static struct StatusUpdate { 1514 MutationId id; 1515 Mutation.Status status; 1516 Duration testTime; 1517 TestCase[] testCases; 1518 DrainElement[] output; 1519 } 1520 1521 alias Value = SumType!(NoResult, StatusUpdate); 1522 Value value; 1523 1524 void opAssign(MutationTestResult rhs) @trusted pure nothrow @nogc { 1525 this.value = rhs.value; 1526 } 1527 1528 void opAssign(StatusUpdate rhs) @trusted pure nothrow @nogc { 1529 this.value = Value(rhs); 1530 } 1531 }