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, Tuple, Yes; 21 22 import blob_model : Blob; 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.common; 31 import dextool.plugin.mutate.backend.test_mutant.interface_ : TestCaseReport; 32 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner; 33 import dextool.plugin.mutate.backend.type : Mutation, TestCase; 34 import dextool.plugin.mutate.config; 35 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand; 36 import dextool.set; 37 import dextool.type : AbsolutePath, ExitStatusType, Path; 38 39 @safe: 40 41 auto makeTestMutant() { 42 return BuildTestMutant(); 43 } 44 45 private: 46 47 struct BuildTestMutant { 48 @safe: 49 nothrow: 50 51 import dextool.plugin.mutate.type : MutationKind; 52 53 private struct InternalData { 54 Mutation.Kind[] mut_kinds; 55 FilesysIO filesys_io; 56 ConfigMutationTest config; 57 } 58 59 private InternalData data; 60 61 auto config(ConfigMutationTest c) { 62 data.config = c; 63 return this; 64 } 65 66 auto mutations(MutationKind[] v) { 67 import dextool.plugin.mutate.backend.utility : toInternal; 68 69 data.mut_kinds = toInternal(v); 70 return this; 71 } 72 73 ExitStatusType run(ref Database db, FilesysIO fio) nothrow { 74 // trusted because the lifetime of the database is guaranteed to outlive any instances in this scope 75 auto db_ref = () @trusted { return &db; }(); 76 77 auto driver_data = DriverData(db_ref, fio, data.mut_kinds, new AutoCleanup, data.config); 78 79 try { 80 auto test_driver = TestDriver(driver_data); 81 82 while (test_driver.isRunning) { 83 test_driver.execute; 84 } 85 86 return test_driver.status; 87 } catch (Exception e) { 88 logger.error(e.msg).collectException; 89 } 90 91 return ExitStatusType.Errors; 92 } 93 } 94 95 struct DriverData { 96 Database* db; 97 FilesysIO filesysIO; 98 Mutation.Kind[] mutKind; 99 AutoCleanup autoCleanup; 100 ConfigMutationTest conf; 101 } 102 103 struct MeasureTestDurationResult { 104 bool ok; 105 Duration runtime; 106 } 107 108 /** Measure the time it takes to run the test command. 109 * 110 * The runtime is the lowest of three executions. Anything else is assumed to 111 * be variations in the system. 112 * 113 * If the tests fail (exit code isn't 0) any time then they are too unreliable 114 * to use for mutation testing. 115 * 116 * Params: 117 * cmd = test command to measure 118 */ 119 MeasureTestDurationResult measureTestCommand(ref TestRunner runner) @safe nothrow { 120 import std.algorithm : min; 121 import std.datetime.stopwatch : StopWatch, AutoStart; 122 import process; 123 124 if (runner.empty) { 125 collectException(logger.error("No test command(s) specified (--test-cmd)")); 126 return MeasureTestDurationResult(false); 127 } 128 129 static struct Rval { 130 TestResult result; 131 Duration runtime; 132 } 133 134 auto runTest() @safe { 135 auto sw = StopWatch(AutoStart.yes); 136 auto res = runner.run; 137 return Rval(res, sw.peek); 138 } 139 140 static void print(DrainElement[] data) @trusted { 141 import std.stdio : stdout, write; 142 143 foreach (l; data) { 144 write(l.byUTF8); 145 } 146 stdout.flush; 147 } 148 149 auto runtime = Duration.max; 150 bool failed; 151 for (int i; i < 3 && !failed; ++i) { 152 try { 153 auto res = runTest; 154 final switch (res.result.status) with (TestResult) { 155 case Status.passed: 156 runtime = min(runtime, res.runtime); 157 break; 158 case Status.failed: 159 goto case; 160 case Status.timeout: 161 goto case; 162 case Status.error: 163 failed = true; 164 print(res.result.output); 165 break; 166 } 167 } catch (Exception e) { 168 logger.error(e.msg).collectException; 169 failed = true; 170 } 171 } 172 173 return MeasureTestDurationResult(!failed, runtime); 174 } 175 176 struct TestDriver { 177 import std.datetime : SysTime; 178 import std.typecons : Unique; 179 import dextool.plugin.mutate.backend.database : Schemata, SchemataId, MutationStatusId; 180 import dextool.plugin.mutate.backend.test_mutant.source_mutant : MutationTestDriver, 181 MutationTestResult; 182 import dextool.plugin.mutate.backend.test_mutant.timeout : calculateTimeout, TimeoutFsm; 183 184 /// Runs the test commands. 185 TestRunner runner; 186 187 /// 188 TestCaseAnalyzer testCaseAnalyzer; 189 190 static struct Global { 191 DriverData data; 192 Unique!MutationTestDriver mut_driver; 193 194 TimeoutFsm timeoutFsm; 195 196 /// The time it takes to execute the test suite when no mutant is injected. 197 Duration testSuiteRuntime; 198 199 /// the next mutant to test, if there are any. 200 MutationEntry nextMutant; 201 202 // when the user manually configure the timeout it means that the 203 // timeout algorithm should not be used. 204 bool hardcodedTimeout; 205 206 /// Max time to run the mutation testing for. 207 SysTime maxRuntime; 208 209 /// Test commands to execute. 210 ShellCommand[] testCmds; 211 } 212 213 static struct UpdateTimeoutData { 214 long lastTimeoutIter; 215 } 216 217 static struct None { 218 } 219 220 static struct Initialize { 221 } 222 223 static struct PullRequest { 224 } 225 226 static struct PullRequestData { 227 import dextool.plugin.mutate.type : TestConstraint; 228 229 TestConstraint constraint; 230 long seed; 231 } 232 233 static struct SanityCheck { 234 bool sanityCheckFailed; 235 } 236 237 static struct AnalyzeTestCmdForTestCase { 238 TestCase[] foundTestCases; 239 } 240 241 static struct UpdateAndResetAliveMutants { 242 TestCase[] foundTestCases; 243 } 244 245 static struct ResetOldMutant { 246 bool doneTestingOldMutants; 247 } 248 249 static struct ResetOldMutantData { 250 /// Number of mutants that where reset. 251 long resetCount; 252 long maxReset; 253 } 254 255 static struct CleanupTempDirs { 256 } 257 258 static struct CheckMutantsLeft { 259 bool allMutantsTested; 260 } 261 262 static struct ParseStdin { 263 } 264 265 static struct PreCompileSut { 266 bool compilationError; 267 } 268 269 static struct FindTestCmds { 270 } 271 272 static struct ChooseMode { 273 } 274 275 static struct MeasureTestSuite { 276 bool unreliableTestSuite; 277 } 278 279 static struct PreMutationTest { 280 } 281 282 static struct MutationTest { 283 bool next; 284 bool mutationError; 285 MutationTestResult result; 286 } 287 288 static struct CheckTimeout { 289 bool timeoutUnchanged; 290 } 291 292 static struct NextSchemata { 293 bool hasSchema; 294 Schemata schemata; 295 } 296 297 static struct PreSchemata { 298 Schemata schemata; 299 300 bool error; 301 } 302 303 static struct SchemataTest { 304 import dextool.plugin.mutate.backend.test_mutant.schemata : MutationTestResult; 305 306 SchemataId id; 307 MutationTestResult[] result; 308 } 309 310 static struct SchemataTestResult { 311 import dextool.plugin.mutate.backend.test_mutant.schemata : MutationTestResult; 312 313 SchemataId id; 314 MutationTestResult[] result; 315 } 316 317 static struct SchemataRestore { 318 bool error; 319 } 320 321 static struct SchemataRestoreData { 322 static struct Original { 323 AbsolutePath path; 324 Blob original; 325 } 326 327 Original[] original; 328 } 329 330 static struct Done { 331 } 332 333 static struct Error { 334 } 335 336 static struct UpdateTimeout { 337 } 338 339 static struct NextPullRequestMutant { 340 bool noUnknownMutantsLeft; 341 } 342 343 static struct NextPullRequestMutantData { 344 import dextool.plugin.mutate.backend.database : MutationStatusId; 345 346 MutationStatusId[] mutants; 347 348 /// If set then stop after this many alive are found. 349 Nullable!int maxAlive; 350 /// number of alive mutants that has been found. 351 int alive; 352 } 353 354 static struct NextMutant { 355 bool noUnknownMutantsLeft; 356 } 357 358 static struct HandleTestResult { 359 MutationTestResult result; 360 } 361 362 static struct CheckRuntime { 363 bool reachedMax; 364 } 365 366 static struct SetMaxRuntime { 367 } 368 369 alias Fsm = dextool.fsm.Fsm!(None, Initialize, SanityCheck, 370 AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, ResetOldMutant, 371 CleanupTempDirs, CheckMutantsLeft, PreCompileSut, MeasureTestSuite, 372 PreMutationTest, NextMutant, MutationTest, HandleTestResult, 373 CheckTimeout, Done, Error, UpdateTimeout, CheckRuntime, 374 SetMaxRuntime, PullRequest, NextPullRequestMutant, ParseStdin, FindTestCmds, ChooseMode, 375 NextSchemata, PreSchemata, SchemataTest, SchemataTestResult, SchemataRestore); 376 alias LocalStateDataT = Tuple!(UpdateTimeoutData, NextPullRequestMutantData, 377 PullRequestData, ResetOldMutantData, SchemataRestoreData); 378 379 private { 380 Fsm fsm; 381 Global global; 382 TypeDataMap!(LocalStateDataT, UpdateTimeout, NextPullRequestMutant, 383 PullRequest, ResetOldMutant, SchemataRestore) local; 384 bool isRunning_ = true; 385 } 386 387 this(DriverData data) { 388 this.global = Global(data); 389 this.global.timeoutFsm = TimeoutFsm(data.mutKind); 390 this.global.hardcodedTimeout = !global.data.conf.mutationTesterRuntime.isNull; 391 local.get!PullRequest.constraint = global.data.conf.constraint; 392 local.get!PullRequest.seed = global.data.conf.pullRequestSeed; 393 local.get!NextPullRequestMutant.maxAlive = global.data.conf.maxAlive; 394 local.get!ResetOldMutant.maxReset = global.data.conf.oldMutantsNr; 395 this.global.testCmds = global.data.conf.mutationTester; 396 397 this.runner = TestRunner.make(global.data.conf.testPoolSize); 398 // using an unreasonable timeout to make it possible to analyze for 399 // test cases and measure the test suite. 400 this.runner.timeout = 999.dur!"hours"; 401 this.runner.put(data.conf.mutationTester); 402 403 // TODO: allow a user, as is for test_cmd, to specify an array of 404 // external analyzers. 405 this.testCaseAnalyzer = TestCaseAnalyzer(global.data.conf.mutationTestCaseBuiltin, 406 global.data.conf.mutationTestCaseAnalyze, global.data.autoCleanup); 407 } 408 409 static void execute_(ref TestDriver self) @trusted { 410 // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a 411 // graphical view of the state machine. 412 413 self.fsm.next!((None a) => fsm(Initialize.init), 414 (Initialize a) => fsm(SanityCheck.init), (SanityCheck a) { 415 if (a.sanityCheckFailed) 416 return fsm(Error.init); 417 if (self.global.data.conf.unifiedDiffFromStdin) 418 return fsm(ParseStdin.init); 419 return fsm(PreCompileSut.init); 420 }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) => fsm( 421 UpdateAndResetAliveMutants(a.foundTestCases)), 422 (UpdateAndResetAliveMutants a) => fsm(CheckMutantsLeft.init), (ResetOldMutant a) { 423 if (a.doneTestingOldMutants) 424 return fsm(Done.init); 425 return fsm(UpdateTimeout.init); 426 }, (CleanupTempDirs a) { 427 if (self.local.get!PullRequest.constraint.empty) 428 return fsm(NextSchemata.init); 429 return fsm(NextPullRequestMutant.init); 430 }, (CheckMutantsLeft a) { 431 if (a.allMutantsTested 432 && self.global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) 433 return fsm(Done.init); 434 return fsm(MeasureTestSuite.init); 435 }, (PreCompileSut a) { 436 if (a.compilationError) 437 return fsm(Error.init); 438 if (self.global.data.conf.testCommandDir.empty) 439 return fsm(ChooseMode.init); 440 return fsm(FindTestCmds.init); 441 }, (FindTestCmds a) { return fsm(ChooseMode.init); }, (ChooseMode a) { 442 if (!self.local.get!PullRequest.constraint.empty) 443 return fsm(PullRequest.init); 444 if (!self.global.data.conf.mutationTestCaseAnalyze.empty 445 || !self.global.data.conf.mutationTestCaseBuiltin.empty) 446 return fsm(AnalyzeTestCmdForTestCase.init); 447 return fsm(CheckMutantsLeft.init); 448 }, (PullRequest a) => fsm(CheckMutantsLeft.init), (MeasureTestSuite a) { 449 if (a.unreliableTestSuite) 450 return fsm(Error.init); 451 return fsm(SetMaxRuntime.init); 452 }, (SetMaxRuntime a) => fsm(UpdateTimeout.init), (NextPullRequestMutant a) { 453 if (a.noUnknownMutantsLeft) 454 return fsm(Done.init); 455 return fsm(PreMutationTest.init); 456 }, (NextSchemata a) { 457 if (a.hasSchema) 458 return fsm(PreSchemata(a.schemata)); 459 return fsm(NextMutant.init); 460 }, (PreSchemata a) { 461 if (a.error) 462 return fsm(Error.init); 463 return fsm(SchemataTest(a.schemata.id)); 464 }, (SchemataTest a) { return fsm(SchemataTestResult(a.id, a.result)); }, 465 (SchemataTestResult a) => fsm(SchemataRestore.init), (SchemataRestore a) { 466 if (a.error) 467 return fsm(Error.init); 468 return fsm(CheckRuntime.init); 469 }, (NextMutant a) { 470 if (a.noUnknownMutantsLeft) 471 return fsm(CheckTimeout.init); 472 return fsm(PreMutationTest.init); 473 }, (PreMutationTest a) => fsm(MutationTest.init), 474 (UpdateTimeout a) => fsm(CleanupTempDirs.init), (MutationTest a) { 475 if (a.next) 476 return fsm(HandleTestResult(a.result)); 477 else if (a.mutationError) 478 return fsm(Error.init); 479 return fsm(a); 480 }, (HandleTestResult a) => fsm(CheckRuntime.init), (CheckRuntime a) { 481 if (a.reachedMax) 482 return fsm(Done.init); 483 return fsm(UpdateTimeout.init); 484 }, (CheckTimeout a) { 485 if (a.timeoutUnchanged) 486 return fsm(ResetOldMutant.init); 487 return fsm(UpdateTimeout.init); 488 }, (Done a) => fsm(a), (Error a) => fsm(a),); 489 490 self.fsm.act!(self); 491 } 492 493 nothrow: 494 void execute() { 495 try { 496 execute_(this); 497 } catch (Exception e) { 498 logger.warning(e.msg).collectException; 499 } 500 } 501 502 bool isRunning() { 503 return isRunning_; 504 } 505 506 ExitStatusType status() { 507 if (fsm.isState!Done) 508 return ExitStatusType.Ok; 509 return ExitStatusType.Errors; 510 } 511 512 void opCall(None data) { 513 } 514 515 void opCall(Initialize data) { 516 } 517 518 void opCall(Done data) { 519 global.data.autoCleanup.cleanup; 520 logger.info("Done!").collectException; 521 isRunning_ = false; 522 } 523 524 void opCall(Error data) { 525 global.data.autoCleanup.cleanup; 526 isRunning_ = false; 527 } 528 529 void opCall(ref SanityCheck data) { 530 // #SPC-sanity_check_db_vs_filesys 531 import colorlog : color, Color; 532 import dextool.plugin.mutate.backend.utility : checksum, Checksum; 533 534 logger.info("Checking that the file(s) on the filesystem match the database") 535 .collectException; 536 537 auto failed = appender!(string[])(); 538 foreach (file; spinSql!(() { return global.data.db.getFiles; })) { 539 auto db_checksum = spinSql!(() { 540 return global.data.db.getFileChecksum(file); 541 }); 542 543 try { 544 auto abs_f = AbsolutePath(file, global.data.filesysIO.getOutputDir); 545 auto f_checksum = checksum(global.data.filesysIO.makeInput(abs_f).content[]); 546 if (db_checksum != f_checksum) { 547 failed.put(abs_f); 548 } 549 } catch (Exception e) { 550 // assume it is a problem reading the file or something like that. 551 failed.put(file); 552 logger.warningf("%s: %s", file, e.msg).collectException; 553 } 554 } 555 556 data.sanityCheckFailed = failed.data.length != 0; 557 558 if (data.sanityCheckFailed) { 559 logger.error("Detected that file(s) has changed since last analyze where done") 560 .collectException; 561 logger.error("Either restore the file(s) or rerun the analyze").collectException; 562 foreach (f; failed.data) { 563 logger.info(f).collectException; 564 } 565 } else { 566 logger.info("Ok".color(Color.green)).collectException; 567 } 568 } 569 570 void opCall(ParseStdin data) { 571 import dextool.plugin.mutate.backend.diff_parser : diffFromStdin; 572 import dextool.plugin.mutate.type : Line; 573 574 try { 575 auto constraint = local.get!PullRequest.constraint; 576 foreach (pkv; diffFromStdin.toRange(global.data.filesysIO.getOutputDir)) { 577 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array; 578 } 579 local.get!PullRequest.constraint = constraint; 580 } catch (Exception e) { 581 logger.warning(e.msg).collectException; 582 } 583 } 584 585 void opCall(ref AnalyzeTestCmdForTestCase data) { 586 import std.datetime.stopwatch : StopWatch; 587 import dextool.plugin.mutate.backend.type : TestCase; 588 589 TestCase[] found; 590 try { 591 auto res = runTester(runner); 592 auto analyze = testCaseAnalyzer.analyze(res.output, Yes.allFound); 593 594 analyze.match!((TestCaseAnalyzer.Success a) { found = a.found; }, 595 (TestCaseAnalyzer.Unstable a) { 596 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable); 597 found = a.found; 598 }, (TestCaseAnalyzer.Failed a) { 599 logger.warning("The parser that analyze the output for test case(s) failed"); 600 }); 601 } catch (Exception e) { 602 logger.warning(e.msg).collectException; 603 } 604 605 warnIfConflictingTestCaseIdentifiers(found); 606 data.foundTestCases = found; 607 } 608 609 void opCall(UpdateAndResetAliveMutants data) { 610 import std.traits : EnumMembers; 611 612 // the test cases before anything has potentially changed. 613 auto old_tcs = spinSql!(() { 614 Set!string old_tcs; 615 foreach (tc; global.data.db.getDetectedTestCases) { 616 old_tcs.add(tc.name); 617 } 618 return old_tcs; 619 }); 620 621 void transaction() @safe { 622 final switch (global.data.conf.onRemovedTestCases) with ( 623 ConfigMutationTest.RemovedTestCases) { 624 case doNothing: 625 global.data.db.addDetectedTestCases(data.foundTestCases); 626 break; 627 case remove: 628 foreach (id; global.data.db.setDetectedTestCases(data.foundTestCases)) { 629 global.data.db.updateMutationStatus(id, Mutation.Status.unknown); 630 } 631 break; 632 } 633 } 634 635 auto found_tcs = spinSql!(() @trusted { 636 auto tr = global.data.db.transaction; 637 transaction(); 638 639 Set!string found_tcs; 640 foreach (tc; global.data.db.getDetectedTestCases) { 641 found_tcs.add(tc.name); 642 } 643 644 tr.commit; 645 return found_tcs; 646 }); 647 648 printDroppedTestCases(old_tcs, found_tcs); 649 650 if (hasNewTestCases(old_tcs, found_tcs) 651 && global.data.conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) { 652 logger.info("Resetting alive mutants").collectException; 653 // there is no use in trying to limit the mutants to reset to those 654 // that are part of "this" execution because new test cases can 655 // only mean one thing: re-test all alive mutants. 656 spinSql!(() { 657 global.data.db.resetMutant([EnumMembers!(Mutation.Kind)], 658 Mutation.Status.alive, Mutation.Status.unknown); 659 }); 660 } 661 } 662 663 void opCall(ref ResetOldMutant data) { 664 import dextool.plugin.mutate.backend.database.type; 665 666 if (global.data.conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) { 667 data.doneTestingOldMutants = true; 668 return; 669 } 670 if (Clock.currTime > global.maxRuntime) { 671 data.doneTestingOldMutants = true; 672 return; 673 } 674 if (local.get!ResetOldMutant.resetCount >= local.get!ResetOldMutant.maxReset) { 675 data.doneTestingOldMutants = true; 676 return; 677 } 678 679 local.get!ResetOldMutant.resetCount++; 680 681 logger.infof("Resetting an old mutant (%s/%s)", local.get!ResetOldMutant.resetCount, 682 local.get!ResetOldMutant.maxReset).collectException; 683 auto oldest = spinSql!(() { 684 return global.data.db.getOldestMutants(global.data.mutKind, 1); 685 }); 686 687 foreach (const old; oldest) { 688 logger.info("Last updated ", old.updated).collectException; 689 spinSql!(() { 690 global.data.db.updateMutationStatus(old.id, Mutation.Status.unknown); 691 }); 692 } 693 } 694 695 void opCall(CleanupTempDirs data) { 696 global.data.autoCleanup.cleanup; 697 } 698 699 void opCall(ref CheckMutantsLeft data) { 700 spinSql!(() { global.timeoutFsm.execute(*global.data.db); }); 701 702 data.allMutantsTested = global.timeoutFsm.output.done; 703 704 if (global.timeoutFsm.output.done) { 705 logger.info("All mutants are tested").collectException; 706 } 707 } 708 709 void opCall(ref PreCompileSut data) { 710 import std.stdio : write; 711 import colorlog : color, Color; 712 import process; 713 714 logger.info("Checking the build command").collectException; 715 try { 716 auto output = appender!(DrainElement[])(); 717 auto p = pipeProcess(global.data.conf.mutationCompile.value).sandbox.drain(output, 718 999.dur!"hours").scopeKill; 719 if (p.wait == 0) { 720 logger.info("Ok".color(Color.green)); 721 return; 722 } 723 724 logger.error("Build commman failed"); 725 foreach (l; output.data) { 726 write(l.byUTF8); 727 } 728 } catch (Exception e) { 729 // unable to for example execute the compiler 730 logger.error(e.msg).collectException; 731 } 732 733 data.compilationError = true; 734 } 735 736 void opCall(FindTestCmds data) { 737 auto cmds = appender!(ShellCommand[])(); 738 foreach (root; global.data.conf.testCommandDir) { 739 try { 740 cmds.put(findExecutables(root.AbsolutePath) 741 .map!(a => ShellCommand([a] ~ global.data.conf.testCommandDirFlag))); 742 } catch (Exception e) { 743 logger.warning(e.msg).collectException; 744 } 745 } 746 747 if (!cmds.data.empty) { 748 this.runner.put(cmds.data); 749 this.global.testCmds ~= cmds.data; 750 logger.infof("Found test commands in %s:", 751 global.data.conf.testCommandDir).collectException; 752 foreach (c; cmds.data) { 753 logger.info(c).collectException; 754 } 755 } 756 } 757 758 void opCall(ChooseMode data) { 759 } 760 761 void opCall(PullRequest data) { 762 import std.array : appender; 763 import std.random : randomCover, Mt19937_64; 764 import dextool.plugin.mutate.backend.database : MutationStatusId; 765 import dextool.plugin.mutate.backend.type : SourceLoc; 766 import dextool.set; 767 768 Set!MutationStatusId mut_ids; 769 770 foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) { 771 const file_id = spinSql!(() => global.data.db.getFileId(kv.key)); 772 if (file_id.isNull) { 773 logger.infof("The file %s do not exist in the database. Skipping...", 774 kv.key).collectException; 775 continue; 776 } 777 778 foreach (l; kv.value) { 779 auto mutants = spinSql!(() { 780 return global.data.db.getMutationsOnLine(global.data.mutKind, 781 file_id.get, SourceLoc(l.value, 0)); 782 }); 783 784 const pre_cnt = mut_ids.length; 785 foreach (v; mutants) 786 mut_ids.add(v); 787 788 logger.infof(mut_ids.length - pre_cnt > 0, "Found %s mutant(s) to test (%s:%s)", 789 mut_ids.length - pre_cnt, kv.key, l.value).collectException; 790 } 791 } 792 793 logger.infof(!mut_ids.empty, "Found %s mutants in the diff", 794 mut_ids.length).collectException; 795 796 const seed = local.get!PullRequest.seed; 797 logger.infof("Using random seed %s when choosing the mutants to test", 798 seed).collectException; 799 auto rng = Mt19937_64(seed); 800 local.get!NextPullRequestMutant.mutants = mut_ids.toArray.sort.randomCover(rng).array; 801 logger.trace("Test sequence ", local.get!NextPullRequestMutant.mutants).collectException; 802 803 if (mut_ids.empty) { 804 logger.warning("None of the locations specified with -L exists").collectException; 805 logger.info("Available files are:").collectException; 806 foreach (f; spinSql!(() => global.data.db.getFiles)) 807 logger.info(f).collectException; 808 } 809 } 810 811 void opCall(ref MeasureTestSuite data) { 812 if (!global.data.conf.mutationTesterRuntime.isNull) { 813 global.testSuiteRuntime = global.data.conf.mutationTesterRuntime.get; 814 return; 815 } 816 817 logger.info("Measuring the runtime of the test command: ", 818 global.testCmds).collectException; 819 820 const tester = () { 821 try { 822 // need to measure the test suite single threaded to get the "worst" 823 // test case execution time because if multiple instances are running 824 // on the same computer the available CPU resources are variable. This 825 // reduces the number of mutants marked as timeout. Further 826 // improvements in the future could be to check the loadavg and let it 827 // affect the number of threads. 828 runner.poolSize = 1; 829 scope (exit) 830 runner.poolSize = global.data.conf.testPoolSize; 831 return measureTestCommand(runner); 832 } catch (Exception e) { 833 logger.error(e.msg).collectException; 834 return MeasureTestDurationResult(false); 835 } 836 }(); 837 838 if (tester.ok) { 839 // The sampling of the test suite become too unreliable when the timeout is <1s. 840 // This is a quick and dirty fix. 841 // A proper fix requires an update of the sampler in runTester. 842 auto t = tester.runtime < 1.dur!"seconds" ? 1.dur!"seconds" : tester.runtime; 843 logger.info("Test command runtime: ", t).collectException; 844 global.testSuiteRuntime = t; 845 } else { 846 data.unreliableTestSuite = true; 847 logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected") 848 .collectException; 849 } 850 } 851 852 void opCall(PreMutationTest) { 853 auto factory(DriverData d, MutationEntry mutp, TestRunner* runner) @safe nothrow { 854 import std.typecons : Unique; 855 import dextool.plugin.mutate.backend.test_mutant.interface_ : GatherTestCase; 856 857 try { 858 auto global = MutationTestDriver.Global(d.filesysIO, d.db, mutp, runner); 859 return Unique!MutationTestDriver(new MutationTestDriver(global, 860 MutationTestDriver.TestMutantData(!(d.conf.mutationTestCaseAnalyze.empty 861 && d.conf.mutationTestCaseBuiltin.empty), d.conf.mutationCompile,), 862 MutationTestDriver.TestCaseAnalyzeData(&testCaseAnalyzer))); 863 } catch (Exception e) { 864 logger.error(e.msg).collectException; 865 } 866 assert(0, "should not happen"); 867 } 868 869 runner.timeout = calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime); 870 global.mut_driver = factory(global.data, global.nextMutant, () @trusted { 871 return &runner; 872 }()); 873 } 874 875 void opCall(ref MutationTest data) { 876 if (global.mut_driver.isRunning) { 877 global.mut_driver.execute(); 878 } else if (global.mut_driver.stopBecauseError) { 879 data.mutationError = true; 880 } else { 881 data.result = global.mut_driver.result; 882 data.next = true; 883 } 884 } 885 886 void opCall(ref CheckTimeout data) { 887 data.timeoutUnchanged = global.hardcodedTimeout || global.timeoutFsm.output.done; 888 } 889 890 void opCall(UpdateTimeout) { 891 spinSql!(() { global.timeoutFsm.execute(*global.data.db); }); 892 893 const lastIter = local.get!UpdateTimeout.lastTimeoutIter; 894 895 if (lastIter != global.timeoutFsm.output.iter) { 896 logger.infof("Changed the timeout from %s to %s (iteration %s)", 897 calculateTimeout(lastIter, global.testSuiteRuntime), 898 calculateTimeout(global.timeoutFsm.output.iter, global.testSuiteRuntime), 899 global.timeoutFsm.output.iter).collectException; 900 local.get!UpdateTimeout.lastTimeoutIter = global.timeoutFsm.output.iter; 901 } 902 } 903 904 void opCall(ref NextPullRequestMutant data) { 905 global.nextMutant = MutationEntry.init; 906 data.noUnknownMutantsLeft = true; 907 908 while (!local.get!NextPullRequestMutant.mutants.empty) { 909 const id = local.get!NextPullRequestMutant.mutants[$ - 1]; 910 const status = spinSql!(() => global.data.db.getMutationStatus(id)); 911 912 if (status.isNull) 913 continue; 914 915 if (status.get == Mutation.Status.alive) { 916 local.get!NextPullRequestMutant.alive++; 917 } 918 919 if (status.get != Mutation.Status.unknown) { 920 local.get!NextPullRequestMutant.mutants 921 = local.get!NextPullRequestMutant.mutants[0 .. $ - 1]; 922 continue; 923 } 924 925 const info = spinSql!(() => global.data.db.getMutantsInfo(global.data.mutKind, [ 926 id 927 ])); 928 if (info.empty) 929 continue; 930 931 global.nextMutant = spinSql!(() => global.data.db.getMutation(info[0].id)); 932 data.noUnknownMutantsLeft = false; 933 break; 934 } 935 936 if (!local.get!NextPullRequestMutant.maxAlive.isNull) { 937 const alive = local.get!NextPullRequestMutant.alive; 938 const maxAlive = local.get!NextPullRequestMutant.maxAlive.get; 939 logger.infof(alive > 0, "Found %s/%s alive mutants", alive, maxAlive).collectException; 940 if (alive >= maxAlive) { 941 data.noUnknownMutantsLeft = true; 942 } 943 } 944 } 945 946 void opCall(ref NextMutant data) { 947 global.nextMutant = MutationEntry.init; 948 949 auto next = spinSql!(() { 950 return global.data.db.nextMutation(global.data.mutKind); 951 }); 952 953 data.noUnknownMutantsLeft = next.st == NextMutationEntry.Status.done; 954 955 if (!next.entry.isNull) { 956 global.nextMutant = next.entry.get; 957 } 958 } 959 960 void opCall(HandleTestResult data) { 961 void statusUpdate(MutationTestResult.StatusUpdate result) { 962 import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus; 963 964 const cnt_action = () { 965 if (result.status == Mutation.Status.alive) 966 return Database.CntAction.incr; 967 return Database.CntAction.reset; 968 }(); 969 970 auto statusId = spinSql!(() { 971 return global.data.db.getMutationStatusId(result.id); 972 }); 973 if (statusId.isNull) 974 return; 975 976 spinSql!(() @trusted { 977 auto t = global.data.db.transaction; 978 updateMutantStatus(*global.data.db, statusId.get, 979 result.status, global.timeoutFsm.output.iter); 980 global.data.db.updateMutation(statusId.get, cnt_action); 981 global.data.db.updateMutation(statusId.get, result.testTime); 982 global.data.db.updateMutationTestCases(statusId.get, result.testCases); 983 t.commit; 984 }); 985 986 logger.infof("%s %s (%s)", result.id, result.status, result.testTime).collectException; 987 logger.infof(!result.testCases.empty, `%s killed by [%-(%s, %)]`, 988 result.id, result.testCases.sort.map!"a.name").collectException; 989 } 990 991 data.result.value.match!((MutationTestResult.NoResult a) {}, 992 (MutationTestResult.StatusUpdate a) => statusUpdate(a)); 993 } 994 995 void opCall(SetMaxRuntime) { 996 global.maxRuntime = Clock.currTime + global.data.conf.maxRuntime; 997 } 998 999 void opCall(ref CheckRuntime data) { 1000 data.reachedMax = Clock.currTime > global.maxRuntime; 1001 if (data.reachedMax) { 1002 logger.infof("Max runtime of %s reached at %s", 1003 global.data.conf.maxRuntime, global.maxRuntime).collectException; 1004 } 1005 } 1006 1007 void opCall(ref NextSchemata data) { 1008 auto schema = spinSql!(() { return global.data.db.nextSchemata; }); 1009 1010 data.hasSchema = !schema.isNull; 1011 if (!schema.isNull) { 1012 data.schemata = schema.get; 1013 logger.info("Running schemata ", data.schemata.id).collectException; 1014 } 1015 } 1016 1017 void opCall(ref PreSchemata data) { 1018 import std.format : format; 1019 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 1020 1021 Blob makeSchemata(Blob original, SchemataFragment[] fragments) { 1022 import blob_model; 1023 1024 Edit[] edits; 1025 foreach (a; fragments) { 1026 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text); 1027 } 1028 auto m = merge(original, edits); 1029 return change(new Blob(original.uri, original.content), m.edits); 1030 } 1031 1032 SchemataFragment[] fragments(Path p) { 1033 return data.schemata.fragments.filter!(a => a.file == p).array; 1034 } 1035 1036 SchemataRestoreData.Original[] orgs; 1037 try { 1038 auto files = data.schemata.fragments.map!(a => a.file).toSet; 1039 foreach (f; files.toRange) { 1040 const absf = AbsolutePath(f, global.data.filesysIO.getOutputDir); 1041 1042 orgs ~= SchemataRestoreData.Original(absf, global.data.filesysIO.makeInput(absf)); 1043 1044 // writing the schemata. 1045 auto s = makeSchemata(orgs[$ - 1].original, fragments(f)); 1046 global.data.filesysIO.makeOutput(absf).write(s); 1047 1048 if (global.data.conf.logSchemata) { 1049 global.data.filesysIO.makeOutput(AbsolutePath(format!"%s.schema%s"(absf, 1050 data.schemata.id).Path)).write(s); 1051 } 1052 } 1053 } catch (Exception e) { 1054 logger.warning(e.msg).collectException; 1055 data.error = true; 1056 } 1057 local.get!SchemataRestore.original = orgs; 1058 } 1059 1060 void opCall(ref SchemataTest data) { 1061 import dextool.plugin.mutate.backend.test_mutant.schemata; 1062 1063 bool successCompile; 1064 compile(global.data.conf.mutationCompile).match!((Mutation.Status a) {}, (bool success) { 1065 successCompile = success; 1066 },); 1067 1068 if (!successCompile) { 1069 logger.infof("Schemata %s failed to compile", data.id).collectException; 1070 return; 1071 } 1072 1073 auto mutants = spinSql!(() { 1074 return global.data.db.getSchemataMutants(data.id); 1075 }); 1076 1077 try { 1078 auto driver = SchemataTestDriver(global.data.filesysIO, &runner, 1079 global.data.db, &testCaseAnalyzer, mutants); 1080 while (driver.isRunning) { 1081 driver.execute; 1082 } 1083 } catch (Exception e) { 1084 logger.info(e.msg).collectException; 1085 logger.warning("Failed executing schemata ", data.id).collectException; 1086 } 1087 } 1088 1089 void opCall(SchemataTestResult data) { 1090 foreach (m; data.result) { 1091 spinSql!(() { 1092 global.data.db.updateMutation(m.id, m.status, m.testTime); 1093 }); 1094 spinSql!(() { 1095 global.data.db.updateMutationTestCases(m.id, m.testCases); 1096 }); 1097 } 1098 1099 spinSql!(() { global.data.db.finishSchemata(data.id); }); 1100 } 1101 1102 void opCall(ref SchemataRestore data) { 1103 foreach (o; local.get!SchemataRestore.original) { 1104 try { 1105 global.data.filesysIO.makeOutput(o.path).write(o.original.content); 1106 } catch (Exception e) { 1107 logger.error(e.msg).collectException; 1108 data.error = true; 1109 } 1110 } 1111 } 1112 } 1113 1114 private: 1115 1116 /** Compare the old test cases with those that have been found this run. 1117 * 1118 * TODO: the side effect that this function print to the console is NOT good. 1119 */ 1120 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow { 1121 bool rval; 1122 1123 auto new_tcs = found_tcs.setDifference(old_tcs); 1124 foreach (tc; new_tcs.toRange) { 1125 logger.info(!rval, "Found new test case(s):").collectException; 1126 logger.infof("%s", tc).collectException; 1127 rval = true; 1128 } 1129 1130 return rval; 1131 } 1132 1133 /** Compare old and new test cases to print those that have been removed. 1134 */ 1135 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow { 1136 auto diff = old_tcs.setDifference(changed_tcs); 1137 auto removed = diff.toArray; 1138 1139 logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException; 1140 foreach (tc; removed) { 1141 logger.infof("%s", tc).collectException; 1142 } 1143 } 1144 1145 /// Returns: true if all tests cases have unique identifiers 1146 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow { 1147 Set!TestCase checked; 1148 bool conflict; 1149 1150 foreach (tc; found_tcs) { 1151 if (checked.contains(tc)) { 1152 logger.info(!conflict, 1153 "Found test cases that do not have global, unique identifiers") 1154 .collectException; 1155 logger.info(!conflict, 1156 "This make the report of test cases that has killed zero mutants unreliable") 1157 .collectException; 1158 logger.info("%s", tc).collectException; 1159 conflict = true; 1160 } 1161 } 1162 }