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