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