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