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