1 /** 2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 */ 10 module dextool.plugin.mutate.backend.test_mutant; 11 12 import core.time : Duration, dur; 13 import logger = std.experimental.logger; 14 import std.algorithm : map, filter, joiner, among, max; 15 import std.array : empty, array, appender, replace; 16 import std.datetime : SysTime, Clock; 17 import std.datetime.stopwatch : StopWatch, AutoStart; 18 import std.exception : collectException; 19 import std.format : format; 20 import std.random : randomCover; 21 import std.traits : EnumMembers; 22 import std.typecons : Nullable, Tuple, Yes, tuple; 23 24 import blob_model : Blob; 25 import miniorm : spinSql, silentLog; 26 import my.actor; 27 import my.container.vector; 28 import my.fsm : Fsm, next, act, get, TypeDataMap; 29 import my.gc.refc; 30 import my.hash : Checksum64; 31 import my.named_type; 32 import my.optional; 33 import my.set; 34 import proc : DrainElement; 35 import sumtype; 36 static import my.fsm; 37 38 import dextool.plugin.mutate.backend.database : Database, MutationEntry, 39 NextMutationEntry, TestFile, ChecksumTestCmdOriginal; 40 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 41 import dextool.plugin.mutate.backend.test_mutant.common; 42 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, 43 findExecutables, TestRunResult = TestResult; 44 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor; 45 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm; 46 import dextool.plugin.mutate.backend.type : Mutation, TestCase, ExitStatus; 47 import dextool.plugin.mutate.config; 48 import dextool.plugin.mutate.type : ShellCommand; 49 import dextool.type : AbsolutePath, ExitStatusType, Path; 50 51 @safe: 52 53 auto makeTestMutant() { 54 return BuildTestMutant(); 55 } 56 57 private: 58 59 struct BuildTestMutant { 60 @safe: 61 62 import dextool.plugin.mutate.type : MutationKind; 63 64 private struct InternalData { 65 FilesysIO filesys_io; 66 ConfigMutationTest config; 67 ConfigSchema schemaConf; 68 ConfigCoverage covConf; 69 } 70 71 private InternalData data; 72 73 auto config(ConfigMutationTest c) @trusted nothrow { 74 data.config = c; 75 return this; 76 } 77 78 auto config(ConfigSchema c) @trusted nothrow { 79 data.schemaConf = c; 80 return this; 81 } 82 83 auto config(ConfigCoverage c) @trusted nothrow { 84 data.covConf = c; 85 return this; 86 } 87 88 ExitStatusType run(const AbsolutePath dbPath, FilesysIO fio) @trusted { 89 try { 90 auto db = spinSql!(() => Database.make(dbPath))(dbOpenTimeout); 91 return internalRun(dbPath, &db, fio); 92 } catch (Exception e) { 93 logger.error(e.msg).collectException; 94 } 95 96 return ExitStatusType.Errors; 97 } 98 99 private ExitStatusType internalRun(AbsolutePath dbPath, Database* db, FilesysIO fio) { 100 auto system = makeSystem; 101 102 auto cleanup = new AutoCleanup; 103 scope (exit) 104 cleanup.cleanup; 105 106 auto test_driver = TestDriver(dbPath, db, () @trusted { return &system; }(), 107 fio, cleanup, data.config, data.covConf, data.schemaConf); 108 109 while (test_driver.isRunning) { 110 test_driver.execute; 111 } 112 113 return test_driver.status; 114 } 115 } 116 117 struct MeasureTestDurationResult { 118 bool ok; 119 Duration[] runtime; 120 } 121 122 /** Measure the time it takes to run the test command. 123 * 124 * The runtime is the lowest of three executions. Anything else is assumed to 125 * be variations in the system. 126 * 127 * If the tests fail (exit code isn't 0) any time then they are too unreliable 128 * to use for mutation testing. 129 * 130 * Params: 131 * runner = ? 132 * samples = number of times to run the test suite 133 */ 134 MeasureTestDurationResult measureTestCommand(ref TestRunner runner, int samples) @safe nothrow { 135 import std.algorithm : min; 136 import proc; 137 138 if (runner.empty) { 139 collectException(logger.error("No test command(s) specified (--test-cmd)")); 140 return MeasureTestDurationResult(false); 141 } 142 143 static struct Rval { 144 TestRunResult result; 145 Duration runtime; 146 } 147 148 auto runTest() @safe { 149 auto sw = StopWatch(AutoStart.yes); 150 auto res = runner.run(4.dur!"hours"); 151 return Rval(res, sw.peek); 152 } 153 154 static void print(TestRunResult res) @trusted { 155 import std.stdio : stdout, write; 156 157 foreach (kv; res.output.byKeyValue) { 158 logger.info("test_cmd: ", kv.key); 159 foreach (l; kv.value) 160 write(l.byUTF8); 161 } 162 163 stdout.flush; 164 } 165 166 static void printFailing(ref TestRunResult res) { 167 print(res); 168 logger.info("failing commands: ", res.output.byKey); 169 logger.info("exit status: ", res.exitStatus.get); 170 } 171 172 Duration[] runtimes; 173 bool failed; 174 for (int i; i < samples && !failed; ++i) { 175 try { 176 auto res = runTest; 177 final switch (res.result.status) with (TestRunResult) { 178 case Status.passed: 179 runtimes ~= res.runtime; 180 break; 181 case Status.failed: 182 goto case; 183 case Status.timeout: 184 goto case; 185 case Status.memOverload: 186 goto case; 187 case Status.error: 188 failed = true; 189 printFailing(res.result); 190 break; 191 } 192 logger.infof("%s: Measured test command runtime %s", i, res.runtime); 193 } catch (Exception e) { 194 logger.error(e.msg).collectException; 195 failed = true; 196 } 197 } 198 199 return MeasureTestDurationResult(!failed, runtimes); 200 } 201 202 struct TestDriver { 203 import std.datetime : SysTime; 204 import dextool.plugin.mutate.backend.database : SchemataId, MutationStatusId; 205 import dextool.plugin.mutate.backend.test_mutant.source_mutant : MutationTestDriver; 206 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig; 207 import dextool.plugin.mutate.type : MutationOrder; 208 209 Database* db; 210 AbsolutePath dbPath; 211 212 FilesysIO filesysIO; 213 AutoCleanup autoCleanup; 214 215 ConfigMutationTest conf; 216 ConfigSchema schemaConf; 217 ConfigCoverage covConf; 218 219 System* system; 220 221 /// Async communication with the database 222 DbSaveActor.Address dbSave; 223 224 /// Async stat update from the database every 30s. 225 StatActor.Address stat; 226 227 /// Runs the test commands. 228 TestRunner runner; 229 230 /// 231 TestCaseAnalyzer testCaseAnalyzer; 232 233 /// Stop conditions (most of them) 234 TestStopCheck stopCheck; 235 236 /// assuming that there are no more than 100 instances running in 237 /// parallel. 238 uint maxParallelInstances; 239 240 // need to use 10000 because in an untested code base it is not 241 // uncommon for mutants being in the thousands. 242 enum long unknownWeight = 10000; 243 // using a factor 1000 to make a pull request mutant very high prio 244 enum long pullRequestWeight = unknownWeight * 1000; 245 246 TimeoutFsm timeoutFsm; 247 248 /// the next mutant to test, if there are any. 249 MutationEntry nextMutant; 250 251 TimeoutConfig timeout; 252 253 /// Test commands to execute. 254 ShellCommand[] testCmds; 255 256 // The order to test mutants. It is either affected by the user directly or if pull request mode is activated. 257 MutationOrder mutationOrder; 258 259 static struct UpdateTimeoutData { 260 long lastTimeoutIter; 261 } 262 263 static struct None { 264 } 265 266 static struct Initialize { 267 bool halt; 268 } 269 270 static struct PullRequest { 271 } 272 273 static struct IncreaseFilePrio { 274 } 275 276 static struct PullRequestData { 277 import dextool.plugin.mutate.type : TestConstraint; 278 279 TestConstraint constraint; 280 } 281 282 static struct SanityCheck { 283 bool sanityCheckFailed; 284 } 285 286 static struct AnalyzeTestCmdForTestCase { 287 bool failed; 288 TestCase[][ShellCommand] foundTestCases; 289 } 290 291 static struct UpdateAndResetAliveMutants { 292 TestCase[][ShellCommand] foundTestCases; 293 } 294 295 static struct RetestOldMutant { 296 } 297 298 static struct ResetOldMutantData { 299 /// Number of mutants that where reset. 300 long maxReset; 301 NamedType!(double, Tag!"OldMutantPercentage", double.init, TagStringable) resetPercentage; 302 } 303 304 static struct Cleanup { 305 } 306 307 static struct CheckMutantsLeft { 308 bool allMutantsTested; 309 } 310 311 static struct SaveMutationScore { 312 } 313 314 static struct UpdateTestCaseTag { 315 } 316 317 static struct ParseStdin { 318 } 319 320 static struct PreCompileSut { 321 bool compilationError; 322 } 323 324 static struct FindTestCmds { 325 } 326 327 static struct UpdateTestCmds { 328 } 329 330 static struct ChooseMode { 331 } 332 333 static struct MeasureTestSuite { 334 bool unreliableTestSuite; 335 } 336 337 static struct MutationTest { 338 NamedType!(bool, Tag!"MutationError", bool.init, TagStringable) mutationError; 339 MutationTestResult[] result; 340 } 341 342 static struct MutationTestData { 343 TestBinaryDb testBinaryDb; 344 } 345 346 static struct CheckTimeout { 347 bool timeoutUnchanged; 348 } 349 350 static struct NextSchemata { 351 } 352 353 static struct NextSchemataData { 354 enum State { 355 first, 356 runOnce, 357 done 358 } 359 360 State runSchema; 361 } 362 363 static struct SchemataTest { 364 bool fatalError; 365 // stop mutation testing because the last schema has been used and the 366 // user has configured that the testing should stop now. 367 NamedType!(bool, Tag!"StopTesting", bool.init, TagStringable, ImplicitConvertable) stop; 368 } 369 370 static struct Done { 371 } 372 373 static struct Error { 374 } 375 376 static struct UpdateTimeout { 377 } 378 379 static struct CheckPullRequestMutant { 380 NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft; 381 } 382 383 static struct CheckPullRequestMutantData { 384 long startWorklistCnt; 385 long stopAfter; 386 } 387 388 static struct NextMutant { 389 NamedType!(bool, Tag!"NoUnknown", bool.init, TagStringable, ImplicitConvertable) noUnknownMutantsLeft; 390 } 391 392 static struct NextMutantData { 393 import dextool.plugin.mutate.backend.database.type : MutationId; 394 395 // because of the asynchronous nature it may be so that the result of 396 // the last executed hasn't finished being written to the DB when we 397 // request a new mutant. This is used to block repeating the same 398 // mutant. 399 MutationStatusId lastTested; 400 } 401 402 static struct HandleTestResult { 403 MutationTestResult[] result; 404 } 405 406 static struct CheckStopCond { 407 bool halt; 408 } 409 410 static struct OverloadCheck { 411 bool sleep; 412 } 413 414 static struct ContinuesCheckTestSuite { 415 bool ok; 416 } 417 418 static struct ContinuesCheckTestSuiteData { 419 long lastWorklistCnt; 420 SysTime lastCheck; 421 } 422 423 static struct Stop { 424 } 425 426 static struct Coverage { 427 bool propagate; 428 bool fatalError; 429 } 430 431 static struct PropagateCoverage { 432 } 433 434 static struct ChecksumTestCmds { 435 } 436 437 static struct SaveTestBinary { 438 } 439 440 alias Fsm = my.fsm.Fsm!(None, Initialize, SanityCheck, 441 AnalyzeTestCmdForTestCase, UpdateAndResetAliveMutants, RetestOldMutant, 442 Cleanup, CheckMutantsLeft, PreCompileSut, MeasureTestSuite, NextMutant, 443 MutationTest, HandleTestResult, CheckTimeout, Done, Error, 444 UpdateTimeout, CheckStopCond, PullRequest, IncreaseFilePrio, 445 CheckPullRequestMutant, ParseStdin, FindTestCmds, UpdateTestCmds, 446 ChooseMode, SchemataTest, Stop, SaveMutationScore, UpdateTestCaseTag, 447 OverloadCheck, Coverage, PropagateCoverage, ContinuesCheckTestSuite, 448 ChecksumTestCmds, SaveTestBinary, NextSchemata); 449 alias LocalStateDataT = Tuple!(UpdateTimeoutData, CheckPullRequestMutantData, PullRequestData, ResetOldMutantData, 450 ContinuesCheckTestSuiteData, MutationTestData, NextMutantData, NextSchemataData); 451 452 private { 453 Fsm fsm; 454 TypeDataMap!(LocalStateDataT, UpdateTimeout, CheckPullRequestMutant, PullRequest, 455 RetestOldMutant, ContinuesCheckTestSuite, MutationTest, NextMutant, NextSchemata) local; 456 bool isRunning_ = true; 457 bool isDone = false; 458 } 459 460 this(AbsolutePath dbPath, Database* db, System* sys, FilesysIO filesysIO, 461 AutoCleanup autoCleanup, ConfigMutationTest conf, 462 ConfigCoverage coverage, ConfigSchema schema) { 463 this.db = db; 464 this.dbPath = dbPath; 465 466 this.system = sys; 467 468 this.filesysIO = filesysIO; 469 this.autoCleanup = autoCleanup; 470 this.conf = conf; 471 this.covConf = coverage; 472 this.schemaConf = schema; 473 474 this.timeoutFsm.setLogLevel; 475 476 if (!conf.mutationTesterRuntime.isNull) 477 timeout.userConfigured(conf.mutationTesterRuntime.get); 478 479 local.get!PullRequest.constraint = conf.constraint; 480 local.get!RetestOldMutant.maxReset = conf.oldMutantsNr; 481 local.get!RetestOldMutant.resetPercentage = conf.oldMutantPercentage; 482 this.testCmds = conf.mutationTester; 483 this.mutationOrder = conf.mutationOrder; 484 485 this.runner.useEarlyStop(conf.useEarlyTestCmdStop); 486 this.runner = TestRunner.make(conf.testPoolSize); 487 this.runner.useEarlyStop(conf.useEarlyTestCmdStop); 488 this.runner.maxOutputCapture( 489 TestRunner.MaxCaptureBytes(conf.maxTestCaseOutput.get * 1024 * 1024)); 490 this.runner.minAvailableMem( 491 TestRunner.MinAvailableMemBytes(toMinMemory(conf.maxMemUsage.get))); 492 this.runner.put(conf.mutationTester); 493 494 // TODO: allow a user, as is for test_cmd, to specify an array of 495 // external analyzers. 496 this.testCaseAnalyzer = TestCaseAnalyzer(conf.mutationTestCaseBuiltin, 497 conf.mutationTestCaseAnalyze, autoCleanup); 498 499 this.stopCheck = TestStopCheck(conf); 500 501 this.maxParallelInstances = () { 502 if (mutationOrder.among(MutationOrder.random, MutationOrder.bySize)) 503 return 100; 504 return 1; 505 }(); 506 507 if (logger.globalLogLevel.among(logger.LogLevel.trace, logger.LogLevel.all)) 508 fsm.logger = (string s) { logger.trace(s); }; 509 } 510 511 static void execute_(ref TestDriver self) @trusted { 512 // see test_mutant/basis.md and figures/test_mutant_fsm.pu for a 513 // graphical view of the state machine. 514 515 self.fsm.next!((None a) => fsm(Initialize.init), (Initialize a) { 516 if (a.halt) 517 return fsm(CheckStopCond.init); 518 return fsm(SanityCheck.init); 519 }, (SanityCheck a) { 520 if (a.sanityCheckFailed) 521 return fsm(Error.init); 522 if (self.conf.unifiedDiffFromStdin) 523 return fsm(ParseStdin.init); 524 return fsm(PreCompileSut.init); 525 }, (ParseStdin a) => fsm(PreCompileSut.init), (AnalyzeTestCmdForTestCase a) { 526 if (a.failed) 527 return fsm(Error.init); 528 return fsm(UpdateAndResetAliveMutants(a.foundTestCases)); 529 }, (UpdateAndResetAliveMutants a) { 530 if (self.conf.onOldMutants == ConfigMutationTest.OldMutant.test) 531 return fsm(RetestOldMutant.init); 532 return fsm(IncreaseFilePrio.init); 533 }, (RetestOldMutant a) => fsm(IncreaseFilePrio.init), (Cleanup a) { 534 if (self.local.get!PullRequest.constraint.empty) 535 return fsm(NextSchemata.init); 536 return fsm(CheckPullRequestMutant.init); 537 }, (IncreaseFilePrio a) { return fsm(CheckMutantsLeft.init); }, (CheckMutantsLeft a) { 538 if (a.allMutantsTested) 539 return fsm(Done.init); 540 if (self.conf.testCmdChecksum.get) 541 return fsm(ChecksumTestCmds.init); 542 return fsm(MeasureTestSuite.init); 543 }, (ChecksumTestCmds a) => MeasureTestSuite.init, (SaveMutationScore a) => UpdateTestCaseTag.init, 544 (UpdateTestCaseTag a) => SaveTestBinary.init, 545 (SaveTestBinary a) => Stop.init, (PreCompileSut a) { 546 if (a.compilationError) 547 return fsm(Error.init); 548 if (self.conf.testCommandDir.empty) 549 return fsm(UpdateTestCmds.init); 550 return fsm(FindTestCmds.init); 551 }, (FindTestCmds a) => fsm(UpdateTestCmds.init), 552 (UpdateTestCmds a) => fsm(ChooseMode.init), (ChooseMode a) { 553 if (!self.local.get!PullRequest.constraint.empty) 554 return fsm(PullRequest.init); 555 if (!self.conf.mutationTestCaseAnalyze.empty 556 || !self.conf.mutationTestCaseBuiltin.empty) 557 return fsm(AnalyzeTestCmdForTestCase.init); 558 if (self.conf.onOldMutants == ConfigMutationTest.OldMutant.test) 559 return fsm(RetestOldMutant.init); 560 return fsm(IncreaseFilePrio.init); 561 }, (PullRequest a) => fsm(IncreaseFilePrio.init), (MeasureTestSuite a) { 562 if (a.unreliableTestSuite) 563 return fsm(Error.init); 564 if (self.covConf.use && self.local.get!PullRequest.constraint.empty) 565 return fsm(Coverage.init); 566 return fsm(UpdateTimeout.init); 567 }, (Coverage a) { 568 if (a.fatalError) 569 return fsm(Error.init); 570 if (a.propagate) 571 return fsm(PropagateCoverage.init); 572 return fsm(UpdateTimeout.init); 573 }, (PropagateCoverage a) => UpdateTimeout.init, (CheckPullRequestMutant a) { 574 if (a.noUnknownMutantsLeft) 575 return fsm(Done.init); 576 return fsm(NextMutant.init); 577 }, (NextSchemata a) { 578 if (self.schemaConf.use) { 579 if (self.local.get!NextSchemata.runSchema == NextSchemataData.State.runOnce) 580 return fsm(SchemataTest.init); 581 if (self.local.get!NextSchemata.runSchema == NextSchemataData.State.done 582 && self.schemaConf.stopAfterLastSchema) 583 return fsm(Done.init); 584 if (self.local.get!NextSchemata.runSchema == NextSchemataData.State.done) 585 return fsm(NextMutant.init); 586 return fsm(a); 587 } 588 return fsm(NextMutant.init); 589 }, (SchemataTest a) { 590 if (a.fatalError) 591 return fsm(Error.init); 592 return fsm(CheckStopCond.init); 593 }, (NextMutant a) { 594 if (a.noUnknownMutantsLeft) 595 return fsm(CheckTimeout.init); 596 return fsm(MutationTest.init); 597 }, (UpdateTimeout a) => fsm(OverloadCheck.init), (OverloadCheck a) { 598 if (a.sleep) 599 return fsm(CheckStopCond.init); 600 return fsm(ContinuesCheckTestSuite.init); 601 }, (ContinuesCheckTestSuite a) { 602 if (a.ok) 603 return fsm(Cleanup.init); 604 return fsm(Error.init); 605 }, (MutationTest a) { 606 if (a.mutationError) 607 return fsm(Error.init); 608 return fsm(HandleTestResult(a.result)); 609 }, (HandleTestResult a) => fsm(CheckStopCond.init), (CheckStopCond a) { 610 if (a.halt) 611 return fsm(Done.init); 612 return fsm(UpdateTimeout.init); 613 }, (CheckTimeout a) { 614 if (a.timeoutUnchanged) 615 return fsm(Done.init); 616 return fsm(UpdateTimeout.init); 617 }, (Done a) => fsm(SaveMutationScore.init), (Error a) => fsm(Stop.init), (Stop a) => fsm(a)); 618 619 self.fsm.act!(self); 620 } 621 622 nothrow: 623 624 void execute() { 625 try { 626 execute_(this); 627 } catch (Exception e) { 628 logger.warning(e.msg).collectException; 629 } 630 } 631 632 bool isRunning() { 633 return isRunning_; 634 } 635 636 ExitStatusType status() { 637 if (isDone) 638 return ExitStatusType.Ok; 639 return ExitStatusType.Errors; 640 } 641 642 void opCall(None data) { 643 } 644 645 void opCall(ref Initialize data) { 646 logger.info("Initializing worklist").collectException; 647 648 auto status = [Mutation.Status.unknown]; 649 if (!conf.useSkipMutant) 650 status ~= Mutation.Status.skipped; 651 652 spinSql!(() { 653 db.worklistApi.update(status, unknownWeight, mutationOrder); 654 }); 655 656 // detect if the system is overloaded before trying to do something 657 // slow such as compiling the SUT. 658 if (conf.loadBehavior == ConfigMutationTest.LoadBehavior.halt && stopCheck.isHalt) { 659 data.halt = true; 660 } 661 662 logger.infof("Memory limit set minium %s Mbyte", 663 cast(ulong)(toMinMemory(conf.maxMemUsage.get) / (1024.0 * 1024.0))) 664 .collectException; 665 666 try { 667 dbSave = system.spawn(&spawnDbSaveActor, dbPath); 668 stat = system.spawn(&spawnStatActor, dbPath); 669 } catch (Exception e) { 670 logger.error(e.msg).collectException; 671 data.halt = true; 672 } 673 } 674 675 void opCall(ref IncreaseFilePrio data) { 676 import std.file : exists, readText; 677 import std.json : JSONValue, JSONOptions, parseJSON; 678 679 if (conf.metadataPath.length == 0) { 680 return; 681 } else if (!exists(conf.metadataPath)) { 682 logger.error("File: " ~ conf.metadataPath ~ " does not exist").collectException; 683 return; 684 } 685 686 string fileContent; 687 try { 688 fileContent = readText(conf.metadataPath); 689 } catch (Exception e) { 690 logger.error("Unable to read file " ~ conf.metadataPath).collectException; 691 return; 692 } 693 694 JSONValue jContent; 695 try { 696 jContent = parseJSON(fileContent, JSONOptions.doNotEscapeSlashes); 697 } catch (Exception e) { 698 logger.info(e.msg).collectException; 699 logger.error("Failed to parse filecontent of " ~ conf.metadataPath ~ "into JSON") 700 .collectException; 701 return; 702 } 703 704 JSONValue objectData; 705 try { 706 objectData = jContent["file-prio"]; 707 } catch (Exception e) { 708 logger.info(e.msg).collectException; 709 logger.error("Object 'file-prio' not found in file " ~ conf.metadataPath) 710 .collectException; 711 return; 712 } 713 714 Path[] prioFiles; 715 try { 716 foreach (prioFileElem; objectData.arrayNoRef) { 717 prioFiles ~= Path(prioFileElem.str); 718 } 719 } catch (Exception e) { 720 logger.info(e.msg).collectException; 721 logger.error("'file-prio' JSON object not a valid array in file " ~ conf.metadataPath) 722 .collectException; 723 return; 724 } 725 726 logger.info("Increasing prio on all mutants in the files from " ~ conf.metadataPath) 727 .collectException; 728 foreach (prioFilePath; prioFiles) { 729 logger.info(prioFilePath).collectException; 730 spinSql!(() @trusted { db.mutantApi.increaseFilePrio(prioFilePath); }); 731 } 732 } 733 734 void opCall(Stop data) { 735 isRunning_ = false; 736 } 737 738 void opCall(Done data) { 739 import dextool.plugin.mutate.backend.test_mutant.common_actors : IsDone; 740 741 try { 742 auto self = scopedActor; 743 // it should NOT take more than five minutes to save the last 744 // results to the database. 745 self.request(dbSave, delay(5.dur!"minutes")).send(IsDone.init).then((bool a) { 746 }); 747 } catch (ScopedActorException e) { 748 logger.trace(e.error).collectException; 749 } catch (Exception e) { 750 logger.warning(e.msg).collectException; 751 } 752 753 logger.info("Done!").collectException; 754 isDone = true; 755 } 756 757 void opCall(Error data) { 758 autoCleanup.cleanup; 759 } 760 761 void opCall(ref SanityCheck data) { 762 import core.sys.posix.sys.stat : S_IWUSR; 763 import std.path : buildPath; 764 import my.file : getAttrs; 765 import colorlog : color; 766 import dextool.plugin.mutate.backend.utility : checksum, Checksum; 767 768 logger.info("Sanity check of files to mutate").collectException; 769 770 auto failed = appender!(string[])(); 771 auto checksumFailed = appender!(string[])(); 772 auto writePermissionFailed = appender!(string[])(); 773 foreach (file; spinSql!(() { return db.getFiles; })) { 774 auto db_checksum = spinSql!(() { return db.getFileChecksum(file); }); 775 776 try { 777 auto abs_f = AbsolutePath(buildPath(filesysIO.getOutputDir, file)); 778 auto f_checksum = checksum(filesysIO.makeInput(abs_f).content[]); 779 if (db_checksum != f_checksum) { 780 checksumFailed.put(abs_f); 781 } 782 783 uint attrs; 784 if (getAttrs(abs_f, attrs)) { 785 if ((attrs & S_IWUSR) == 0) { 786 writePermissionFailed.put(abs_f); 787 } 788 } else { 789 writePermissionFailed.put(abs_f); 790 } 791 } catch (Exception e) { 792 failed.put(file); 793 logger.warningf("%s: %s", file, e.msg).collectException; 794 } 795 } 796 797 data.sanityCheckFailed = !failed.data.empty 798 || !checksumFailed.data.empty || !writePermissionFailed.data.empty; 799 800 if (data.sanityCheckFailed) { 801 logger.info(!failed.data.empty, 802 "Unknown error when checking the files").collectException; 803 foreach (f; failed.data) 804 logger.info(f).collectException; 805 806 logger.info(!checksumFailed.data.empty, 807 "Detected that file(s) has changed since last analyze where done") 808 .collectException; 809 logger.info(!checksumFailed.data.empty, 810 "Either restore the file(s) or rerun the analyze").collectException; 811 foreach (f; checksumFailed.data) 812 logger.info(f).collectException; 813 814 logger.info(!writePermissionFailed.data.empty, 815 "Files to mutate are not writable").collectException; 816 foreach (f; writePermissionFailed.data) 817 logger.info(f).collectException; 818 819 logger.info("Failed".color.fgRed).collectException; 820 } else { 821 logger.info("Ok".color.fgGreen).collectException; 822 } 823 } 824 825 void opCall(ref OverloadCheck data) { 826 if (conf.loadBehavior == ConfigMutationTest.LoadBehavior.slowdown && stopCheck.isOverloaded) { 827 data.sleep = true; 828 logger.info(stopCheck.overloadToString).collectException; 829 stopCheck.pause; 830 } 831 } 832 833 void opCall(ref ContinuesCheckTestSuite data) { 834 import colorlog : color; 835 836 data.ok = true; 837 838 if (!conf.contCheckTestSuite) 839 return; 840 841 enum forceCheckEach = 1.dur!"hours"; 842 843 const wlist = spinSql!(() => db.worklistApi.getCount); 844 if (local.get!ContinuesCheckTestSuite.lastWorklistCnt == 0) { 845 // first time, just initialize. 846 local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist; 847 local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach; 848 return; 849 } 850 851 const period = conf.contCheckTestSuitePeriod.get; 852 const diffCnt = local.get!ContinuesCheckTestSuite.lastWorklistCnt - wlist; 853 // period == 0 is mostly for test purpose because it makes it possible 854 // to force a check for every mutant. 855 if (!(period == 0 || wlist % period == 0 || diffCnt >= period 856 || Clock.currTime > local.get!ContinuesCheckTestSuite.lastCheck)) 857 return; 858 859 logger.info("Checking the test environment").collectException; 860 861 local.get!ContinuesCheckTestSuite.lastWorklistCnt = wlist; 862 local.get!ContinuesCheckTestSuite.lastCheck = Clock.currTime + forceCheckEach; 863 864 compile(conf.mutationCompile, conf.buildCmdTimeout, PrintCompileOnFailure(true)).match!( 865 (Mutation.Status a) { data.ok = false; }, (bool success) { 866 data.ok = success; 867 }); 868 869 if (data.ok) { 870 try { 871 data.ok = measureTestCommand(runner, 1).ok; 872 } catch (Exception e) { 873 logger.error(e.msg).collectException; 874 data.ok = false; 875 } 876 } 877 878 if (data.ok) { 879 logger.info("Ok".color.fgGreen).collectException; 880 } else { 881 logger.info("Failed".color.fgRed).collectException; 882 logger.warning("Continues sanity check of the test suite has failed.").collectException; 883 logger.infof("Rolling back the status of the last %s mutants to status unknown.", 884 period).collectException; 885 foreach (a; spinSql!(() => db.mutantApi.getLatestMutants(max(diffCnt, period)))) { 886 spinSql!(() => db.mutantApi.update(a.id, Mutation.Status.unknown, 887 ExitStatus(0), MutantTimeProfile.init)); 888 } 889 } 890 } 891 892 void opCall(ParseStdin data) { 893 import dextool.plugin.mutate.backend.diff_parser : diffFromStdin; 894 import dextool.plugin.mutate.type : Line; 895 896 try { 897 auto constraint = local.get!PullRequest.constraint; 898 foreach (pkv; diffFromStdin.toRange(filesysIO.getOutputDir)) { 899 constraint.value[pkv.key] ~= pkv.value.toRange.map!(a => Line(a)).array; 900 } 901 local.get!PullRequest.constraint = constraint; 902 } catch (Exception e) { 903 logger.warning(e.msg).collectException; 904 } 905 } 906 907 void opCall(ref AnalyzeTestCmdForTestCase data) { 908 import std.conv : to; 909 import colorlog : color; 910 911 TestCase[][ShellCommand] found; 912 913 try { 914 runner.captureAll(true); 915 scope (exit) 916 runner.captureAll(false); 917 918 // using an unreasonable timeout to make it possible to analyze for 919 // test cases and measure the test suite. 920 auto res = runTester(runner, 999.dur!"hours"); 921 data.failed = res.status != Mutation.Status.alive; 922 923 foreach (testCmd; res.output.byKeyValue) { 924 auto analyze = testCaseAnalyzer.analyze(testCmd.key, testCmd.value, Yes.allFound); 925 926 analyze.match!((TestCaseAnalyzer.Success a) { 927 found[testCmd.key] = a.found; 928 }, (TestCaseAnalyzer.Unstable a) { 929 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable); 930 found[testCmd.key] = a.found; 931 }, (TestCaseAnalyzer.Failed a) { 932 logger.warning("The parser that analyze the output for test case(s) failed"); 933 }); 934 } 935 936 if (data.failed) { 937 logger.infof("Some or all tests have status %s (exit code %s)", 938 res.status.to!string.color.fgRed, res.exitStatus.get); 939 try { 940 // TODO: this is a lazy way to execute the test suite again 941 // to show the failing tests. prettify.... 942 measureTestCommand(runner, 1); 943 } catch (Exception e) { 944 } 945 logger.warning("Failing test suite"); 946 } 947 948 warnIfConflictingTestCaseIdentifiers(found.byValue.joiner.array); 949 } catch (Exception e) { 950 logger.warning(e.msg).collectException; 951 } 952 953 if (!data.failed) { 954 data.foundTestCases = found; 955 } 956 } 957 958 void opCall(UpdateAndResetAliveMutants data) { 959 // the test cases before anything has potentially changed. 960 auto old_tcs = spinSql!(() { 961 Set!string old_tcs; 962 foreach (tc; db.testCaseApi.getDetectedTestCases) 963 old_tcs.add(tc.name); 964 return old_tcs; 965 }); 966 967 void transaction() @safe { 968 final switch (conf.onRemovedTestCases) with (ConfigMutationTest.RemovedTestCases) { 969 case doNothing: 970 db.testCaseApi.addDetectedTestCases(data.foundTestCases.byValue.joiner.array); 971 break; 972 case remove: 973 bool update; 974 // change all mutants which, if a test case is removed, no 975 // longer has a test case that kills it to unknown status 976 foreach (id; db.testCaseApi.setDetectedTestCases( 977 data.foundTestCases.byValue.joiner.array)) { 978 if (!db.testCaseApi.hasTestCases(id)) { 979 update = true; 980 db.mutantApi.update(id, Mutation.Status.unknown, ExitStatus(0)); 981 } 982 } 983 if (update) { 984 db.worklistApi.update([ 985 Mutation.Status.unknown, Mutation.Status.skipped 986 ]); 987 } 988 break; 989 } 990 } 991 992 auto found_tcs = spinSql!(() @trusted { 993 auto tr = db.transaction; 994 transaction(); 995 996 Set!string found_tcs; 997 foreach (tc; db.testCaseApi.getDetectedTestCases) 998 found_tcs.add(tc.name); 999 1000 tr.commit; 1001 return found_tcs; 1002 }); 1003 1004 printDroppedTestCases(old_tcs, found_tcs); 1005 1006 if (hasNewTestCases(old_tcs, found_tcs) 1007 && conf.onNewTestCases == ConfigMutationTest.NewTestCases.resetAlive) { 1008 logger.info("Adding alive mutants to worklist").collectException; 1009 spinSql!(() { 1010 db.worklistApi.update([ 1011 Mutation.Status.alive, Mutation.Status.skipped, 1012 // if these mutants are covered by the tests then they will be 1013 // removed from the worklist in PropagateCoverage. 1014 Mutation.Status.noCoverage 1015 ]); 1016 }); 1017 } 1018 } 1019 1020 void opCall(RetestOldMutant data) { 1021 import std.range : enumerate; 1022 import dextool.plugin.mutate.backend.database.type; 1023 import dextool.plugin.mutate.backend.test_mutant.timeout : resetTimeoutContext; 1024 1025 const statusTypes = [EnumMembers!(Mutation.Status)].filter!( 1026 a => a != Mutation.Status.noCoverage).array; 1027 1028 void printStatus(T0)(T0 oldestMutant, SysTime newestTest, SysTime newestFile) { 1029 logger.info("Tests last changed ", newestTest).collectException; 1030 logger.info("Source code last changed ", newestFile).collectException; 1031 1032 if (!oldestMutant.empty) { 1033 logger.info("The oldest mutant is ", oldestMutant[0].updated).collectException; 1034 } 1035 } 1036 1037 if (conf.onOldMutants == ConfigMutationTest.OldMutant.nothing) 1038 return; 1039 1040 // do not add mutants to worklist if there already are mutants there 1041 // because other states and functions need it to sooner or late reach 1042 // zero. 1043 const wlist = spinSql!(() => db.worklistApi.getCount); 1044 if (wlist != 0) 1045 return; 1046 1047 const oldestMutant = spinSql!(() => db.mutantApi.getOldestMutants(1, statusTypes)); 1048 const newestTest = spinSql!(() => db.testFileApi.getNewestTestFile).orElse( 1049 TestFile.init).timeStamp; 1050 const newestFile = spinSql!(() => db.getNewestFile).orElse(SysTime.init); 1051 if (!oldestMutant.empty && oldestMutant[0].updated >= newestTest 1052 && oldestMutant[0].updated >= newestFile) { 1053 // only re-test old mutants if needed. 1054 logger.info("Mutation status is up to date").collectException; 1055 printStatus(oldestMutant, newestTest, newestFile); 1056 return; 1057 } else { 1058 logger.info("Mutation status is out of sync").collectException; 1059 printStatus(oldestMutant, newestTest, newestFile); 1060 } 1061 1062 const long testCnt = () { 1063 if (local.get!RetestOldMutant.resetPercentage.get == 0.0) { 1064 return local.get!RetestOldMutant.maxReset; 1065 } 1066 1067 const total = spinSql!(() => db.mutantApi.totalSrcMutants().count); 1068 const rval = cast(long)(1 + total 1069 * local.get!RetestOldMutant.resetPercentage.get / 100.0); 1070 return rval; 1071 }(); 1072 1073 spinSql!(() { 1074 auto oldest = db.mutantApi.getOldestMutants(testCnt, statusTypes); 1075 logger.infof("Adding %s old mutants to the worklist", oldest.length); 1076 foreach (const old; oldest) { 1077 db.worklistApi.add(old.id); 1078 } 1079 if (oldest.length > 3) { 1080 logger.infof("Range of when the added mutants where last tested is %s -> %s", 1081 oldest[0].updated, oldest[$ - 1].updated); 1082 } 1083 1084 // because the mutants are zero it is assumed that they it is 1085 // starting from scratch thus the timeout algorithm need to 1086 // re-start from its initial state. 1087 logger.info("Resetting timeout context"); 1088 resetTimeoutContext(*db); 1089 }); 1090 } 1091 1092 void opCall(Cleanup data) { 1093 autoCleanup.cleanup; 1094 } 1095 1096 void opCall(ref CheckMutantsLeft data) { 1097 spinSql!(() { timeoutFsm.execute(*db); }); 1098 1099 data.allMutantsTested = timeoutFsm.output.done; 1100 1101 if (timeoutFsm.output.done) { 1102 logger.info("All mutants are tested").collectException; 1103 } 1104 } 1105 1106 void opCall(ChecksumTestCmds data) @trusted { 1107 import std.file : exists; 1108 import my.hash : Checksum64, makeCrc64Iso, checksum; 1109 import dextool.plugin.mutate.backend.database.type : ChecksumTestCmdOriginal; 1110 1111 auto previous = spinSql!(() => db.testCmdApi.original); 1112 1113 try { 1114 Set!Checksum64 current; 1115 1116 void helper() { 1117 // clearing just to be on the safe side if helper is called 1118 // multiple times and a checksum is different between the 1119 // calls..... shouldn't happen but 1120 current = typeof(current).init; 1121 auto tr = db.transaction; 1122 1123 foreach (testCmd; hashFiles(testCmds.filter!(a => !a.empty) 1124 .map!(a => a.value[0]))) { 1125 current.add(testCmd.cs); 1126 1127 if (testCmd.cs !in previous) 1128 db.testCmdApi.set(testCmd.file, ChecksumTestCmdOriginal(testCmd.cs)); 1129 } 1130 1131 foreach (a; previous.setDifference(current).toRange) { 1132 const name = db.testCmdApi.getTestCmd(ChecksumTestCmdOriginal(a)); 1133 if (!name.empty) 1134 db.testCmdApi.clearTestCmdToMutant(name); 1135 db.testCmdApi.remove(ChecksumTestCmdOriginal(a)); 1136 } 1137 1138 tr.commit; 1139 } 1140 1141 // the operation must succeed as a whole or fail. 1142 spinSql!(() => helper); 1143 1144 local.get!MutationTest.testBinaryDb.original = current; 1145 } catch (Exception e) { 1146 logger.warning(e.msg).collectException; 1147 } 1148 1149 local.get!MutationTest.testBinaryDb.mutated = spinSql!( 1150 () @trusted => db.testCmdApi.mutated); 1151 } 1152 1153 void opCall(SaveMutationScore data) { 1154 import dextool.plugin.mutate.backend.database.type : MutationScore, 1155 MutationScore, FileScore; 1156 import dextool.plugin.mutate.backend.report.analyzers : reportScore, reportScores; 1157 import std.algorithm : canFind; 1158 1159 if (spinSql!(() => db.mutantApi.unknownSrcMutants()).count != 0) 1160 return; 1161 // users are unhappy when the score go first up and then down because 1162 // mutants are first classified as "timeout" (killed) and then changed 1163 // to alive when the timeout is increased. This lead to a trend graph 1164 // that always looks like /\ which inhibit the "motivational drive" to 1165 // work with mutation testing. Thus if there are any timeout mutants 1166 // to test, do not sample the score. It avoids the "hill" behavior in 1167 // the trend. 1168 if (spinSql!(() => db.timeoutApi.countMutantTimeoutWorklist) != 0) 1169 return; 1170 1171 // 10000 mutation scores is only ~80kbyte. Should be enough entries 1172 // without taking up unreasonable amount of space. 1173 immutable maxScoreHistory = 10000; 1174 1175 const time = Clock.currTime.toUTC; 1176 1177 const score = reportScore(*db); 1178 spinSql!(() @trusted { 1179 auto t = db.transaction; 1180 db.putMutationScore(MutationScore(time, typeof(MutationScore.score)(score.score))); 1181 db.trimMutationScore(maxScoreHistory); 1182 t.commit; 1183 }); 1184 1185 foreach (fileScore; reportScores(*db, spinSql!(() => db.getFiles())).filter!( 1186 a => a.hasMutants)) { 1187 spinSql!(() @trusted { 1188 auto t = db.transaction; 1189 db.fileApi.put(FileScore(time, 1190 typeof(FileScore.score)(fileScore.score), fileScore.file)); 1191 db.fileApi.trim(fileScore.file, maxScoreHistory); 1192 t.commit; 1193 }); 1194 } 1195 1196 // If a file only exists in the FileScores table, and not in the Files table, 1197 // then the file's stored scores should be removed 1198 spinSql!(() @trusted { 1199 auto t = db.transaction; 1200 db.fileApi.prune(); 1201 t.commit; 1202 }); 1203 } 1204 1205 void opCall(UpdateTestCaseTag data) { 1206 if (spinSql!(() => db.worklistApi.getCount([ 1207 Mutation.Status.alive, Mutation.Status.unknown 1208 ])) == 0) { 1209 spinSql!(() => db.testCaseApi.removeNewTestCaseTag); 1210 logger.info("All alive in worklist tested. Removing 'new test' tag.").collectException; 1211 } 1212 } 1213 1214 void opCall(SaveTestBinary data) { 1215 if (!local.get!MutationTest.testBinaryDb.empty) 1216 saveTestBinaryDb(local.get!MutationTest.testBinaryDb); 1217 } 1218 1219 void opCall(ref PreCompileSut data) { 1220 import proc; 1221 1222 logger.info("Checking the build command").collectException; 1223 compile(conf.mutationCompile, conf.buildCmdTimeout, PrintCompileOnFailure(true)).match!( 1224 (Mutation.Status a) { data.compilationError = true; }, (bool success) { 1225 data.compilationError = !success; 1226 }); 1227 1228 if (data.compilationError) { 1229 logger.info("[mutant_test.build_cmd]: ", conf.mutationCompile).collectException; 1230 logger.error( 1231 "Either [mutant_test.build_cmd] is not configured or there is an error running the build command") 1232 .collectException; 1233 } 1234 } 1235 1236 void opCall(FindTestCmds data) { 1237 auto cmds = appender!(ShellCommand[])(); 1238 foreach (root; conf.testCommandDir) { 1239 try { 1240 cmds.put(findExecutables(root.AbsolutePath, () { 1241 import std.file : SpanMode; 1242 1243 final switch (conf.testCmdDirSearch) with ( 1244 ConfigMutationTest.TestCmdDirSearch) { 1245 case shallow: 1246 return SpanMode.shallow; 1247 case recursive: 1248 return SpanMode.breadth; 1249 } 1250 }()).map!(a => ShellCommand([a] ~ conf.testCommandDirFlag))); 1251 } catch (Exception e) { 1252 logger.warning(e.msg).collectException; 1253 } 1254 } 1255 1256 if (!cmds.data.empty) { 1257 testCmds ~= cmds.data; 1258 runner.put(this.testCmds); 1259 logger.infof("Found test commands in %s:", conf.testCommandDir).collectException; 1260 foreach (c; cmds.data) { 1261 logger.info(c).collectException; 1262 } 1263 } 1264 } 1265 1266 void opCall(UpdateTestCmds data) { 1267 spinSql!(() @trusted { 1268 auto tr = db.transaction; 1269 db.testCmdApi.set(runner.testCmds.map!(a => a.cmd.toString).array); 1270 tr.commit; 1271 }); 1272 } 1273 1274 void opCall(ChooseMode data) { 1275 } 1276 1277 void opCall(PullRequest data) { 1278 import std.algorithm : sort; 1279 import my.set; 1280 import dextool.plugin.mutate.backend.database : MutationStatusId; 1281 import dextool.plugin.mutate.backend.type : SourceLoc; 1282 1283 // deterministic testing of mutants and prioritized by their size. 1284 mutationOrder = MutationOrder.bySize; 1285 maxParallelInstances = 1; 1286 1287 // make sure they are unique. 1288 Set!MutationStatusId mutantIds; 1289 1290 foreach (kv; local.get!PullRequest.constraint.value.byKeyValue) { 1291 const file_id = spinSql!(() => db.getFileId(kv.key)); 1292 if (file_id.isNull) { 1293 logger.infof("The file %s do not exist in the database. Skipping...", 1294 kv.key).collectException; 1295 continue; 1296 } 1297 1298 foreach (l; kv.value) { 1299 auto mutants = spinSql!(() => db.mutantApi.getMutationsOnLine(file_id.get, 1300 SourceLoc(l.value, 0))); 1301 1302 const preCnt = mutantIds.length; 1303 foreach (v; mutants) 1304 mutantIds.add(v); 1305 1306 logger.infof(mutantIds.length - preCnt > 0, "Found %s mutant(s) to test (%s:%s)", 1307 mutantIds.length - preCnt, kv.key, l.value).collectException; 1308 } 1309 } 1310 1311 logger.infof(!mutantIds.empty, "Found %s mutants in the diff", 1312 mutantIds.length).collectException; 1313 spinSql!(() { 1314 foreach (id; mutantIds.toArray.sort) 1315 db.worklistApi.add(id, pullRequestWeight, MutationOrder.bySize); 1316 }); 1317 1318 local.get!CheckPullRequestMutant.startWorklistCnt = spinSql!(() => db.worklistApi.getCount); 1319 local.get!CheckPullRequestMutant.stopAfter = mutantIds.length; 1320 1321 if (mutantIds.empty) { 1322 logger.warning("None of the locations specified with -L exists").collectException; 1323 logger.info("Available files are:").collectException; 1324 foreach (f; spinSql!(() => db.getFiles)) 1325 logger.info(f).collectException; 1326 } 1327 } 1328 1329 void opCall(ref MeasureTestSuite data) { 1330 import std.algorithm : sum; 1331 import dextool.plugin.mutate.backend.database.type : TestCmdRuntime; 1332 1333 if (timeout.isUserConfig) { 1334 runner.timeout = timeout.base; 1335 return; 1336 } 1337 1338 logger.infof("Measuring the runtime of the test command(s):\n%(%s\n%)", 1339 testCmds).collectException; 1340 1341 auto measures = spinSql!(() => db.testCmdApi.getTestCmdRuntimes); 1342 1343 const tester = () { 1344 try { 1345 return measureTestCommand(runner, max(1, cast(int)(3 - measures.length))); 1346 } catch (Exception e) { 1347 logger.error(e.msg).collectException; 1348 return MeasureTestDurationResult(false); 1349 } 1350 }(); 1351 1352 if (tester.ok) { 1353 measures ~= tester.runtime.map!(a => TestCmdRuntime(Clock.currTime, a)).array; 1354 if (measures.length > 3) { 1355 measures = measures[1 .. $]; // drop the oldest 1356 } 1357 1358 auto mean = sum(measures.map!(a => a.runtime), Duration.zero) / measures.length; 1359 logger.info("Test command runtime: ", mean).collectException; 1360 timeout.set(mean); 1361 runner.timeout = timeout.value; 1362 1363 spinSql!(() @trusted { 1364 auto t = db.transaction; 1365 db.testCmdApi.setTestCmdRuntimes(measures); 1366 t.commit; 1367 }); 1368 } else { 1369 data.unreliableTestSuite = true; 1370 logger.error("The test command is unreliable. It must return exit status '0' when no mutants are injected") 1371 .collectException; 1372 } 1373 } 1374 1375 void opCall(ref MutationTest data) @trusted { 1376 auto runnerPtr = () @trusted { return &runner; }(); 1377 auto testBinaryDbPtr = () @trusted { 1378 return &local.get!MutationTest.testBinaryDb; 1379 }(); 1380 1381 try { 1382 auto g = MutationTestDriver.Global(filesysIO, db, nextMutant, 1383 runnerPtr, testBinaryDbPtr, conf.useSkipMutant); 1384 auto driver = MutationTestDriver(g, 1385 MutationTestDriver.TestMutantData(!(conf.mutationTestCaseAnalyze.empty 1386 && conf.mutationTestCaseBuiltin.empty), 1387 conf.mutationCompile, conf.buildCmdTimeout), 1388 MutationTestDriver.TestCaseAnalyzeData(&testCaseAnalyzer)); 1389 1390 while (driver.isRunning) { 1391 driver.execute(); 1392 } 1393 1394 if (driver.stopBecauseError) { 1395 data.mutationError.get = true; 1396 } else { 1397 data.result = driver.result; 1398 } 1399 } catch (Exception e) { 1400 data.mutationError.get = true; 1401 logger.error(e.msg).collectException; 1402 } 1403 } 1404 1405 void opCall(ref CheckTimeout data) { 1406 data.timeoutUnchanged = timeout.isUserConfig || timeoutFsm.output.done; 1407 } 1408 1409 void opCall(UpdateTimeout) { 1410 spinSql!(() { timeoutFsm.execute(*db); }); 1411 1412 const lastIter = local.get!UpdateTimeout.lastTimeoutIter; 1413 1414 if (lastIter != timeoutFsm.output.iter) { 1415 const old = timeout.value; 1416 timeout.updateIteration(timeoutFsm.output.iter); 1417 logger.infof("Changed the timeout from %s to %s (iteration %s)", 1418 old, timeout.value, timeoutFsm.output.iter).collectException; 1419 local.get!UpdateTimeout.lastTimeoutIter = timeoutFsm.output.iter; 1420 } 1421 1422 runner.timeout = timeout.value; 1423 } 1424 1425 void opCall(ref CheckPullRequestMutant data) { 1426 const left = spinSql!(() => db.worklistApi.getCount); 1427 data.noUnknownMutantsLeft.get = ( 1428 local.get!CheckPullRequestMutant.startWorklistCnt - left) >= local 1429 .get!CheckPullRequestMutant.stopAfter; 1430 1431 logger.infof(stopCheck.aliveMutants > 0, "Found %s/%s alive mutants", 1432 stopCheck.aliveMutants, conf.maxAlive.get).collectException; 1433 } 1434 1435 void opCall(ref NextMutant data) { 1436 nextMutant = MutationEntry.init; 1437 1438 // it is OK to re-test the same mutant thus using a somewhat short timeout. It isn't fatal. 1439 const giveUpAfter = Clock.currTime + 30.dur!"seconds"; 1440 NextMutationEntry next; 1441 while (Clock.currTime < giveUpAfter) { 1442 next = spinSql!(() => db.nextMutation(maxParallelInstances)); 1443 1444 if (next.st == NextMutationEntry.Status.done) 1445 break; 1446 else if (!next.entry.isNull && next.entry.get.id != local.get!NextMutant.lastTested) 1447 break; 1448 else if (next.entry.isNull) 1449 break; 1450 } 1451 1452 data.noUnknownMutantsLeft.get = next.st == NextMutationEntry.Status.done; 1453 1454 if (!next.entry.isNull) { 1455 nextMutant = next.entry.get; 1456 local.get!NextMutant.lastTested = next.entry.get.id; 1457 } 1458 } 1459 1460 void opCall(HandleTestResult data) { 1461 saveTestResult(data.result); 1462 if (!local.get!MutationTest.testBinaryDb.empty) 1463 saveTestBinaryDb(local.get!MutationTest.testBinaryDb); 1464 } 1465 1466 void opCall(ref CheckStopCond data) { 1467 const halt = stopCheck.isHalt; 1468 data.halt = halt != TestStopCheck.HaltReason.none; 1469 1470 final switch (halt) with (TestStopCheck.HaltReason) { 1471 case none: 1472 break; 1473 case maxRuntime: 1474 logger.info(stopCheck.maxRuntimeToString).collectException; 1475 break; 1476 case aliveTested: 1477 logger.info("Alive mutants threshold reached").collectException; 1478 break; 1479 case overloaded: 1480 logger.info(stopCheck.overloadToString).collectException; 1481 stopCheck.startBgShutdown; 1482 break; 1483 } 1484 logger.warning(data.halt, "Halting").collectException; 1485 } 1486 1487 void opCall(NextSchemata data) { 1488 final switch (local.get!NextSchemata.runSchema) { 1489 case NextSchemataData.State.first: 1490 local.get!NextSchemata.runSchema = NextSchemataData.State.runOnce; 1491 break; 1492 case NextSchemataData.State.runOnce: 1493 local.get!NextSchemata.runSchema = NextSchemataData.State.done; 1494 break; 1495 case NextSchemataData.State.done: 1496 break; 1497 } 1498 } 1499 1500 void opCall(ref SchemataTest data) { 1501 import core.thread : Thread; 1502 import core.time : dur; 1503 import dextool.plugin.mutate.backend.test_mutant.schemata; 1504 1505 try { 1506 auto driver = system.spawn(&spawnSchema, filesysIO, runner, 1507 dbPath, testCaseAnalyzer, schemaConf, stopCheck, 1508 conf.mutationCompile, conf.buildCmdTimeout, dbSave, stat, timeout); 1509 scope (exit) 1510 sendExit(driver, ExitReason.userShutdown); 1511 auto self = scopedActor; 1512 1513 { 1514 bool waiting = true; 1515 while (waiting) { 1516 try { 1517 self.request(driver, infTimeout).send(IsDone.init).then((bool x) { 1518 waiting = !x; 1519 }); 1520 } catch (ScopedActorException e) { 1521 if (e.error != ScopedActorError.timeout) { 1522 logger.trace(e.error); 1523 return; 1524 } 1525 } 1526 () @trusted { Thread.sleep(100.dur!"msecs"); }(); 1527 } 1528 } 1529 1530 FinalResult fr; 1531 { 1532 try { 1533 self.request(driver, delay(1.dur!"minutes")) 1534 .send(GetDoneStatus.init).then((FinalResult x) { fr = x; }); 1535 logger.trace("final schema status ", fr.status); 1536 } catch (ScopedActorException e) { 1537 logger.trace(e.error); 1538 return; 1539 } 1540 } 1541 1542 final switch (fr.status) with (FinalResult.Status) { 1543 case fatalError: 1544 data.fatalError = true; 1545 break; 1546 case invalidSchema: 1547 // TODO: remove this enum value 1548 break; 1549 case ok: 1550 break; 1551 } 1552 1553 stopCheck.incrAliveMutants(fr.alive); 1554 } catch (Exception e) { 1555 logger.info(e.msg).collectException; 1556 } 1557 } 1558 1559 void opCall(ref Coverage data) @trusted { 1560 import dextool.plugin.mutate.backend.test_mutant.coverage; 1561 1562 auto tracked = spinSql!(() => db.getLatestTimeStampOfTestOrSut).orElse(SysTime.init); 1563 auto covTimeStamp = spinSql!(() => db.coverageApi.getCoverageTimeStamp).orElse( 1564 SysTime.init); 1565 1566 if (tracked < covTimeStamp) { 1567 logger.info("Coverage information is up to date").collectException; 1568 return; 1569 } else { 1570 logger.infof("Coverage is out of date with SUT/tests (%s < %s)", 1571 covTimeStamp, tracked).collectException; 1572 } 1573 1574 try { 1575 auto driver = CoverageDriver(filesysIO, db, &runner, covConf, 1576 conf.mutationCompile, conf.buildCmdTimeout); 1577 while (driver.isRunning) { 1578 driver.execute; 1579 } 1580 data.propagate = true; 1581 data.fatalError = driver.hasFatalError; 1582 } catch (Exception e) { 1583 logger.warning(e.msg).collectException; 1584 data.fatalError = true; 1585 } 1586 1587 if (data.fatalError) 1588 logger.warning("Error detected when trying to gather coverage information") 1589 .collectException; 1590 } 1591 1592 void opCall(PropagateCoverage data) { 1593 void propagate() @trusted { 1594 auto trans = db.transaction; 1595 1596 // needed if tests have changed but not the implementation 1597 db.mutantApi.resetMutant([EnumMembers!(Mutation.Kind)], 1598 Mutation.Status.noCoverage, Mutation.Status.unknown); 1599 1600 auto noCov = db.coverageApi.getNotCoveredMutants; 1601 foreach (id; noCov) 1602 db.mutantApi.update(id, Mutation.Status.noCoverage, ExitStatus(0)); 1603 db.worklistApi.remove(Mutation.Status.noCoverage); 1604 1605 trans.commit; 1606 logger.infof("Marked %s mutants as alive because they where not covered by any test", 1607 noCov.length); 1608 } 1609 1610 spinSql!(() => propagate); 1611 } 1612 1613 void saveTestResult(MutationTestResult[] results) @safe nothrow { 1614 import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft, 1615 UnknownMutantTested; 1616 1617 foreach (a; results.filter!(a => a.status == Mutation.Status.alive)) { 1618 stopCheck.incrAliveMutants; 1619 } 1620 1621 try { 1622 foreach (result; results) 1623 send(dbSave, result, timeoutFsm); 1624 send(stat, UnknownMutantTested.init, cast(long) results.length); 1625 } catch (Exception e) { 1626 logger.warning("Failed to send the result to the database: ", e.msg).collectException; 1627 } 1628 1629 try { 1630 auto self = scopedActor; 1631 self.request(stat, delay(2.dur!"msecs")).send(GetMutantsLeft.init).then((long x) { 1632 logger.infof("%s mutants left to test.", x).collectException; 1633 }); 1634 } catch (Exception e) { 1635 // just ignoring a slow answer 1636 } 1637 } 1638 1639 void saveTestBinaryDb(ref TestBinaryDb testBinaryDb) @safe nothrow { 1640 import dextool.plugin.mutate.backend.database.type : ChecksumTestCmdMutated; 1641 1642 spinSql!(() @trusted { 1643 auto t = db.transaction; 1644 foreach (a; testBinaryDb.added.byKeyValue) { 1645 db.testCmdApi.add(ChecksumTestCmdMutated(a.key), a.value); 1646 } 1647 // magic number. about 10 Mbyte in the database (8+8+8)*20000 1648 db.testCmdApi.trimMutated(200000); 1649 t.commit; 1650 }); 1651 1652 testBinaryDb.clearAdded; 1653 } 1654 } 1655 1656 private: 1657 1658 /** Compare the old test cases with those that have been found this run. 1659 * 1660 * TODO: the side effect that this function print to the console is NOT good. 1661 */ 1662 bool hasNewTestCases(ref Set!string old_tcs, ref Set!string found_tcs) @safe nothrow { 1663 bool rval; 1664 1665 auto new_tcs = found_tcs.setDifference(old_tcs); 1666 foreach (tc; new_tcs.toRange) { 1667 logger.info(!rval, "Found new test case(s):").collectException; 1668 logger.infof("%s", tc).collectException; 1669 rval = true; 1670 } 1671 1672 return rval; 1673 } 1674 1675 /** Compare old and new test cases to print those that have been removed. 1676 */ 1677 void printDroppedTestCases(ref Set!string old_tcs, ref Set!string changed_tcs) @safe nothrow { 1678 auto diff = old_tcs.setDifference(changed_tcs); 1679 auto removed = diff.toArray; 1680 1681 logger.info(removed.length != 0, "Detected test cases that has been removed:").collectException; 1682 foreach (tc; removed) { 1683 logger.infof("%s", tc).collectException; 1684 } 1685 } 1686 1687 /// Returns: true if all tests cases have unique identifiers 1688 void warnIfConflictingTestCaseIdentifiers(TestCase[] found_tcs) @safe nothrow { 1689 Set!TestCase checked; 1690 bool conflict; 1691 1692 foreach (tc; found_tcs) { 1693 if (checked.contains(tc)) { 1694 logger.info(!conflict, 1695 "Found test cases that do not have global, unique identifiers") 1696 .collectException; 1697 logger.info(!conflict, 1698 "This make the report of test cases that has killed zero mutants unreliable") 1699 .collectException; 1700 logger.info("%s", tc).collectException; 1701 conflict = true; 1702 } 1703 } 1704 } 1705 1706 private: 1707 1708 import dextool.plugin.mutate.backend.database : dbOpenTimeout; 1709 1710 ulong toMinMemory(double percentageOfTotal) { 1711 import core.sys.posix.unistd : _SC_PHYS_PAGES, _SC_PAGESIZE, sysconf; 1712 1713 return cast(ulong)((1.0 - (percentageOfTotal / 100.0)) * sysconf( 1714 _SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE)); 1715 } 1716 1717 auto spawnDbSaveActor(DbSaveActor.Impl self, AbsolutePath dbPath) @trusted { 1718 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ, SchemaSizeQ; 1719 import dextool.plugin.mutate.backend.test_mutant.common_actors : Init, IsDone; 1720 1721 static struct State { 1722 Database db; 1723 } 1724 1725 auto st = tuple!("self", "state")(self, refCounted(State.init)); 1726 alias Ctx = typeof(st); 1727 1728 static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath) nothrow { 1729 try { 1730 ctx.state.get.db = spinSql!(() => Database.make(dbPath), silentLog)(dbOpenTimeout); 1731 } catch (Exception e) { 1732 logger.error(e.msg).collectException; 1733 ctx.self.shutdown; 1734 } 1735 } 1736 1737 static void save2(ref Ctx ctx, MutationTestResult result, TimeoutFsm timeoutFsm) @safe nothrow { 1738 try { 1739 send(ctx.self, result, timeoutFsm.output.iter); 1740 } catch (Exception e) { 1741 logger.warning(e.msg).collectException; 1742 } 1743 } 1744 1745 static void save(ref Ctx ctx, MutationTestResult result, long timeoutIter) @safe nothrow { 1746 void statusUpdate(MutationTestResult result) @safe { 1747 import dextool.plugin.mutate.backend.test_mutant.timeout : updateMutantStatus; 1748 1749 updateMutantStatus(ctx.state.get.db, result.id, result.status, 1750 result.exitStatus, timeoutIter); 1751 ctx.state.get.db.mutantApi.update(result.id, result.profile); 1752 foreach (a; result.testCmds) 1753 ctx.state.get.db.mutantApi.relate(result.id, a.toString); 1754 ctx.state.get.db.testCaseApi.updateMutationTestCases(result.id, result.testCases); 1755 ctx.state.get.db.worklistApi.remove(result.id); 1756 } 1757 1758 spinSql!(() @trusted { 1759 auto t = ctx.state.get.db.transaction; 1760 statusUpdate(result); 1761 t.commit; 1762 }); 1763 } 1764 1765 static void save3(ref Ctx ctx, SchemaQ result) @safe nothrow { 1766 spinSql!(() @trusted { 1767 auto t = ctx.state.get.db.transaction; 1768 foreach (p; result.state.byKeyValue) { 1769 ctx.state.get.db.schemaApi.saveMutantProbability(p.key, p.value, SchemaQ.MaxState); 1770 debug logger.tracef("schemaq saving %s with %s values", p.key, p.value.length); 1771 debug logger.trace(p.value); 1772 } 1773 t.commit; 1774 }); 1775 logger.trace("Saved schemaq").collectException; 1776 } 1777 1778 static void save4(ref Ctx ctx, SchemaSizeQ result) @safe nothrow { 1779 logger.trace("Saving schema size ", result.currentSize).collectException; 1780 spinSql!(() @safe { 1781 ctx.state.get.db.schemaApi.saveSchemaSize(result.currentSize); 1782 }); 1783 } 1784 1785 static bool isDone(IsDone _) @safe nothrow { 1786 // the mailbox is a FIFO queue. all results have been saved if this returns true. 1787 return true; 1788 } 1789 1790 self.name = "db"; 1791 send(self, Init.init, dbPath); 1792 return impl(self, &init_, st, &save, st, &save2, st, &isDone, &save3, st, &save4, st); 1793 } 1794 1795 auto spawnStatActor(StatActor.Impl self, AbsolutePath dbPath) @trusted { 1796 import dextool.plugin.mutate.backend.test_mutant.common_actors : Init, 1797 GetMutantsLeft, UnknownMutantTested, Tick, ForceUpdate; 1798 1799 static struct State { 1800 Database db; 1801 long worklistCount; 1802 } 1803 1804 auto st = tuple!("self", "state")(self, refCounted(State.init)); 1805 alias Ctx = typeof(st); 1806 1807 static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath) nothrow { 1808 try { 1809 ctx.state.get.db = spinSql!(() => Database.make(dbPath), silentLog)(dbOpenTimeout); 1810 send(ctx.self, Tick.init); 1811 } catch (Exception e) { 1812 logger.error(e.msg).collectException; 1813 ctx.self.shutdown; 1814 } 1815 } 1816 1817 static void tick(ref Ctx ctx, Tick _) @safe nothrow { 1818 try { 1819 ctx.state.get.worklistCount = spinSql!(() => ctx.state.get.db.worklistApi.getCount, 1820 logger.trace); 1821 delayedSend(ctx.self, delay(30.dur!"seconds"), Tick.init); 1822 } catch (Exception e) { 1823 logger.error(e.msg).collectException; 1824 } 1825 } 1826 1827 static void unknownTested(ref Ctx ctx, UnknownMutantTested _, long tested) @safe nothrow { 1828 ctx.state.get.worklistCount = max(0, ctx.state.get.worklistCount - tested); 1829 } 1830 1831 static void forceUpdate(ref Ctx ctx, ForceUpdate _) @safe nothrow { 1832 tick(ctx, Tick.init); 1833 } 1834 1835 static long left(ref Ctx ctx, GetMutantsLeft _) @safe nothrow { 1836 return ctx.state.get.worklistCount; 1837 } 1838 1839 self.name = "stat"; 1840 send(self, Init.init, dbPath); 1841 return impl(self, &init_, st, &tick, st, &left, st, &forceUpdate, st, &unknownTested, st); 1842 }