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.time : Duration; 13 import std.typecons : Nullable, NullableRef, nullableRef; 14 import std.exception : collectException; 15 16 import logger = std.experimental.logger; 17 18 import dextool.type : AbsolutePath, ExitStatusType, FileName, DirName; 19 import dextool.plugin.mutate.backend.database : Database, MutationEntry, 20 NextMutationEntry; 21 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 22 import dextool.plugin.mutate.backend.type : Mutation; 23 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin; 24 25 @safe: 26 27 auto makeTestMutant() { 28 return BuildTestMutant(); 29 } 30 31 private: 32 33 struct BuildTestMutant { 34 @safe: 35 nothrow: 36 37 import dextool.plugin.mutate.type : MutationKind; 38 39 private struct InternalData { 40 Mutation.Kind[] mut_kinds; 41 AbsolutePath test_suite_execute_program; 42 AbsolutePath compile_program; 43 AbsolutePath test_case_analyze_program; 44 Nullable!Duration test_suite_execute_timeout; 45 FilesysIO filesys_io; 46 TestCaseAnalyzeBuiltin[] tc_analyze_builtin; 47 } 48 49 private InternalData data; 50 51 auto mutations(MutationKind[] v) { 52 import dextool.plugin.mutate.backend.utility; 53 54 data.mut_kinds = toInternal(v); 55 return this; 56 } 57 58 /// a program to execute that test the mutant. The mutant is marked as alive if the exit code is 0, otherwise it is dead. 59 auto testSuiteProgram(AbsolutePath v) { 60 data.test_suite_execute_program = v; 61 return this; 62 } 63 64 /// program to use to compile the SUT + tests after a mutation has been performed. 65 auto compileProgram(AbsolutePath v) { 66 data.compile_program = v; 67 return this; 68 } 69 70 /// The time it takes to run the tests. 71 auto testSuiteTimeout(Nullable!Duration v) { 72 data.test_suite_execute_timeout = v; 73 return this; 74 } 75 76 auto testCaseAnalyzeProgram(AbsolutePath v) { 77 data.test_case_analyze_program = v; 78 return this; 79 } 80 81 auto testCaseAnalyzeBuiltin(TestCaseAnalyzeBuiltin[] v) { 82 data.tc_analyze_builtin = v; 83 return this; 84 } 85 86 ExitStatusType run(ref Database db, FilesysIO fio) nothrow { 87 auto mutationFactory(DriverData data, Duration test_base_timeout) @safe { 88 static class Rval { 89 ImplMutationDriver impl; 90 MutationTestDriver!(ImplMutationDriver*) driver; 91 92 this(DriverData d, Duration test_base_timeout) { 93 this.impl = ImplMutationDriver(d.filesysIO, d.db, d.mutKind, d.compilerProgram, 94 d.testProgram, d.testCaseAnalyzeProgram, 95 d.testCaseAnalyzeBuiltin, test_base_timeout); 96 97 this.driver = MutationTestDriver!(ImplMutationDriver*)(() @trusted{ 98 return &impl; 99 }()); 100 } 101 102 alias driver this; 103 } 104 105 return new Rval(data, test_base_timeout); 106 } 107 108 // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope 109 auto db_ref = () @trusted{ return nullableRef(&db); }(); 110 111 auto driver_data = DriverData(db_ref, fio, data.mut_kinds, 112 data.compile_program, data.test_suite_execute_program, 113 data.test_case_analyze_program, data.tc_analyze_builtin, 114 data.test_suite_execute_timeout); 115 116 auto test_driver_impl = ImplTestDriver!mutationFactory(driver_data); 117 auto test_driver_impl_ref = () @trusted{ 118 return nullableRef(&test_driver_impl); 119 }(); 120 auto test_driver = TestDriver!(typeof(test_driver_impl_ref))(test_driver_impl_ref); 121 122 while (test_driver.isRunning) { 123 test_driver.execute; 124 } 125 126 return test_driver.status; 127 } 128 } 129 130 immutable stdoutLog = "stdout.log"; 131 immutable stderrLog = "stderr.log"; 132 133 struct DriverData { 134 NullableRef!Database db; 135 FilesysIO filesysIO; 136 Mutation.Kind[] mutKind; 137 AbsolutePath compilerProgram; 138 AbsolutePath testProgram; 139 AbsolutePath testCaseAnalyzeProgram; 140 TestCaseAnalyzeBuiltin[] testCaseAnalyzeBuiltin; 141 Nullable!Duration testProgramTimeout; 142 } 143 144 /** Run the test suite to verify a mutation. 145 * 146 * Params: 147 * p = ? 148 * timeout = timeout threshold. 149 */ 150 Mutation.Status runTester(WatchdogT)(AbsolutePath compile_p, AbsolutePath tester_p, 151 AbsolutePath test_output_dir, WatchdogT watchdog, FilesysIO fio) nothrow { 152 import core.thread : Thread; 153 import std.algorithm : among; 154 import std.datetime.stopwatch : StopWatch; 155 import dextool.plugin.mutate.backend.linux_process : spawnSession, tryWait, 156 kill, wait; 157 import std.stdio : File; 158 import core.sys.posix.signal : SIGKILL; 159 160 Mutation.Status rval; 161 162 try { 163 auto p = spawnSession([cast(string) compile_p]); 164 auto res = p.wait; 165 if (res.terminated && res.status != 0) 166 return Mutation.Status.killedByCompiler; 167 else if (!res.terminated) { 168 logger.warning("unknown error when executing the compiler").collectException; 169 return Mutation.Status.unknown; 170 } 171 } 172 catch (Exception e) { 173 logger.warning(e.msg).collectException; 174 } 175 176 string stdout_p; 177 string stderr_p; 178 179 if (test_output_dir.length != 0) { 180 import std.path : buildPath; 181 182 stdout_p = buildPath(test_output_dir, stdoutLog); 183 stderr_p = buildPath(test_output_dir, stderrLog); 184 } 185 186 try { 187 auto p = spawnSession([cast(string) tester_p], stdout_p, stderr_p); 188 // trusted: killing the process started in this scope 189 void cleanup() @safe nothrow { 190 import core.sys.posix.signal : SIGKILL; 191 192 if (rval.among(Mutation.Status.timeout, Mutation.Status.unknown)) { 193 kill(p, SIGKILL); 194 wait(p); 195 } 196 } 197 198 scope (exit) 199 cleanup; 200 201 rval = Mutation.Status.timeout; 202 watchdog.start; 203 while (watchdog.isOk) { 204 auto res = tryWait(p); 205 if (res.terminated) { 206 if (res.status == 0) 207 rval = Mutation.Status.alive; 208 else 209 rval = Mutation.Status.killed; 210 break; 211 } 212 213 import core.time : dur; 214 215 // trusted: a hard coded value is used, no user input. 216 () @trusted{ Thread.sleep(10.dur!"msecs"); }(); 217 } 218 } 219 catch (Exception e) { 220 // unable to for example execute the test suite 221 logger.warning(e.msg).collectException; 222 return Mutation.Status.unknown; 223 } 224 225 return rval; 226 } 227 228 struct MeasureTestDurationResult { 229 ExitStatusType status; 230 Duration runtime; 231 } 232 233 /** 234 * If the tests fail (exit code isn't 0) any time then they are too unreliable 235 * to use for mutation testing. 236 * 237 * The runtime is the lowest of the three executions. 238 * 239 * Params: 240 * p = ? 241 */ 242 auto measureTesterDuration(AbsolutePath p) nothrow { 243 if (p.length == 0) { 244 collectException(logger.error("No test suite runner specified (--mutant-tester)")); 245 return MeasureTestDurationResult(ExitStatusType.Errors); 246 } 247 248 auto any_failure = ExitStatusType.Ok; 249 250 void fun() { 251 import std.process : execute; 252 253 auto res = execute([cast(string) p]); 254 if (res.status != 0) 255 any_failure = ExitStatusType.Errors; 256 } 257 258 import std.datetime.stopwatch : benchmark; 259 import std.algorithm : minElement, map; 260 import core.time : dur; 261 262 try { 263 auto bench = benchmark!fun(3); 264 265 if (any_failure != ExitStatusType.Ok) 266 return MeasureTestDurationResult(ExitStatusType.Errors); 267 268 auto a = (cast(long)((bench[0].total!"msecs") / 3.0)).dur!"msecs"; 269 return MeasureTestDurationResult(ExitStatusType.Ok, a); 270 } 271 catch (Exception e) { 272 collectException(logger.error(e.msg)); 273 return MeasureTestDurationResult(ExitStatusType.Errors); 274 } 275 } 276 277 enum MutationDriverSignal { 278 /// stay in the current state 279 stop, 280 /// advance to the next state 281 next, 282 /// All mutants are tested. Stopping mutation testing 283 allMutantsTested, 284 /// An error occured when interacting with the filesystem (fatal). Stopping all mutation testing 285 filesysError, 286 /// An error for a single mutation. It is skipped. 287 mutationError, 288 } 289 290 /** Drive the control flow when testing a mutant. 291 * 292 * The architecture assume that there will be behavior changes therefore a 293 * strict FSM that separate the context, action and next_state. 294 * 295 * The intention is to separate the control flow from the implementation of the 296 * actions that are done when mutation testing. 297 */ 298 struct MutationTestDriver(ImplT) { 299 import std.experimental.typecons : Final; 300 301 /// The internal state of the FSM. 302 private enum State { 303 none, 304 initialize, 305 mutateCode, 306 testMutant, 307 restoreCode, 308 testCaseAnalyze, 309 storeResult, 310 done, 311 allMutantsTested, 312 filesysError, 313 /// happens when an error occurs during mutations testing but that do not prohibit testing of other mutants 314 noResultRestoreCode, 315 noResult, 316 } 317 318 private { 319 State st; 320 ImplT impl; 321 } 322 323 this(ImplT impl) { 324 this.impl = impl; 325 } 326 327 /// Returns: true as long as the driver is processing a mutant. 328 bool isRunning() { 329 import std.algorithm : among; 330 331 return st.among(State.done, State.noResult, State.filesysError, State.allMutantsTested) == 0; 332 } 333 334 bool stopBecauseError() { 335 return st == State.filesysError; 336 } 337 338 /// Returns: true when the mutation testing should be stopped 339 bool stopMutationTesting() { 340 return st == State.allMutantsTested; 341 } 342 343 void execute() { 344 const auto signal = impl.signal; 345 346 debug auto old_st = st; 347 348 st = nextState(st, signal); 349 350 debug logger.trace(old_st, "->", st, ":", signal).collectException; 351 352 final switch (st) { 353 case State.none: 354 break; 355 case State.initialize: 356 impl.initialize; 357 break; 358 case State.mutateCode: 359 impl.mutateCode; 360 break; 361 case State.testMutant: 362 impl.testMutant; 363 break; 364 case State.testCaseAnalyze: 365 impl.testCaseAnalyze; 366 break; 367 case State.restoreCode: 368 impl.cleanup; 369 break; 370 case State.storeResult: 371 impl.storeResult; 372 break; 373 case State.done: 374 break; 375 case State.allMutantsTested: 376 break; 377 case State.filesysError: 378 logger.warning("Filesystem error").collectException; 379 break; 380 case State.noResultRestoreCode: 381 impl.cleanup; 382 break; 383 case State.noResult: 384 break; 385 } 386 } 387 388 private static State nextState(immutable State current, immutable MutationDriverSignal signal) @safe pure nothrow @nogc { 389 State next_ = current; 390 391 final switch (current) { 392 case State.none: 393 next_ = State.initialize; 394 break; 395 case State.initialize: 396 if (signal == MutationDriverSignal.next) 397 next_ = State.mutateCode; 398 break; 399 case State.mutateCode: 400 if (signal == MutationDriverSignal.next) 401 next_ = State.testMutant; 402 else if (signal == MutationDriverSignal.allMutantsTested) 403 next_ = State.allMutantsTested; 404 else if (signal == MutationDriverSignal.filesysError) 405 next_ = State.filesysError; 406 else if (signal == MutationDriverSignal.mutationError) 407 next_ = State.noResultRestoreCode; 408 break; 409 case State.testMutant: 410 if (signal == MutationDriverSignal.next) 411 next_ = State.testCaseAnalyze; 412 else if (signal == MutationDriverSignal.mutationError) 413 next_ = State.noResultRestoreCode; 414 else if (signal == MutationDriverSignal.allMutantsTested) 415 next_ = State.allMutantsTested; 416 break; 417 case State.testCaseAnalyze: 418 if (signal == MutationDriverSignal.next) 419 next_ = State.restoreCode; 420 else if (signal == MutationDriverSignal.mutationError) 421 next_ = State.noResultRestoreCode; 422 break; 423 case State.restoreCode: 424 if (signal == MutationDriverSignal.next) 425 next_ = State.storeResult; 426 else if (signal == MutationDriverSignal.filesysError) 427 next_ = State.filesysError; 428 break; 429 case State.storeResult: 430 if (signal == MutationDriverSignal.next) 431 next_ = State.done; 432 break; 433 case State.done: 434 break; 435 case State.allMutantsTested: 436 break; 437 case State.filesysError: 438 break; 439 case State.noResultRestoreCode: 440 next_ = State.noResult; 441 break; 442 case State.noResult: 443 break; 444 } 445 446 return next_; 447 } 448 } 449 450 /** Implementation of the actions during the test of a mutant. 451 * 452 * The intention is that this driver do NOT control the flow. 453 */ 454 struct ImplMutationDriver { 455 import std.datetime.stopwatch : StopWatch; 456 import dextool.plugin.mutate.backend.type : TestCase; 457 458 nothrow: 459 460 FilesysIO fio; 461 NullableRef!Database db; 462 463 StopWatch sw; 464 MutationDriverSignal driver_sig; 465 466 Nullable!MutationEntry mutp; 467 AbsolutePath mut_file; 468 const(ubyte)[] original_content; 469 470 const(Mutation.Kind)[] mut_kind; 471 const TestCaseAnalyzeBuiltin[] tc_analyze_builtin; 472 473 AbsolutePath compile_cmd; 474 AbsolutePath test_cmd; 475 AbsolutePath test_case_cmd; 476 Duration tester_runtime; 477 478 /// Temporary directory where stdout/stderr should be written. 479 AbsolutePath test_tmp_output; 480 481 Mutation.Status mut_status; 482 483 TestCase[] test_cases; 484 485 this(FilesysIO fio, NullableRef!Database db, Mutation.Kind[] mut_kind, AbsolutePath compile_cmd, AbsolutePath test_cmd, 486 AbsolutePath test_case_cmd, 487 TestCaseAnalyzeBuiltin[] tc_analyze_builtin, Duration tester_runtime) { 488 this.fio = fio; 489 this.db = db; 490 this.mut_kind = mut_kind; 491 this.compile_cmd = compile_cmd; 492 this.test_cmd = test_cmd; 493 this.test_case_cmd = test_case_cmd; 494 this.tc_analyze_builtin = tc_analyze_builtin; 495 this.tester_runtime = tester_runtime; 496 } 497 498 void initialize() { 499 sw.start; 500 driver_sig = MutationDriverSignal.next; 501 } 502 503 void mutateCode() { 504 import core.thread : Thread; 505 import std.random : uniform; 506 import dextool.plugin.mutate.backend.generate_mutant : generateMutant, 507 GenerateMutantResult, GenerateMutantStatus; 508 509 driver_sig = MutationDriverSignal.stop; 510 511 auto next_m = db.nextMutation(mut_kind); 512 if (next_m.st == NextMutationEntry.Status.done) { 513 logger.info("Done! All mutants are tested").collectException; 514 driver_sig = MutationDriverSignal.allMutantsTested; 515 return; 516 } else if (next_m.st == NextMutationEntry.Status.queryError) { 517 // the database is locked. It will automatically sleep and continue. 518 return; 519 } else { 520 mutp = next_m.entry; 521 } 522 523 try { 524 mut_file = AbsolutePath(FileName(mutp.file), DirName(fio.getOutputDir)); 525 526 // must duplicate because the buffer is memory mapped thus it can change 527 original_content = fio.makeInput(mut_file).read.dup; 528 } 529 catch (Exception e) { 530 logger.error(e.msg).collectException; 531 driver_sig = MutationDriverSignal.filesysError; 532 return; 533 } 534 535 if (original_content.length == 0) { 536 logger.warning("Unable to read ", mut_file).collectException; 537 driver_sig = MutationDriverSignal.filesysError; 538 return; 539 } 540 541 // mutate 542 try { 543 auto fout = fio.makeOutput(mut_file); 544 auto mut_res = generateMutant(db.get, mutp, original_content, fout); 545 546 final switch (mut_res.status) with (GenerateMutantStatus) { 547 case error: 548 driver_sig = MutationDriverSignal.mutationError; 549 break; 550 case filesysError: 551 driver_sig = MutationDriverSignal.filesysError; 552 break; 553 case databaseError: 554 // such as when the database is locked 555 driver_sig = MutationDriverSignal.mutationError; 556 break; 557 case checksumError: 558 driver_sig = MutationDriverSignal.filesysError; 559 break; 560 case noMutation: 561 driver_sig = MutationDriverSignal.mutationError; 562 break; 563 case ok: 564 driver_sig = MutationDriverSignal.next; 565 logger.infof("%s from '%s' to '%s' in %s:%s:%s", mutp.id, mut_res.from, 566 mut_res.to, mut_file, mutp.sloc.line, mutp.sloc.column); 567 break; 568 } 569 } 570 catch (Exception e) { 571 logger.warning(e.msg).collectException; 572 driver_sig = MutationDriverSignal.mutationError; 573 } 574 } 575 576 void testMutant() { 577 import std.random : uniform; 578 import std.format : format; 579 import std.file : mkdir, exists; 580 581 assert(!mutp.isNull); 582 driver_sig = MutationDriverSignal.mutationError; 583 584 if (test_case_cmd.length != 0 || tc_analyze_builtin.length != 0) { 585 // try 5 times or bailout 586 foreach (const _; 0 .. 5) { 587 try { 588 auto tmp = format("dextool_tmp_%s", uniform!ulong); 589 mkdir(tmp); 590 test_tmp_output = AbsolutePath(FileName(tmp)); 591 break; 592 } 593 catch (Exception e) { 594 logger.warning(e.msg).collectException; 595 } 596 } 597 598 if (test_tmp_output.length == 0) { 599 logger.warning("Unable to create a temporary directory to store stdout/stderr in") 600 .collectException; 601 return; 602 } 603 } 604 605 try { 606 import dextool.plugin.mutate.backend.watchdog : StaticTime; 607 608 auto watchdog = StaticTime!StopWatch(tester_runtime); 609 610 mut_status = runTester(compile_cmd, test_cmd, test_tmp_output, watchdog, fio); 611 driver_sig = MutationDriverSignal.next; 612 } 613 catch (Exception e) { 614 logger.warning(e.msg).collectException; 615 } 616 } 617 618 void testCaseAnalyze() { 619 import std.algorithm : splitter, map, filter; 620 import std.array : array; 621 import std.ascii : newline; 622 import std.file : exists; 623 import std.path : buildPath; 624 import std.process : execute; 625 import std..string : strip; 626 627 if (mut_status != Mutation.Status.killed || test_tmp_output.length == 0) { 628 driver_sig = MutationDriverSignal.next; 629 return; 630 } 631 632 bool externalProgram(T)(string stdout_, string stderr_, ref T app) { 633 import std.algorithm : copy; 634 635 auto p = execute([test_case_cmd, stdout_, stderr_]); 636 if (p.status == 0) { 637 p.output.splitter(newline).map!(a => a.strip) 638 .filter!(a => a.length != 0).map!(a => TestCase(a)).copy(app); 639 return true; 640 } else { 641 logger.warning(p.output); 642 logger.warning("Failed to analyze the test case output"); 643 return false; 644 } 645 } 646 647 // trusted: because the paths to the File object are created by this 648 // program and can thus not lead to memory related problems. 649 bool builtin(T)(string stdout_, string stderr_, ref T app) @trusted { 650 import std.stdio : File; 651 import dextool.plugin.mutate.backend.test_mutant.ctest_post_analyze; 652 import dextool.plugin.mutate.backend.test_mutant.gtest_post_analyze; 653 654 auto reldir = fio.getOutputDir; 655 656 foreach (f; [stdout_, stderr_]) { 657 auto gtest = GtestParser(reldir); 658 CtestParser ctest; 659 660 foreach (l; File(f).byLine) { 661 foreach (const p; tc_analyze_builtin) { 662 final switch (p) { 663 case TestCaseAnalyzeBuiltin.gtest: 664 gtest.process(l, app); 665 break; 666 case TestCaseAnalyzeBuiltin.ctest: 667 ctest.process(l, app); 668 break; 669 } 670 } 671 } 672 } 673 674 return true; 675 } 676 677 driver_sig = MutationDriverSignal.mutationError; 678 679 try { 680 auto stdout_ = buildPath(test_tmp_output, stdoutLog); 681 auto stderr_ = buildPath(test_tmp_output, stderrLog); 682 683 if (!exists(stdout_) || !exists(stderr_)) { 684 logger.warningf("Unable to open %s and %s for test case analyze", stdout_, stderr_); 685 return; 686 } 687 688 import std.array : appender; 689 690 auto app = appender!(TestCase[])(); 691 692 bool success = true; 693 if (test_case_cmd.length != 0) 694 success = success && externalProgram(stdout_, stderr_, app); 695 if (tc_analyze_builtin.length != 0) 696 success = success && builtin(stdout_, stderr_, app); 697 698 if (success) { 699 test_cases = app.data; 700 driver_sig = MutationDriverSignal.next; 701 } 702 } 703 catch (Exception e) { 704 logger.warning(e.msg).collectException; 705 } 706 } 707 708 void storeResult() { 709 import dextool.plugin.mutate.backend.mutation_type : broadcast; 710 711 driver_sig = MutationDriverSignal.stop; 712 713 sw.stop; 714 715 try { 716 auto bcast = broadcast(mutp.mp.mutations[0].kind); 717 718 db.updateMutationBroadcast(mutp.id, mut_status, sw.peek, test_cases, bcast); 719 logger.infof("%s %s (%s)", mutp.id, mut_status, sw.peek); 720 logger.infof(test_cases.length != 0, "%s killed by [%(%s,%)]", mutp.id, test_cases); 721 driver_sig = MutationDriverSignal.next; 722 } 723 catch (Exception e) { 724 logger.warning(e.msg).collectException; 725 } 726 } 727 728 void cleanup() { 729 driver_sig = MutationDriverSignal.next; 730 731 // restore the original file. 732 try { 733 fio.makeOutput(mut_file).write(original_content); 734 } 735 catch (Exception e) { 736 logger.error(e.msg).collectException; 737 // fatal error because being unable to restore a file prohibit 738 // future mutations. 739 driver_sig = MutationDriverSignal.filesysError; 740 } 741 742 if (test_tmp_output.length != 0) { 743 import std.file : rmdirRecurse; 744 745 // trusted: test_tmp_output is tested to be valid data. 746 // it is further created via mkdtemp which I assume can be 747 // considered safe because its input is created wholly in this 748 // driver. 749 () @trusted{ 750 try { 751 rmdirRecurse(test_tmp_output); 752 } 753 catch (Exception e) { 754 logger.info(e.msg).collectException; 755 } 756 }(); 757 } 758 } 759 760 /// Signal from the ImplMutationDriver to the Driver. 761 auto signal() { 762 return driver_sig; 763 } 764 } 765 766 enum TestDriverSignal { 767 stop, 768 next, 769 allMutantsTested, 770 unreliableTestSuite, 771 compilationError, 772 mutationError, 773 timeoutUnchanged, 774 sanityCheckFailed, 775 } 776 777 struct TestDriver(ImplT) { 778 private enum State { 779 none, 780 initialize, 781 sanityCheck, 782 checkMutantsLeft, 783 preCompileSut, 784 measureTestSuite, 785 preMutationTest, 786 mutationTest, 787 checkTimeout, 788 incrWatchdog, 789 resetTimeout, 790 done, 791 error, 792 } 793 794 private { 795 State st; 796 ImplT impl; 797 } 798 799 this(ImplT impl) { 800 this.impl = impl; 801 } 802 803 bool isRunning() { 804 import std.algorithm : among; 805 806 return st.among(State.done, State.error) == 0; 807 } 808 809 ExitStatusType status() { 810 if (st == State.done) 811 return ExitStatusType.Ok; 812 else 813 return ExitStatusType.Errors; 814 } 815 816 void execute() { 817 const auto signal = impl.signal; 818 819 debug auto old_st = st; 820 821 st = nextState(st, signal); 822 823 debug logger.trace(old_st, "->", st, ":", signal).collectException; 824 825 final switch (st) with (State) { 826 case none: 827 break; 828 case initialize: 829 impl.initialize; 830 break; 831 case sanityCheck: 832 impl.sanityCheck; 833 break; 834 case checkMutantsLeft: 835 impl.checkMutantsLeft; 836 break; 837 case preCompileSut: 838 impl.compileProgram; 839 break; 840 case measureTestSuite: 841 impl.measureTestSuite; 842 break; 843 case preMutationTest: 844 impl.preMutationTest; 845 break; 846 case mutationTest: 847 impl.testMutant; 848 break; 849 case checkTimeout: 850 impl.checkTimeout; 851 break; 852 case incrWatchdog: 853 impl.incrWatchdog; 854 break; 855 case resetTimeout: 856 impl.resetTimeout; 857 break; 858 case done: 859 break; 860 case error: 861 break; 862 } 863 } 864 865 private static State nextState(const State current, const TestDriverSignal signal) { 866 State next_ = current; 867 868 final switch (current) with (State) { 869 case none: 870 next_ = State.initialize; 871 break; 872 case initialize: 873 if (signal == TestDriverSignal.next) 874 next_ = State.sanityCheck; 875 break; 876 case sanityCheck: 877 if (signal == TestDriverSignal.next) 878 next_ = State.checkMutantsLeft; 879 else if (signal == TestDriverSignal.sanityCheckFailed) 880 next_ = State.error; 881 break; 882 case checkMutantsLeft: 883 if (signal == TestDriverSignal.next) 884 next_ = State.preCompileSut; 885 else if (signal == TestDriverSignal.allMutantsTested) 886 next_ = State.done; 887 break; 888 case preCompileSut: 889 if (signal == TestDriverSignal.next) 890 next_ = State.measureTestSuite; 891 else if (signal == TestDriverSignal.compilationError) 892 next_ = State.error; 893 break; 894 case measureTestSuite: 895 if (signal == TestDriverSignal.next) 896 next_ = State.preMutationTest; 897 else if (signal == TestDriverSignal.unreliableTestSuite) 898 next_ = State.error; 899 break; 900 case preMutationTest: 901 next_ = State.mutationTest; 902 break; 903 case mutationTest: 904 if (signal == TestDriverSignal.next) 905 next_ = State.preMutationTest; 906 else if (signal == TestDriverSignal.allMutantsTested) 907 next_ = State.checkTimeout; 908 else if (signal == TestDriverSignal.mutationError) 909 next_ = State.error; 910 break; 911 case checkTimeout: 912 if (signal == TestDriverSignal.timeoutUnchanged) 913 next_ = State.done; 914 else if (signal == TestDriverSignal.next) 915 next_ = State.incrWatchdog; 916 break; 917 case incrWatchdog: 918 next_ = State.resetTimeout; 919 break; 920 case resetTimeout: 921 if (signal == TestDriverSignal.next) 922 next_ = State.preMutationTest; 923 break; 924 case done: 925 break; 926 case error: 927 break; 928 } 929 930 return next_; 931 } 932 } 933 934 struct ImplTestDriver(alias mutationDriverFactory) { 935 import dextool.plugin.mutate.backend.watchdog : ProgressivWatchdog; 936 import std.traits : ReturnType; 937 938 nothrow: 939 DriverData data; 940 941 ProgressivWatchdog prog_wd; 942 TestDriverSignal driver_sig; 943 ReturnType!mutationDriverFactory mut_driver; 944 long last_timeout_mutant_count = long.max; 945 946 this(DriverData data) { 947 this.data = data; 948 } 949 950 void initialize() { 951 driver_sig = TestDriverSignal.next; 952 } 953 954 void sanityCheck() { 955 // #SPC-plugin_mutate_sanity_check_db_vs_filesys 956 import dextool.type : Path; 957 import dextool.plugin.mutate.backend.utility : checksum, 958 trustedRelativePath; 959 import dextool.plugin.mutate.backend.type : Checksum; 960 961 const(Path)[] files; 962 try { 963 files = data.db.getFiles; 964 } 965 catch (Exception e) { 966 // assume the database is locked thus need to retry 967 driver_sig = TestDriverSignal.stop; 968 logger.trace(e.msg).collectException; 969 return; 970 } 971 972 bool has_sanity_check_failed; 973 for (size_t i; i < files.length;) { 974 Checksum db_checksum; 975 try { 976 db_checksum = data.db.getFileChecksum(files[i]); 977 } 978 catch (Exception e) { 979 // the database is locked 980 logger.trace(e.msg).collectException; 981 // retry 982 continue; 983 } 984 985 try { 986 auto abs_f = AbsolutePath(FileName(files[i]), 987 DirName(cast(string) data.filesysIO.getOutputDir)); 988 auto f_checksum = checksum(data.filesysIO.makeInput(abs_f).read[]); 989 if (db_checksum != f_checksum) { 990 logger.errorf("Mismatch between the file on the filesystem and the analyze of '%s'", 991 abs_f); 992 has_sanity_check_failed = true; 993 } 994 } 995 catch (Exception e) { 996 // assume it is a problem reading the file or something like that. 997 has_sanity_check_failed = true; 998 logger.trace(e.msg).collectException; 999 } 1000 1001 // all done. continue with the next file 1002 ++i; 1003 } 1004 1005 if (has_sanity_check_failed) { 1006 driver_sig = TestDriverSignal.sanityCheckFailed; 1007 logger.error("Detected that one or more file has changed since last analyze where done") 1008 .collectException; 1009 logger.error("Either restore the files to the previous state or rerun the analyzer") 1010 .collectException; 1011 } else { 1012 logger.info("Sanity check passed. Files on the filesystem are consistent") 1013 .collectException; 1014 driver_sig = TestDriverSignal.next; 1015 } 1016 } 1017 1018 void checkMutantsLeft() { 1019 driver_sig = TestDriverSignal.next; 1020 1021 const auto mutant = data.db.nextMutation(data.mutKind); 1022 1023 if (mutant.st == NextMutationEntry.Status.queryError) { 1024 // the database is locked 1025 driver_sig = TestDriverSignal.stop; 1026 } else if (mutant.st == NextMutationEntry.Status.done) { 1027 logger.info("Done! All mutants are tested").collectException; 1028 driver_sig = TestDriverSignal.allMutantsTested; 1029 } 1030 } 1031 1032 void compileProgram() { 1033 driver_sig = TestDriverSignal.compilationError; 1034 1035 logger.info("Preparing for mutation testing by checking that the program and tests compile without any errors (no mutants injected)") 1036 .collectException; 1037 1038 try { 1039 import std.process : execute; 1040 1041 const comp_res = execute([cast(string) data.compilerProgram]); 1042 1043 if (comp_res.status == 0) { 1044 driver_sig = TestDriverSignal.next; 1045 } else { 1046 logger.info(comp_res.output); 1047 logger.error("Compiler command failed: ", comp_res.status); 1048 } 1049 } 1050 catch (Exception e) { 1051 // unable to for example execute the compiler 1052 logger.error(e.msg).collectException; 1053 } 1054 } 1055 1056 void measureTestSuite() { 1057 driver_sig = TestDriverSignal.unreliableTestSuite; 1058 1059 if (data.testProgramTimeout.isNull) { 1060 logger.info("Measuring the time to run the tests: ", data.testProgram).collectException; 1061 auto tester = measureTesterDuration(data.testProgram); 1062 if (tester.status == ExitStatusType.Ok) { 1063 logger.info("Tester measured to: ", tester.runtime).collectException; 1064 prog_wd = ProgressivWatchdog(tester.runtime); 1065 driver_sig = TestDriverSignal.next; 1066 } else { 1067 logger.error( 1068 "Test suite is unreliable. It must return exit status '0' when running with unmodified mutants") 1069 .collectException; 1070 } 1071 } else { 1072 prog_wd = ProgressivWatchdog(data.testProgramTimeout.get); 1073 driver_sig = TestDriverSignal.next; 1074 } 1075 } 1076 1077 void preMutationTest() { 1078 driver_sig = TestDriverSignal.next; 1079 mut_driver = mutationDriverFactory(data, prog_wd.timeout); 1080 } 1081 1082 void testMutant() { 1083 if (mut_driver.isRunning) { 1084 mut_driver.execute(); 1085 driver_sig = TestDriverSignal.stop; 1086 } else if (mut_driver.stopBecauseError) { 1087 driver_sig = TestDriverSignal.mutationError; 1088 } else if (mut_driver.stopMutationTesting) { 1089 driver_sig = TestDriverSignal.allMutantsTested; 1090 } else { 1091 driver_sig = TestDriverSignal.next; 1092 } 1093 } 1094 1095 void checkTimeout() { 1096 driver_sig = TestDriverSignal.stop; 1097 1098 auto entry = data.db.timeoutMutants(data.mutKind); 1099 if (entry.isNull) { 1100 // the database is locked 1101 return; 1102 } 1103 1104 try { 1105 if (!data.testProgramTimeout.isNull) { 1106 // the user have supplied a timeout thus ignore this algorithm 1107 // for increasing the timeout 1108 driver_sig = TestDriverSignal.timeoutUnchanged; 1109 } else if (entry.count == 0) { 1110 driver_sig = TestDriverSignal.timeoutUnchanged; 1111 } else if (entry.count == last_timeout_mutant_count) { 1112 // no change between current pool of timeout mutants and the previous 1113 driver_sig = TestDriverSignal.timeoutUnchanged; 1114 } else if (entry.count < last_timeout_mutant_count) { 1115 driver_sig = TestDriverSignal.next; 1116 logger.info("Mutants with the status timeout: ", entry.count); 1117 } 1118 1119 last_timeout_mutant_count = entry.count; 1120 } 1121 catch (Exception e) { 1122 logger.warning(e.msg).collectException; 1123 } 1124 } 1125 1126 void incrWatchdog() { 1127 driver_sig = TestDriverSignal.next; 1128 prog_wd.incrTimeout; 1129 logger.info("Increasing timeout to: ", prog_wd.timeout).collectException; 1130 } 1131 1132 void resetTimeout() { 1133 // database is locked 1134 driver_sig = TestDriverSignal.stop; 1135 1136 try { 1137 data.db.resetMutant(data.mutKind, Mutation.Status.timeout, Mutation.Status.unknown); 1138 driver_sig = TestDriverSignal.next; 1139 } 1140 catch (Exception e) { 1141 logger.warning(e.msg).collectException; 1142 } 1143 } 1144 1145 auto signal() { 1146 return driver_sig; 1147 } 1148 }