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