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 std.datetime : SysTime; 15 import std.typecons : Nullable, NullableRef, nullableRef; 16 import std.exception : collectException; 17 18 import logger = std.experimental.logger; 19 20 import blob_model : Blob, Uri; 21 22 import dextool.plugin.mutate.backend.database : Database, MutationEntry, 23 NextMutationEntry, spinSqlQuery; 24 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 25 import dextool.plugin.mutate.backend.type : Mutation; 26 import dextool.plugin.mutate.config; 27 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin; 28 import dextool.type : AbsolutePath, ShellCommand, ExitStatusType, FileName, DirName; 29 30 @safe: 31 32 auto makeTestMutant() { 33 return BuildTestMutant(); 34 } 35 36 private: 37 38 struct BuildTestMutant { 39 @safe: 40 nothrow: 41 42 import dextool.plugin.mutate.type : MutationKind; 43 44 private struct InternalData { 45 Mutation.Kind[] mut_kinds; 46 FilesysIO filesys_io; 47 ConfigMutationTest config; 48 } 49 50 private InternalData data; 51 52 auto config(ConfigMutationTest c) { 53 data.config = c; 54 return this; 55 } 56 57 auto mutations(MutationKind[] v) { 58 import dextool.plugin.mutate.backend.utility : toInternal; 59 60 data.mut_kinds = toInternal(v); 61 return this; 62 } 63 64 ExitStatusType run(ref Database db, FilesysIO fio) nothrow { 65 auto mutationFactory(DriverData data, Duration test_base_timeout) @safe { 66 import std.typecons : Unique; 67 68 static struct Rval { 69 ImplMutationDriver impl; 70 MutationTestDriver!(ImplMutationDriver*) driver; 71 72 this(DriverData d, Duration test_base_timeout) { 73 this.impl = ImplMutationDriver(d.filesysIO, d.db, d.autoCleanup, 74 d.mutKind, d.conf.mutationCompile, d.conf.mutationTester, d.conf.mutationTestCaseAnalyze, 75 d.conf.mutationTestCaseBuiltin, test_base_timeout); 76 77 this.driver = MutationTestDriver!(ImplMutationDriver*)(() @trusted { 78 return &impl; 79 }()); 80 } 81 82 alias driver this; 83 } 84 85 return Unique!Rval(new Rval(data, test_base_timeout)); 86 } 87 88 // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope 89 auto db_ref = () @trusted { return nullableRef(&db); }(); 90 91 auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config); 92 93 auto test_driver_impl = ImplTestDriver!mutationFactory(driver_data); 94 auto test_driver_impl_ref = () @trusted { 95 return nullableRef(&test_driver_impl); 96 }(); 97 auto test_driver = TestDriver!(typeof(test_driver_impl_ref))(test_driver_impl_ref); 98 99 while (test_driver.isRunning) { 100 test_driver.execute; 101 } 102 103 return test_driver.status; 104 } 105 } 106 107 immutable stdoutLog = "stdout.log"; 108 immutable stderrLog = "stderr.log"; 109 110 struct DriverData { 111 NullableRef!Database db; 112 FilesysIO filesysIO; 113 Mutation.Kind[] mutKind; 114 AutoCleanup autoCleanup; 115 ConfigMutationTest conf; 116 } 117 118 /** Run the test suite to verify a mutation. 119 * 120 * Params: 121 * p = ? 122 * timeout = timeout threshold. 123 */ 124 Mutation.Status runTester(WatchdogT)(ShellCommand compile_p, ShellCommand tester_p, 125 AbsolutePath test_output_dir, WatchdogT watchdog, FilesysIO fio) nothrow { 126 import std.algorithm : among; 127 import std.datetime.stopwatch : StopWatch; 128 import dextool.plugin.mutate.backend.linux_process : spawnSession, tryWait, kill, wait; 129 import std.stdio : File; 130 import core.sys.posix.signal : SIGKILL; 131 import dextool.plugin.mutate.backend.utility : rndSleep; 132 133 Mutation.Status rval; 134 135 try { 136 auto p = spawnSession(compile_p.program ~ compile_p.arguments); 137 auto res = p.wait; 138 if (res.terminated && res.status != 0) 139 return Mutation.Status.killedByCompiler; 140 else if (!res.terminated) { 141 logger.warning("unknown error when executing the compiler").collectException; 142 return Mutation.Status.unknown; 143 } 144 } catch (Exception e) { 145 logger.warning(e.msg).collectException; 146 } 147 148 string stdout_p; 149 string stderr_p; 150 151 if (test_output_dir.length != 0) { 152 import std.path : buildPath; 153 154 stdout_p = buildPath(test_output_dir, stdoutLog); 155 stderr_p = buildPath(test_output_dir, stderrLog); 156 } 157 158 try { 159 auto p = spawnSession(tester_p.program ~ tester_p.arguments, stdout_p, stderr_p); 160 // trusted: killing the process started in this scope 161 void cleanup() @safe nothrow { 162 import core.sys.posix.signal : SIGKILL; 163 164 if (rval.among(Mutation.Status.timeout, Mutation.Status.unknown)) { 165 kill(p, SIGKILL); 166 wait(p); 167 } 168 } 169 170 scope (exit) 171 cleanup; 172 173 rval = Mutation.Status.timeout; 174 watchdog.start; 175 while (watchdog.isOk) { 176 auto res = tryWait(p); 177 if (res.terminated) { 178 if (res.status == 0) 179 rval = Mutation.Status.alive; 180 else 181 rval = Mutation.Status.killed; 182 break; 183 } 184 185 rndSleep(10.dur!"msecs", 50); 186 } 187 } catch (Exception e) { 188 // unable to for example execute the test suite 189 logger.warning(e.msg).collectException; 190 return Mutation.Status.unknown; 191 } 192 193 return rval; 194 } 195 196 struct MeasureTestDurationResult { 197 ExitStatusType status; 198 Duration runtime; 199 } 200 201 /** 202 * If the tests fail (exit code isn't 0) any time then they are too unreliable 203 * to use for mutation testing. 204 * 205 * The runtime is the lowest of the three executions. 206 * 207 * Params: 208 * p = ? 209 */ 210 MeasureTestDurationResult measureTesterDuration(ShellCommand cmd) nothrow { 211 if (cmd.program.length == 0) { 212 collectException(logger.error("No test suite runner specified (--mutant-tester)")); 213 return MeasureTestDurationResult(ExitStatusType.Errors); 214 } 215 216 auto any_failure = ExitStatusType.Ok; 217 218 void fun() { 219 import std.process : execute; 220 221 auto res = execute(cmd.program ~ cmd.arguments); 222 if (res.status != 0) 223 any_failure = ExitStatusType.Errors; 224 } 225 226 import std.datetime.stopwatch : benchmark; 227 import std.algorithm : minElement, map; 228 import core.time : dur; 229 230 try { 231 auto bench = benchmark!fun(3); 232 233 if (any_failure != ExitStatusType.Ok) 234 return MeasureTestDurationResult(ExitStatusType.Errors); 235 236 auto a = (cast(long)((bench[0].total!"msecs") / 3.0)).dur!"msecs"; 237 return MeasureTestDurationResult(ExitStatusType.Ok, a); 238 } catch (Exception e) { 239 collectException(logger.error(e.msg)); 240 return MeasureTestDurationResult(ExitStatusType.Errors); 241 } 242 } 243 244 enum MutationDriverSignal { 245 /// stay in the current state 246 stop, 247 /// advance to the next state 248 next, 249 /// All mutants are tested. Stopping mutation testing 250 allMutantsTested, 251 /// An error occured when interacting with the filesystem (fatal). Stopping all mutation testing 252 filesysError, 253 /// An error for a single mutation. It is skipped. 254 mutationError, 255 /// The test suite is unreliable which mean the mutant should be re-tested. 256 unstableTests, 257 } 258 259 /** Drive the control flow when testing **a** mutant. 260 * 261 * The architecture assume that there will be behavior changes therefore a 262 * strict FSM that separate the context, action and next_state. 263 * 264 * The intention is to separate the control flow from the implementation of the 265 * actions that are done when mutation testing. 266 */ 267 struct MutationTestDriver(ImplT) { 268 import std.experimental.typecons : Final; 269 270 /// The internal state of the FSM. 271 private enum State { 272 none, 273 initialize, 274 mutateCode, 275 testMutant, 276 restoreCode, 277 testCaseAnalyze, 278 storeResult, 279 done, 280 allMutantsTested, 281 filesysError, 282 /// happens when an error occurs during mutations testing but that do not prohibit testing of other mutants 283 noResultRestoreCode, 284 noResult, 285 } 286 287 private { 288 State st; 289 ImplT impl; 290 } 291 292 this(ImplT impl) { 293 this.impl = impl; 294 } 295 296 /// Returns: true as long as the driver is processing a mutant. 297 bool isRunning() { 298 import std.algorithm : among; 299 300 return st.among(State.done, State.noResult, State.filesysError, State.allMutantsTested) == 0; 301 } 302 303 bool stopBecauseError() { 304 return st == State.filesysError; 305 } 306 307 /// Returns: true when the mutation testing should be stopped 308 bool stopMutationTesting() { 309 return st == State.allMutantsTested; 310 } 311 312 void execute() { 313 import dextool.fsm : makeCallbacks; 314 315 const auto signal = impl.signal; 316 317 debug auto old_st = st; 318 319 st = nextState(st, signal); 320 321 debug logger.trace(old_st, "->", st, ":", signal).collectException; 322 323 mixin(makeCallbacks!State().switchOn("st").callbackOn("impl").finalize); 324 } 325 326 private static State nextState(immutable State current, immutable MutationDriverSignal signal) @safe pure nothrow @nogc { 327 State next_ = current; 328 329 final switch (current) { 330 case State.none: 331 next_ = State.initialize; 332 break; 333 case State.initialize: 334 if (signal == MutationDriverSignal.next) 335 next_ = State.mutateCode; 336 break; 337 case State.mutateCode: 338 if (signal == MutationDriverSignal.next) 339 next_ = State.testMutant; 340 else if (signal == MutationDriverSignal.allMutantsTested) 341 next_ = State.allMutantsTested; 342 else if (signal == MutationDriverSignal.filesysError) 343 next_ = State.filesysError; 344 else if (signal == MutationDriverSignal.mutationError) 345 next_ = State.noResultRestoreCode; 346 break; 347 case State.testMutant: 348 if (signal == MutationDriverSignal.next) 349 next_ = State.testCaseAnalyze; 350 else if (signal == MutationDriverSignal.mutationError) 351 next_ = State.noResultRestoreCode; 352 else if (signal == MutationDriverSignal.allMutantsTested) 353 next_ = State.allMutantsTested; 354 break; 355 case State.testCaseAnalyze: 356 if (signal == MutationDriverSignal.next) 357 next_ = State.restoreCode; 358 else if (signal == MutationDriverSignal.mutationError) 359 next_ = State.noResultRestoreCode; 360 else if (signal == MutationDriverSignal.unstableTests) 361 next_ = State.noResultRestoreCode; 362 break; 363 case State.restoreCode: 364 if (signal == MutationDriverSignal.next) 365 next_ = State.storeResult; 366 else if (signal == MutationDriverSignal.filesysError) 367 next_ = State.filesysError; 368 break; 369 case State.storeResult: 370 if (signal == MutationDriverSignal.next) 371 next_ = State.done; 372 break; 373 case State.done: 374 break; 375 case State.allMutantsTested: 376 break; 377 case State.filesysError: 378 break; 379 case State.noResultRestoreCode: 380 next_ = State.noResult; 381 break; 382 case State.noResult: 383 break; 384 } 385 386 return next_; 387 } 388 } 389 390 /** Implementation of the actions during the test of a mutant. 391 * 392 * The intention is that this driver do NOT control the flow. 393 */ 394 struct ImplMutationDriver { 395 import std.datetime.stopwatch : StopWatch; 396 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 397 398 nothrow: 399 400 FilesysIO fio; 401 NullableRef!Database db; 402 403 StopWatch sw; 404 MutationDriverSignal driver_sig; 405 406 Nullable!MutationEntry mutp; 407 AbsolutePath mut_file; 408 Blob original; 409 410 const(Mutation.Kind)[] mut_kind; 411 const TestCaseAnalyzeBuiltin[] tc_analyze_builtin; 412 413 ShellCommand compile_cmd; 414 ShellCommand test_cmd; 415 AbsolutePath test_case_cmd; 416 Duration tester_runtime; 417 418 /// Temporary directory where stdout/stderr should be written. 419 AbsolutePath test_tmp_output; 420 421 Mutation.Status mut_status; 422 423 GatherTestCase test_cases; 424 425 AutoCleanup auto_cleanup; 426 427 this(FilesysIO fio, NullableRef!Database db, AutoCleanup auto_cleanup, 428 Mutation.Kind[] mut_kind, ShellCommand compile_cmd, 429 ShellCommand test_cmd, AbsolutePath test_case_cmd, 430 TestCaseAnalyzeBuiltin[] tc_analyze_builtin, Duration tester_runtime) { 431 this.fio = fio; 432 this.db = db; 433 this.mut_kind = mut_kind; 434 this.compile_cmd = compile_cmd; 435 this.test_cmd = test_cmd; 436 this.test_case_cmd = test_case_cmd; 437 this.tc_analyze_builtin = tc_analyze_builtin; 438 this.tester_runtime = tester_runtime; 439 this.test_cases = new GatherTestCase; 440 this.auto_cleanup = auto_cleanup; 441 } 442 443 void none() { 444 } 445 446 void done() { 447 } 448 449 void allMutantsTested() { 450 } 451 452 void filesysError() { 453 logger.warning("Filesystem error").collectException; 454 } 455 456 void noResultRestoreCode() { 457 restoreCode; 458 } 459 460 void noResult() { 461 } 462 463 void initialize() { 464 sw.start; 465 driver_sig = MutationDriverSignal.next; 466 } 467 468 void mutateCode() { 469 import core.thread : Thread; 470 import std.random : uniform; 471 import dextool.plugin.mutate.backend.generate_mutant : generateMutant, 472 GenerateMutantResult, GenerateMutantStatus; 473 474 driver_sig = MutationDriverSignal.stop; 475 476 auto next_m = spinSqlQuery!(() { return db.nextMutation(mut_kind); }); 477 if (next_m.st == NextMutationEntry.Status.done) { 478 logger.info("Done! All mutants are tested").collectException; 479 driver_sig = MutationDriverSignal.allMutantsTested; 480 return; 481 } else { 482 mutp = next_m.entry; 483 } 484 485 try { 486 mut_file = AbsolutePath(FileName(mutp.file), DirName(fio.getOutputDir)); 487 original = fio.makeInput(mut_file); 488 } catch (Exception e) { 489 logger.error(e.msg).collectException; 490 logger.warning("Unable to read ", mut_file).collectException; 491 driver_sig = MutationDriverSignal.filesysError; 492 return; 493 } 494 495 // mutate 496 try { 497 auto fout = fio.makeOutput(mut_file); 498 auto mut_res = generateMutant(db.get, mutp, original, fout); 499 500 final switch (mut_res.status) with (GenerateMutantStatus) { 501 case error: 502 driver_sig = MutationDriverSignal.mutationError; 503 break; 504 case filesysError: 505 driver_sig = MutationDriverSignal.filesysError; 506 break; 507 case databaseError: 508 // such as when the database is locked 509 driver_sig = MutationDriverSignal.mutationError; 510 break; 511 case checksumError: 512 driver_sig = MutationDriverSignal.filesysError; 513 break; 514 case noMutation: 515 driver_sig = MutationDriverSignal.mutationError; 516 break; 517 case ok: 518 driver_sig = MutationDriverSignal.next; 519 try { 520 logger.infof("%s from '%s' to '%s' in %s:%s:%s", mutp.id, 521 cast(const(char)[]) mut_res.from, cast(const(char)[]) mut_res.to, 522 mut_file, mutp.sloc.line, mutp.sloc.column); 523 524 } catch (Exception e) { 525 logger.warning("Mutation ID", e.msg); 526 } 527 break; 528 } 529 } catch (Exception e) { 530 logger.warning(e.msg).collectException; 531 driver_sig = MutationDriverSignal.mutationError; 532 } 533 } 534 535 void testMutant() { 536 import dextool.type : Path; 537 538 assert(!mutp.isNull); 539 driver_sig = MutationDriverSignal.mutationError; 540 541 if (test_case_cmd.length != 0 || tc_analyze_builtin.length != 0) { 542 try { 543 auto tmpdir = createTmpDir(mutp.id); 544 if (tmpdir.length == 0) 545 return; 546 test_tmp_output = Path(tmpdir).AbsolutePath; 547 auto_cleanup.add(test_tmp_output); 548 } catch (Exception e) { 549 logger.warning(e.msg).collectException; 550 return; 551 } 552 } 553 554 try { 555 import dextool.plugin.mutate.backend.watchdog : StaticTime; 556 557 auto watchdog = StaticTime!StopWatch(tester_runtime); 558 559 mut_status = runTester(compile_cmd, test_cmd, test_tmp_output, watchdog, fio); 560 driver_sig = MutationDriverSignal.next; 561 } catch (Exception e) { 562 logger.warning(e.msg).collectException; 563 } 564 } 565 566 void testCaseAnalyze() { 567 import std.algorithm : splitter, map, filter; 568 import std.array : array; 569 import std.ascii : newline; 570 import std.file : exists; 571 import std.path : buildPath; 572 import std.process : execute; 573 import std.string : strip; 574 575 if (mut_status != Mutation.Status.killed || test_tmp_output.length == 0) { 576 driver_sig = MutationDriverSignal.next; 577 return; 578 } 579 580 driver_sig = MutationDriverSignal.mutationError; 581 582 try { 583 auto stdout_ = buildPath(test_tmp_output, stdoutLog); 584 auto stderr_ = buildPath(test_tmp_output, stderrLog); 585 586 if (!exists(stdout_) || !exists(stderr_)) { 587 logger.warningf("Unable to open %s and %s for test case analyze", stdout_, stderr_); 588 return; 589 } 590 591 auto gather_tc = new GatherTestCase; 592 593 // the post processer must succeeed for the data to be stored. if 594 // is considered a major error that may corrupt existing data if it 595 // fails. 596 bool success = true; 597 598 if (test_case_cmd.length != 0) { 599 success = success && externalProgram([ 600 test_case_cmd, stdout_, stderr_ 601 ], gather_tc); 602 } 603 if (tc_analyze_builtin.length != 0) { 604 success = success && builtin(fio.getOutputDir, [ 605 stdout_, stderr_ 606 ], tc_analyze_builtin, gather_tc); 607 } 608 609 if (gather_tc.unstable.length != 0) { 610 logger.warningf("Unstable test cases found: [%-(%s, %)]", 611 gather_tc.unstableAsArray); 612 logger.info( 613 "As configured the result is ignored which will force the mutant to be re-tested"); 614 driver_sig = MutationDriverSignal.unstableTests; 615 } else if (success) { 616 test_cases = gather_tc; 617 driver_sig = MutationDriverSignal.next; 618 } 619 } catch (Exception e) { 620 logger.warning(e.msg).collectException; 621 } 622 } 623 624 void storeResult() { 625 import std.algorithm : sort, map; 626 627 driver_sig = MutationDriverSignal.next; 628 629 sw.stop; 630 631 const cnt_action = () { 632 if (mut_status == Mutation.Status.alive) 633 return Database.CntAction.incr; 634 return Database.CntAction.reset; 635 }(); 636 637 spinSqlQuery!(() { 638 db.updateMutation(mutp.id, mut_status, sw.peek, test_cases.failedAsArray, cnt_action); 639 }); 640 641 logger.infof("%s %s (%s)", mutp.id, mut_status, sw.peek).collectException; 642 logger.infof(test_cases.failed.length != 0, `%s killed by [%-(%s, %)]`, 643 mutp.id, test_cases.failedAsArray.sort.map!"a.name").collectException; 644 } 645 646 void restoreCode() { 647 driver_sig = MutationDriverSignal.next; 648 649 // restore the original file. 650 try { 651 fio.makeOutput(mut_file).write(original.content); 652 } catch (Exception e) { 653 logger.error(e.msg).collectException; 654 // fatal error because being unable to restore a file prohibit 655 // future mutations. 656 driver_sig = MutationDriverSignal.filesysError; 657 } 658 659 if (test_tmp_output.length != 0) { 660 import std.file : rmdirRecurse; 661 662 // trusted: test_tmp_output is tested to be valid data. 663 () @trusted { 664 try { 665 rmdirRecurse(test_tmp_output); 666 } catch (Exception e) { 667 logger.info(e.msg).collectException; 668 } 669 }(); 670 } 671 } 672 673 /// Signal from the ImplMutationDriver to the Driver. 674 auto signal() { 675 return driver_sig; 676 } 677 } 678 679 enum TestDriverSignal { 680 stop, 681 next, 682 allMutantsTested, 683 unreliableTestSuite, 684 compilationError, 685 mutationError, 686 timeoutUnchanged, 687 sanityCheckFailed, 688 } 689 690 struct TestDriver(ImplT) { 691 private enum State { 692 none, 693 initialize, 694 sanityCheck, 695 updateAndResetAliveMutants, 696 resetOldMutants, 697 cleanupTempDirs, 698 checkMutantsLeft, 699 preCompileSut, 700 measureTestSuite, 701 preMutationTest, 702 mutationTest, 703 checkTimeout, 704 incrWatchdog, 705 resetTimeout, 706 done, 707 error, 708 } 709 710 private { 711 State st; 712 ImplT impl; 713 } 714 715 this(ImplT impl) { 716 this.impl = impl; 717 } 718 719 bool isRunning() { 720 import std.algorithm : among; 721 722 return st.among(State.done, State.error) == 0; 723 } 724 725 ExitStatusType status() { 726 if (st == State.done) 727 return ExitStatusType.Ok; 728 else 729 return ExitStatusType.Errors; 730 } 731 732 void execute() { 733 import dextool.fsm : makeCallbacks; 734 735 const auto signal = impl.signal; 736 737 debug auto old_st = st; 738 739 st = nextState(st, signal); 740 741 debug logger.trace(old_st, "->", st, ":", signal).collectException; 742 743 mixin(makeCallbacks!State().switchOn("st").callbackOn("impl").finalize); 744 } 745 746 private static State nextState(const State current, const TestDriverSignal signal) { 747 State next_ = current; 748 749 final switch (current) with (State) { 750 case none: 751 next_ = State.initialize; 752 break; 753 case initialize: 754 if (signal == TestDriverSignal.next) 755 next_ = State.sanityCheck; 756 break; 757 case sanityCheck: 758 if (signal == TestDriverSignal.next) 759 next_ = State.preCompileSut; 760 else if (signal == TestDriverSignal.sanityCheckFailed) 761 next_ = State.error; 762 break; 763 case updateAndResetAliveMutants: 764 next_ = resetOldMutants; 765 break; 766 case resetOldMutants: 767 next_ = checkMutantsLeft; 768 break; 769 case checkMutantsLeft: 770 if (signal == TestDriverSignal.next) 771 next_ = State.measureTestSuite; 772 else if (signal == TestDriverSignal.allMutantsTested) 773 next_ = State.done; 774 break; 775 case preCompileSut: 776 if (signal == TestDriverSignal.next) 777 next_ = State.updateAndResetAliveMutants; 778 else if (signal == TestDriverSignal.compilationError) 779 next_ = State.error; 780 break; 781 case measureTestSuite: 782 if (signal == TestDriverSignal.next) 783 next_ = State.cleanupTempDirs; 784 else if (signal == TestDriverSignal.unreliableTestSuite) 785 next_ = State.error; 786 break; 787 case cleanupTempDirs: 788 next_ = preMutationTest; 789 break; 790 case preMutationTest: 791 next_ = State.mutationTest; 792 break; 793 case mutationTest: 794 if (signal == TestDriverSignal.next) 795 next_ = State.cleanupTempDirs; 796 else if (signal == TestDriverSignal.allMutantsTested) 797 next_ = State.checkTimeout; 798 else if (signal == TestDriverSignal.mutationError) 799 next_ = State.error; 800 break; 801 case checkTimeout: 802 if (signal == TestDriverSignal.timeoutUnchanged) 803 next_ = State.done; 804 else if (signal == TestDriverSignal.next) 805 next_ = State.incrWatchdog; 806 break; 807 case incrWatchdog: 808 next_ = State.resetTimeout; 809 break; 810 case resetTimeout: 811 if (signal == TestDriverSignal.next) 812 next_ = State.cleanupTempDirs; 813 break; 814 case done: 815 break; 816 case error: 817 break; 818 } 819 820 return next_; 821 } 822 } 823 824 struct ImplTestDriver(alias mutationDriverFactory) { 825 import std.traits : ReturnType; 826 import dextool.plugin.mutate.backend.watchdog : ProgressivWatchdog; 827 828 nothrow: 829 DriverData data; 830 831 ProgressivWatchdog prog_wd; 832 TestDriverSignal driver_sig; 833 ReturnType!mutationDriverFactory mut_driver; 834 long last_timeout_mutant_count = long.max; 835 836 this(DriverData data) { 837 this.data = data; 838 } 839 840 void none() { 841 } 842 843 void done() { 844 data.autoCleanup.cleanup; 845 } 846 847 void error() { 848 data.autoCleanup.cleanup; 849 } 850 851 void initialize() { 852 driver_sig = TestDriverSignal.next; 853 } 854 855 void sanityCheck() { 856 // #SPC-sanity_check_db_vs_filesys 857 import dextool.type : Path; 858 import dextool.plugin.mutate.backend.utility : checksum, trustedRelativePath; 859 import dextool.plugin.mutate.backend.type : Checksum; 860 861 driver_sig = TestDriverSignal.sanityCheckFailed; 862 863 const(Path)[] files; 864 spinSqlQuery!(() { files = data.db.getFiles; }); 865 866 bool has_sanity_check_failed; 867 for (size_t i; i < files.length;) { 868 Checksum db_checksum; 869 spinSqlQuery!(() { db_checksum = data.db.getFileChecksum(files[i]); }); 870 871 try { 872 auto abs_f = AbsolutePath(FileName(files[i]), 873 DirName(cast(string) data.filesysIO.getOutputDir)); 874 auto f_checksum = checksum(data.filesysIO.makeInput(abs_f).content[]); 875 if (db_checksum != f_checksum) { 876 logger.errorf("Mismatch between the file on the filesystem and the analyze of '%s'", 877 abs_f); 878 has_sanity_check_failed = true; 879 } 880 } catch (Exception e) { 881 // assume it is a problem reading the file or something like that. 882 has_sanity_check_failed = true; 883 logger.trace(e.msg).collectException; 884 } 885 886 // all done. continue with the next file 887 ++i; 888 } 889 890 if (has_sanity_check_failed) { 891 driver_sig = TestDriverSignal.sanityCheckFailed; 892 logger.error("Detected that one or more file has changed since last analyze where done") 893 .collectException; 894 logger.error("Either restore the files to the previous state or rerun the analyzer") 895 .collectException; 896 } else { 897 logger.info("Sanity check passed. Files on the filesystem are consistent") 898 .collectException; 899 driver_sig = TestDriverSignal.next; 900 } 901 } 902 903 // TODO: refactor. This method is too long. 904 void updateAndResetAliveMutants() { 905 import core.time : dur; 906 import std.algorithm : map; 907 import std.datetime.stopwatch : StopWatch; 908 import std.path : buildPath; 909 import dextool.type : Path; 910 import dextool.plugin.mutate.backend.type : TestCase; 911 912 driver_sig = TestDriverSignal.next; 913 914 if (data.conf.mutationTestCaseAnalyze.length == 0 915 && data.conf.mutationTestCaseBuiltin.length == 0) 916 return; 917 918 AbsolutePath test_tmp_output; 919 try { 920 auto tmpdir = createTmpDir(0); 921 if (tmpdir.length == 0) 922 return; 923 test_tmp_output = Path(tmpdir).AbsolutePath; 924 data.autoCleanup.add(test_tmp_output); 925 } catch (Exception e) { 926 logger.warning(e.msg).collectException; 927 return; 928 } 929 930 TestCase[] all_found_tc; 931 932 try { 933 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 934 import dextool.plugin.mutate.backend.watchdog : StaticTime; 935 936 auto stdout_ = buildPath(test_tmp_output, stdoutLog); 937 auto stderr_ = buildPath(test_tmp_output, stderrLog); 938 939 // using an unreasonable timeout because this is more intended to reuse the functionality in runTester 940 auto watchdog = StaticTime!StopWatch(999.dur!"hours"); 941 runTester(data.conf.mutationCompile, data.conf.mutationTester, 942 test_tmp_output, watchdog, data.filesysIO); 943 944 auto gather_tc = new GatherTestCase; 945 946 if (data.conf.mutationTestCaseAnalyze.length != 0) { 947 externalProgram([ 948 data.conf.mutationTestCaseAnalyze, stdout_, stderr_ 949 ], gather_tc); 950 logger.warningf(gather_tc.unstable.length != 0, 951 "Unstable test cases found: [%-(%s, %)]", gather_tc.unstableAsArray); 952 } 953 if (data.conf.mutationTestCaseBuiltin.length != 0) { 954 builtin(data.filesysIO.getOutputDir, [stdout_, stderr_], 955 data.conf.mutationTestCaseBuiltin, gather_tc); 956 } 957 958 all_found_tc = gather_tc.foundAsArray; 959 } catch (Exception e) { 960 logger.warning(e.msg).collectException; 961 } 962 963 warnIfConflictingTestCaseIdentifiers(all_found_tc); 964 965 // the test cases before anything has potentially changed. 966 Set!string old_tcs; 967 spinSqlQuery!(() { 968 foreach (tc; data.db.getDetectedTestCases) 969 old_tcs.add(tc.name); 970 }); 971 972 final switch (data.conf.onRemovedTestCases) with (ConfigMutationTest.RemovedTestCases) { 973 case doNothing: 974 spinSqlQuery!(() { data.db.addDetectedTestCases(all_found_tc); }); 975 break; 976 case remove: 977 import dextool.plugin.mutate.backend.database : MutationStatusId; 978 979 MutationStatusId[] ids; 980 spinSqlQuery!(() { ids = data.db.setDetectedTestCases(all_found_tc); }); 981 foreach (id; ids) 982 spinSqlQuery!(() { 983 data.db.updateMutationStatus(id, Mutation.Status.unknown); 984 }); 985 break; 986 } 987 988 Set!string found_tcs; 989 spinSqlQuery!(() { 990 found_tcs = null; 991 foreach (tc; data.db.getDetectedTestCases) 992 found_tcs.add(tc.name); 993 }); 994 995 printDroppedTestCases(old_tcs, found_tcs); 996 997 const new_test_cases = hasNewTestCases(old_tcs, found_tcs); 998 999 if (new_test_cases && data.conf.onNewTestCases == ConfigMutationTest 1000 .NewTestCases.resetAlive) { 1001 logger.info("Resetting alive mutants").collectException; 1002 resetAliveMutants(data.db); 1003 } 1004 } 1005 1006 void resetOldMutants() { 1007 import dextool.plugin.mutate.backend.database.type; 1008 1009 if (data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) 1010 return; 1011 1012 logger.infof("Resetting the %s oldest mutants", data.conf.oldMutantsNr).collectException; 1013 MutationStatusTime[] oldest; 1014 spinSqlQuery!(() { 1015 oldest = data.db.getOldestMutants(data.mutKind, data.conf.oldMutantsNr); 1016 }); 1017 foreach (const old; oldest) { 1018 logger.info(" Last updated ", old.updated).collectException; 1019 spinSqlQuery!(() { 1020 data.db.updateMutationStatus(old.id, Mutation.Status.unknown); 1021 }); 1022 } 1023 } 1024 1025 void cleanupTempDirs() { 1026 driver_sig = TestDriverSignal.next; 1027 data.autoCleanup.cleanup; 1028 } 1029 1030 void checkMutantsLeft() { 1031 driver_sig = TestDriverSignal.next; 1032 1033 auto mutant = spinSqlQuery!(() { 1034 return data.db.nextMutation(data.mutKind); 1035 }); 1036 1037 if (mutant.st == NextMutationEntry.Status.done) { 1038 logger.info("Done! All mutants are tested").collectException; 1039 driver_sig = TestDriverSignal.allMutantsTested; 1040 } 1041 } 1042 1043 void preCompileSut() { 1044 driver_sig = TestDriverSignal.compilationError; 1045 1046 logger.info("Preparing for mutation testing by checking that the program and tests compile without any errors (no mutants injected)") 1047 .collectException; 1048 1049 try { 1050 import std.process : execute; 1051 1052 const comp_res = execute( 1053 data.conf.mutationCompile.program ~ data.conf.mutationCompile.arguments); 1054 1055 if (comp_res.status == 0) { 1056 driver_sig = TestDriverSignal.next; 1057 } else { 1058 logger.info(comp_res.output); 1059 logger.error("Compiler command failed: ", comp_res.status); 1060 } 1061 } catch (Exception e) { 1062 // unable to for example execute the compiler 1063 logger.error(e.msg).collectException; 1064 } 1065 } 1066 1067 void measureTestSuite() { 1068 driver_sig = TestDriverSignal.unreliableTestSuite; 1069 1070 if (data.conf.mutationTesterRuntime.isNull) { 1071 logger.info("Measuring the time to run the tests: ", 1072 data.conf.mutationTester).collectException; 1073 auto tester = measureTesterDuration(data.conf.mutationTester); 1074 if (tester.status == ExitStatusType.Ok) { 1075 // The sampling of the test suite become too unreliable when the timeout is <1s. 1076 // This is a quick and dirty fix. 1077 // A proper fix requires an update of the sampler in runTester. 1078 auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime; 1079 logger.info("Tester measured to: ", t).collectException; 1080 prog_wd = ProgressivWatchdog(t); 1081 driver_sig = TestDriverSignal.next; 1082 } else { 1083 logger.error( 1084 "Test suite is unreliable. It must return exit status '0' when running with unmodified mutants") 1085 .collectException; 1086 } 1087 } else { 1088 prog_wd = ProgressivWatchdog(data.conf.mutationTesterRuntime.get); 1089 driver_sig = TestDriverSignal.next; 1090 } 1091 } 1092 1093 void preMutationTest() { 1094 driver_sig = TestDriverSignal.next; 1095 mut_driver = mutationDriverFactory(data, prog_wd.timeout); 1096 } 1097 1098 void mutationTest() { 1099 if (mut_driver.isRunning) { 1100 mut_driver.execute(); 1101 driver_sig = TestDriverSignal.stop; 1102 } else if (mut_driver.stopBecauseError) { 1103 driver_sig = TestDriverSignal.mutationError; 1104 } else if (mut_driver.stopMutationTesting) { 1105 driver_sig = TestDriverSignal.allMutantsTested; 1106 } else { 1107 driver_sig = TestDriverSignal.next; 1108 } 1109 } 1110 1111 void checkTimeout() { 1112 driver_sig = TestDriverSignal.stop; 1113 1114 auto entry = spinSqlQuery!(() { 1115 return data.db.timeoutMutants(data.mutKind); 1116 }); 1117 1118 try { 1119 if (!data.conf.mutationTesterRuntime.isNull) { 1120 // the user have supplied a timeout thus ignore this algorithm 1121 // for increasing the timeout 1122 driver_sig = TestDriverSignal.timeoutUnchanged; 1123 } else if (entry.count == 0) { 1124 driver_sig = TestDriverSignal.timeoutUnchanged; 1125 } else if (entry.count == last_timeout_mutant_count) { 1126 // no change between current pool of timeout mutants and the previous 1127 driver_sig = TestDriverSignal.timeoutUnchanged; 1128 } else if (entry.count < last_timeout_mutant_count) { 1129 driver_sig = TestDriverSignal.next; 1130 logger.info("Mutants with the status timeout: ", entry.count); 1131 } 1132 1133 last_timeout_mutant_count = entry.count; 1134 } catch (Exception e) { 1135 logger.warning(e.msg).collectException; 1136 } 1137 } 1138 1139 void incrWatchdog() { 1140 driver_sig = TestDriverSignal.next; 1141 prog_wd.incrTimeout; 1142 logger.info("Increasing timeout to: ", prog_wd.timeout).collectException; 1143 } 1144 1145 void resetTimeout() { 1146 // database is locked 1147 driver_sig = TestDriverSignal.stop; 1148 1149 try { 1150 data.db.resetMutant(data.mutKind, Mutation.Status.timeout, Mutation.Status.unknown); 1151 driver_sig = TestDriverSignal.next; 1152 } catch (Exception e) { 1153 logger.warning(e.msg).collectException; 1154 } 1155 } 1156 1157 auto signal() { 1158 return driver_sig; 1159 } 1160 } 1161 1162 private: 1163 1164 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport; 1165 import dextool.plugin.mutate.backend.type : TestCase; 1166 import dextool.set; 1167 1168 /// Run an external program that analyze the output from the test suite for test cases that failed. 1169 bool externalProgram(string[] cmd, TestCaseReport report) nothrow { 1170 import std.algorithm : copy, splitter, filter, map; 1171 import std.ascii : newline; 1172 import std.process : execute; 1173 import std.string : strip, startsWith; 1174 import dextool.plugin.mutate.backend.type : TestCase; 1175 1176 immutable passed = "passed:"; 1177 immutable failed = "failed:"; 1178 immutable unstable = "unstable:"; 1179 1180 try { 1181 // [test_case_cmd, stdout_, stderr_] 1182 auto p = execute(cmd); 1183 if (p.status == 0) { 1184 foreach (l; p.output.splitter(newline).map!(a => a.strip) 1185 .filter!(a => a.length != 0)) { 1186 if (l.startsWith(passed)) 1187 report.reportFound(TestCase(l[passed.length .. $].strip)); 1188 else if (l.startsWith(failed)) 1189 report.reportFailed(TestCase(l[failed.length .. $].strip)); 1190 else if (l.startsWith(unstable)) 1191 report.reportUnstable(TestCase(l[unstable.length .. $].strip)); 1192 } 1193 return true; 1194 } else { 1195 logger.warning(p.output); 1196 logger.warning("Failed to analyze the test case output"); 1197 return false; 1198 } 1199 } catch (Exception e) { 1200 logger.warning(e.msg).collectException; 1201 } 1202 1203 return false; 1204 } 1205 1206 /** Analyze the output from the test suite with one of the builtin analyzers. 1207 * 1208 * trusted: because the paths to the File object are created by this program 1209 * and can thus not lead to memory related problems. 1210 */ 1211 bool builtin(AbsolutePath reldir, string[] analyze_files, 1212 const(TestCaseAnalyzeBuiltin)[] tc_analyze_builtin, TestCaseReport app) @trusted nothrow { 1213 import std.stdio : File; 1214 import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze; 1215 import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze; 1216 import dextool.plugin.mutate.backend.test_mutant.makefile_post_analyze; 1217 1218 foreach (f; analyze_files) { 1219 auto gtest = GtestParser(reldir); 1220 CtestParser ctest; 1221 MakefileParser makefile; 1222 1223 File* fin; 1224 try { 1225 fin = new File(f); 1226 } catch (Exception e) { 1227 logger.warning(e.msg).collectException; 1228 return false; 1229 } 1230 1231 scope (exit) 1232 () { 1233 try { 1234 fin.close; 1235 destroy(fin); 1236 } catch (Exception e) { 1237 logger.warning(e.msg).collectException; 1238 } 1239 }(); 1240 1241 // an invalid UTF-8 char shall only result in the rest of the file being skipped 1242 try { 1243 foreach (l; fin.byLine) { 1244 // this is a magic number that felt good. Why would there be a line in a test case log that is longer than this? 1245 immutable magic_nr = 2048; 1246 if (l.length > magic_nr) { 1247 // The byLine split may fail and thus result in one huge line. 1248 // The result of this is that regex's that use backtracking become really slow. 1249 // By skipping these lines dextool at list doesn't hang. 1250 logger.warningf("Line in test case log is too long to analyze (%s > %s). Skipping...", 1251 l.length, magic_nr); 1252 continue; 1253 } 1254 1255 foreach (const p; tc_analyze_builtin) { 1256 final switch (p) { 1257 case TestCaseAnalyzeBuiltin.gtest: 1258 gtest.process(l, app); 1259 break; 1260 case TestCaseAnalyzeBuiltin.ctest: 1261 ctest.process(l, app); 1262 break; 1263 case TestCaseAnalyzeBuiltin.makefile: 1264 makefile.process(l, app); 1265 break; 1266 } 1267 } 1268 } 1269 } catch (Exception e) { 1270 logger.warning(e.msg).collectException; 1271 } 1272 } 1273 1274 return true; 1275 } 1276 1277 /// Returns: path to a tmp directory or null on failure. 1278 string createTmpDir(long id) nothrow { 1279 import std.random : uniform; 1280 import std.format : format; 1281 import std.file : mkdir, exists; 1282 1283 string test_tmp_output; 1284 1285 // try 5 times or bailout 1286 foreach (const _; 0 .. 5) { 1287 try { 1288 auto tmp = format("dextool_tmp_id_%s_%s", id, uniform!ulong); 1289 mkdir(tmp); 1290 test_tmp_output = AbsolutePath(FileName(tmp)); 1291 break; 1292 } catch (Exception e) { 1293 logger.warning(e.msg).collectException; 1294 } 1295 } 1296 1297 if (test_tmp_output.length == 0) { 1298 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 1299 .collectException; 1300 } 1301 1302 return test_tmp_output; 1303 } 1304 1305 /// Reset all alive mutants. 1306 void resetAliveMutants(ref Database db) @safe nothrow { 1307 import std.traits : EnumMembers; 1308 1309 // there is no use in trying to limit the mutants to reset to those that 1310 // are part of "this" execution because new test cases can only mean one 1311 // thing: re-test all alive mutants. 1312 1313 spinSqlQuery!(() { 1314 db.resetMutant([EnumMembers!(Mutation.Kind)], Mutation.Status.alive, 1315 Mutation.Status.unknown); 1316 }); 1317 } 1318 1319 /** Compare the old test cases with those that have been found this run. 1320 * 1321 * TODO: the side effect that this function print to the console is NOT good. 1322 */ 1323 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow { 1324 bool rval; 1325 1326 auto new_tcs = found_tcs.setDifference(old_tcs); 1327 foreach (tc; new_tcs.byKey) { 1328 logger.info(!rval, "Found new test case(s):").collectException; 1329 logger.infof("%s", tc).collectException; 1330 rval = true; 1331 } 1332 1333 return rval; 1334 } 1335 1336 /** Compare old and new test cases to print those that have been removed. 1337 */ 1338 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow { 1339 auto diff = old_tcs.setDifference(changed_tcs); 1340 auto removed = diff.setToList!string; 1341 1342 logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException; 1343 foreach (tc; removed) { 1344 logger.infof("%s", tc).collectException; 1345 } 1346 } 1347 1348 /// Returns: true if all tests cases have unique identifiers 1349 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow { 1350 Set!TestCase checked; 1351 bool conflict; 1352 1353 foreach (tc; found_tcs) { 1354 if (checked.contains(tc)) { 1355 logger.info(!conflict, 1356 "Found test cases that do not have global, unique identifiers") 1357 .collectException; 1358 logger.info(!conflict, 1359 "This make the report of test cases that has killed zero mutants unreliable") 1360 .collectException; 1361 logger.info("%s", tc).collectException; 1362 conflict = true; 1363 } 1364 } 1365 } 1366 1367 /** Paths stored will be removed automatically either when manually called or goes out of scope. 1368 */ 1369 class AutoCleanup { 1370 private string[] remove_dirs; 1371 1372 void add(AbsolutePath p) @safe nothrow { 1373 remove_dirs ~= cast(string) p; 1374 } 1375 1376 // trusted: the paths are forced to be valid paths. 1377 void cleanup() @trusted nothrow { 1378 import std.algorithm : filter; 1379 import std.array : array; 1380 import std.file : rmdirRecurse, exists; 1381 1382 foreach (ref p; remove_dirs.filter!(a => a.length != 0)) { 1383 try { 1384 if (exists(p)) 1385 rmdirRecurse(p); 1386 if (!exists(p)) 1387 p = null; 1388 } catch (Exception e) { 1389 logger.info(e.msg).collectException; 1390 } 1391 } 1392 1393 remove_dirs = remove_dirs.filter!(a => a.length != 0).array; 1394 } 1395 }