1 /** 2 Copyright: Copyright (c) 2020, 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.schemata; 11 12 import logger = std.experimental.logger; 13 import std.algorithm : sort, map, filter, among; 14 import std.array : empty, array, appender; 15 import std.conv : to; 16 import std.datetime : Duration, dur, Clock, SysTime; 17 import std.datetime.stopwatch : StopWatch, AutoStart; 18 import std.exception : collectException; 19 import std.format : format; 20 import std.typecons : Tuple, tuple; 21 22 import blob_model; 23 import colorlog; 24 import miniorm : spinSql, silentLog; 25 import my.actor; 26 import my.gc.refc; 27 import my.optional; 28 import my.container.vector; 29 import proc : DrainElement; 30 import sumtype; 31 32 import my.path; 33 import my.set; 34 35 import dextool.plugin.mutate.backend.database : MutationStatusId, Database, 36 spinSql, SchemataId, Schemata; 37 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 38 import dextool.plugin.mutate.backend.test_mutant.common; 39 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor; 40 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult; 41 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig; 42 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum; 43 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand, 44 UserRuntime, SchemaRuntime; 45 import dextool.plugin.mutate.config : ConfigSchema; 46 47 @safe: 48 49 private { 50 struct Init { 51 } 52 53 struct UpdateWorkList { 54 } 55 56 struct Mark { 57 } 58 59 struct InjectAndCompile { 60 } 61 62 struct ScheduleTestMsg { 63 } 64 65 struct RestoreMsg { 66 } 67 68 struct StartTestMsg { 69 } 70 71 struct CheckStopCondMsg { 72 } 73 } 74 75 struct IsDone { 76 } 77 78 struct GetDoneStatus { 79 } 80 81 struct FinalResult { 82 enum Status { 83 fatalError, 84 invalidSchema, 85 ok 86 } 87 88 Status status; 89 int alive; 90 } 91 92 alias SchemaActor = typedActor!(void function(Init, AbsolutePath, ShellCommand, Duration), 93 bool function(IsDone), void function(UpdateWorkList), FinalResult function(GetDoneStatus), 94 void function(SchemaTestResult), void function(Mark, FinalResult.Status), void function(InjectAndCompile, 95 ShellCommand, Duration), void function(RestoreMsg), void function(StartTestMsg), 96 void function(ScheduleTestMsg), void function(CheckStopCondMsg)); 97 98 auto spawnSchema(SchemaActor.Impl self, FilesysIO fio, ref TestRunner runner, AbsolutePath dbPath, 99 TestCaseAnalyzer testCaseAnalyzer, ConfigSchema conf, SchemataId id, 100 TestStopCheck stopCheck, Mutation.Kind[] kinds, 101 ShellCommand buildCmd, Duration buildCmdTimeout, DbSaveActor.Address dbSave, 102 StatActor.Address stat, TimeoutConfig timeoutConf) @trusted { 103 104 static struct State { 105 SchemataId id; 106 Mutation.Kind[] kinds; 107 TestStopCheck stopCheck; 108 DbSaveActor.Address dbSave; 109 StatActor.Address stat; 110 TimeoutConfig timeoutConf; 111 FilesysIO fio; 112 TestRunner runner; 113 TestCaseAnalyzer analyzer; 114 ConfigSchema conf; 115 116 Database db; 117 118 AbsolutePath[] modifiedFiles; 119 120 InjectIdResult injectIds; 121 122 ScheduleTest scheduler; 123 124 Set!MutationStatusId whiteList; 125 126 Duration compileTime; 127 128 int alive; 129 130 bool hasFatalError; 131 bool isInvalidSchema; 132 133 bool isRunning; 134 } 135 136 auto st = tuple!("self", "state")(self, refCounted(State(id, kinds, stopCheck, 137 dbSave, stat, timeoutConf, fio.dup, runner.dup, testCaseAnalyzer, conf))); 138 alias Ctx = typeof(st); 139 140 static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath, 141 ShellCommand buildCmd, Duration buildCmdTimeout) nothrow { 142 import dextool.plugin.mutate.backend.database : dbOpenTimeout; 143 144 try { 145 ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout); 146 ctx.state.get.scheduler = () { 147 TestMutantActor.Address[] testers; 148 foreach (_0; 0 .. ctx.state.get.conf.parallelMutants) { 149 auto a = ctx.self.homeSystem.spawn(&spawnTestMutant, 150 ctx.state.get.runner.dup, ctx.state.get.analyzer); 151 a.linkTo(ctx.self.address); 152 testers ~= a; 153 } 154 return ScheduleTest(testers); 155 }(); 156 157 ctx.state.get.injectIds = mutantsFromSchema(ctx.state.get.db, 158 ctx.state.get.id, ctx.state.get.kinds); 159 160 if (!ctx.state.get.injectIds.empty) { 161 send(ctx.self, UpdateWorkList.init); 162 send(ctx.self, InjectAndCompile.init, buildCmd, buildCmdTimeout); 163 send(ctx.self, CheckStopCondMsg.init); 164 165 ctx.state.get.isRunning = true; 166 } 167 } catch (Exception e) { 168 ctx.state.get.hasFatalError = true; 169 logger.error(e.msg).collectException; 170 } 171 } 172 173 static bool isDone(ref Ctx ctx, IsDone _) { 174 return !ctx.state.get.isRunning; 175 } 176 177 static void mark(ref Ctx ctx, Mark _, FinalResult.Status status) { 178 import std.traits : EnumMembers; 179 import dextool.plugin.mutate.backend.database : SchemaStatus; 180 181 SchemaStatus schemaStatus; 182 final switch (status) with (FinalResult.Status) { 183 case fatalError: 184 break; 185 case invalidSchema: 186 schemaStatus = SchemaStatus.broken; 187 break; 188 case ok: 189 const total = spinSql!(() => ctx.state.get.db.schemaApi.countMutants(ctx.state.get.id, 190 ctx.state.get.kinds, [EnumMembers!(Mutation.Status)])); 191 const killed = spinSql!(() => ctx.state.get.db.schemaApi.countMutants(ctx.state.get.id, 192 ctx.state.get.kinds, [ 193 Mutation.Status.killed, Mutation.Status.timeout, 194 Mutation.Status.memOverload 195 ])); 196 schemaStatus = (total == killed) ? SchemaStatus.allKilled : SchemaStatus.ok; 197 break; 198 } 199 200 spinSql!(() => ctx.state.get.db.schemaApi.markUsed(ctx.state.get.id, schemaStatus)); 201 } 202 203 static void updateWlist(ref Ctx ctx, UpdateWorkList _) @safe nothrow { 204 if (!ctx.state.get.isRunning) 205 return; 206 207 delayedSend(ctx.self, 1.dur!"minutes".delay, UpdateWorkList.init).collectException; 208 // TODO: should injectIds be updated too? 209 210 try { 211 ctx.state.get.whiteList = spinSql!( 212 () => ctx.state.get.db.schemaApi.getSchemataMutants(ctx.state.get.id, 213 ctx.state.get.kinds)).toSet; 214 logger.trace("update schema worklist mutants: ", ctx.state.get.whiteList.length); 215 debug logger.trace("update schema worklist: ", ctx.state.get.whiteList.toRange); 216 } catch (Exception e) { 217 logger.trace(e.msg).collectException; 218 } 219 } 220 221 static FinalResult doneStatus(ref Ctx ctx, GetDoneStatus _) @safe nothrow { 222 FinalResult.Status status = () { 223 if (ctx.state.get.hasFatalError) 224 return FinalResult.Status.fatalError; 225 if (ctx.state.get.isInvalidSchema) 226 return FinalResult.Status.invalidSchema; 227 return FinalResult.Status.ok; 228 }(); 229 230 if (!ctx.state.get.isRunning) 231 send(ctx.self, Mark.init, status).collectException; 232 233 return FinalResult(status, ctx.state.get.alive); 234 } 235 236 static void save(ref Ctx ctx, SchemaTestResult data) { 237 import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft, 238 UnknownMutantTested; 239 240 void update(MutationTestResult a) { 241 final switch (a.status) with (Mutation.Status) { 242 case skipped: 243 goto case; 244 case unknown: 245 goto case; 246 case equivalent: 247 goto case; 248 case noCoverage: 249 goto case; 250 case alive: 251 ctx.state.get.alive++; 252 ctx.state.get.stopCheck.incrAliveMutants(1); 253 return; 254 case killed: 255 goto case; 256 case timeout: 257 goto case; 258 case memOverload: 259 goto case; 260 case killedByCompiler: 261 break; 262 } 263 } 264 265 debug logger.trace(data); 266 267 if (!data.unstable.empty) { 268 logger.warningf("Unstable test cases found: [%-(%s, %)]", data.unstable); 269 logger.info( 270 "As configured the result is ignored which will force the mutant to be re-tested"); 271 return; 272 } 273 274 update(data.result); 275 276 auto result = data.result; 277 result.profile = MutantTimeProfile(ctx.state.get.compileTime, data.testTime); 278 ctx.state.get.compileTime = Duration.zero; 279 280 logger.infof("%s:%s (%s)", data.result.status, 281 data.result.exitStatus.get, result.profile).collectException; 282 logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`, 283 data.result.testCases.sort.map!"a.name").collectException; 284 285 send(ctx.state.get.dbSave, result, ctx.state.get.timeoutConf.iter); 286 send(ctx.state.get.stat, UnknownMutantTested.init, 1L); 287 288 // an error handler is required because the stat actor can be held up 289 // for more than a minute. 290 ctx.self.request(ctx.state.get.stat, delay(5.dur!"seconds")) 291 .send(GetMutantsLeft.init).then((long x) { 292 logger.infof("%s mutants left to test.", x); 293 }, (ref Actor self, ErrorMsg) {}); 294 295 if (ctx.state.get.injectIds.empty) 296 send(ctx.self, RestoreMsg.init).collectException; 297 } 298 299 static void injectAndCompile(ref Ctx ctx, InjectAndCompile _, 300 ShellCommand buildCmd, Duration buildCmdTimeout) @safe nothrow { 301 try { 302 auto sw = StopWatch(AutoStart.yes); 303 scope (exit) 304 ctx.state.get.compileTime = sw.peek; 305 306 auto codeInject = CodeInject(ctx.state.get.fio, ctx.state.get.conf, ctx.state.get.id); 307 ctx.state.get.modifiedFiles = codeInject.inject(ctx.state.get.db); 308 codeInject.compile(buildCmd, buildCmdTimeout); 309 310 if (ctx.state.get.conf.sanityCheckSchemata) { 311 logger.info("Sanity check of the generated schemata"); 312 const sanity = sanityCheck(ctx.state.get.runner); 313 if (sanity.isOk) { 314 if (ctx.state.get.timeoutConf.base < sanity.runtime) { 315 ctx.state.get.timeoutConf.set(sanity.runtime); 316 ctx.state.get.runner.timeout = ctx.state.get.timeoutConf.value; 317 } 318 319 logger.info("Ok".color(Color.green), ". Using test suite timeout ", 320 ctx.state.get.timeoutConf.value).collectException; 321 send(ctx.self, StartTestMsg.init); 322 } else { 323 logger.info("Skipping the schemata because the test suite failed".color(Color.yellow) 324 .toString); 325 ctx.state.get.isInvalidSchema = true; 326 send(ctx.self, RestoreMsg.init).collectException; 327 } 328 } else { 329 send(ctx.self, StartTestMsg.init); 330 } 331 } catch (Exception e) { 332 ctx.state.get.isInvalidSchema = true; 333 send(ctx.self, RestoreMsg.init).collectException; 334 logger.warning(e.msg).collectException; 335 } 336 } 337 338 static void restore(ref Ctx ctx, RestoreMsg _) @safe nothrow { 339 try { 340 restoreFiles(ctx.state.get.modifiedFiles, ctx.state.get.fio); 341 ctx.state.get.isRunning = false; 342 } catch (Exception e) { 343 ctx.state.get.hasFatalError = true; 344 logger.error(e.msg).collectException; 345 } 346 } 347 348 static void startTest(ref Ctx ctx, StartTestMsg _) @safe nothrow { 349 try { 350 foreach (_0; 0 .. ctx.state.get.scheduler.testers.length) 351 send(ctx.self, ScheduleTestMsg.init); 352 353 logger.tracef("sent %s ScheduleTestMsg", ctx.state.get.scheduler.testers.length); 354 } catch (Exception e) { 355 ctx.state.get.hasFatalError = true; 356 ctx.state.get.isRunning = false; 357 logger.error(e.msg).collectException; 358 } 359 } 360 361 static void test(ref Ctx ctx, ScheduleTestMsg _) nothrow { 362 // TODO: move this printer to another thread because it perform 363 // significant DB lookup and can potentially slow down the testing. 364 void print(MutationStatusId statusId) { 365 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 366 367 auto id = spinSql!(() => ctx.state.get.db.mutantApi.getMutationId(statusId)); 368 if (id.isNull) 369 return; 370 auto entry_ = spinSql!(() => ctx.state.get.db.mutantApi.getMutation(id.get)); 371 if (entry_.isNull) 372 return; 373 auto entry = entry_.get; 374 375 try { 376 const file = ctx.state.get.fio.toAbsoluteRoot(entry.file); 377 auto txt = makeMutationText(ctx.state.get.fio.makeInput(file), 378 entry.mp.offset, entry.mp.mutations[0].kind, entry.lang); 379 debug logger.trace(entry); 380 logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original, 381 txt.mutation, file, entry.sloc.line, entry.sloc.column); 382 } catch (Exception e) { 383 logger.info(e.msg).collectException; 384 } 385 } 386 387 try { 388 if (!ctx.state.get.isRunning) 389 return; 390 391 if (ctx.state.get.injectIds.empty) { 392 logger.trace("no mutants left to test"); 393 return; 394 } 395 396 if (ctx.state.get.scheduler.empty) { 397 logger.trace("no free worker"); 398 delayedSend(ctx.self, 1.dur!"seconds".delay, ScheduleTestMsg.init); 399 return; 400 } 401 402 if (ctx.state.get.stopCheck.isOverloaded) { 403 logger.info(ctx.state.get.stopCheck.overloadToString).collectException; 404 delayedSend(ctx.self, 30.dur!"seconds".delay, ScheduleTestMsg.init); 405 ctx.state.get.stopCheck.pause; 406 return; 407 } 408 409 auto m = ctx.state.get.injectIds.front; 410 ctx.state.get.injectIds.popFront; 411 412 if (m.statusId in ctx.state.get.whiteList) { 413 auto testerId = ctx.state.get.scheduler.pop; 414 auto tester = ctx.state.get.scheduler.get(testerId); 415 print(m.statusId); 416 ctx.self.request(tester, infTimeout).send(m).capture(ctx, 417 testerId).then((ref Capture!(Ctx, size_t) ctx, SchemaTestResult x) { 418 ctx[0].state.get.scheduler.put(ctx[1]); 419 send(ctx[0].self, x); 420 send(ctx[0].self, ScheduleTestMsg.init); 421 }); 422 } else { 423 debug logger.tracef("%s not in whitelist. Skipping", m); 424 send(ctx.self, ScheduleTestMsg.init); 425 } 426 } catch (Exception e) { 427 ctx.state.get.hasFatalError = true; 428 ctx.state.get.isRunning = false; 429 logger.error(e.msg).collectException; 430 } 431 } 432 433 static void checkHaltCond(ref Ctx ctx, CheckStopCondMsg _) @safe nothrow { 434 if (!ctx.state.get.isRunning) 435 return; 436 try { 437 delayedSend(ctx.self, 5.dur!"seconds".delay, CheckStopCondMsg.init).collectException; 438 439 if (ctx.state.get.stopCheck.isHalt != TestStopCheck.HaltReason.none) { 440 send(ctx.self, RestoreMsg.init); 441 logger.info(ctx.state.get.stopCheck.overloadToString).collectException; 442 } 443 } catch (Exception e) { 444 } 445 } 446 447 import std.functional : toDelegate; 448 449 self.name = "schemaDriver"; 450 self.exceptionHandler = toDelegate(&logExceptionHandler); 451 try { 452 send(self, Init.init, dbPath, buildCmd, buildCmdTimeout); 453 } catch (Exception e) { 454 logger.error(e.msg).collectException; 455 self.shutdown; 456 } 457 458 return impl(self, &init_, st, &isDone, st, &updateWlist, st, 459 &doneStatus, st, &save, st, &mark, st, &injectAndCompile, st, 460 &restore, st, &startTest, st, &test, st, &checkHaltCond, st); 461 } 462 463 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit). 464 * 465 * There is a possibility that an injection ID result in a collision because 466 * they are only 32 bit. If that happens the mutant is discarded as unfeasable 467 * to use for schemata. 468 * 469 * TODO: if this is changed to being order dependent then it can handle all 470 * mutants. But I can't see how that can be done easily both because of how the 471 * schemas are generated and how the database is setup. 472 */ 473 struct InjectIdBuilder { 474 private { 475 alias InjectId = InjectIdResult.InjectId; 476 477 InjectId[uint] result; 478 Set!uint collisions; 479 } 480 481 void put(MutationStatusId id, Checksum cs) @safe pure nothrow { 482 import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId; 483 484 const injectId = checksumToId(cs); 485 debug logger.tracef("%s %s %s", id, cs, injectId).collectException; 486 487 if (injectId in collisions) { 488 } else if (injectId in result) { 489 collisions.add(injectId); 490 result.remove(injectId); 491 } else { 492 result[injectId] = InjectId(id, injectId); 493 } 494 } 495 496 InjectIdResult finalize() @safe nothrow { 497 import std.array : array; 498 import std.random : randomCover; 499 500 return InjectIdResult(result.byValue.array.randomCover.array); 501 } 502 } 503 504 struct InjectIdResult { 505 struct InjectId { 506 MutationStatusId statusId; 507 uint injectId; 508 } 509 510 InjectId[] ids; 511 512 InjectId front() @safe pure nothrow { 513 assert(!empty, "Can't get front of an empty range"); 514 return ids[0]; 515 } 516 517 void popFront() @safe pure nothrow { 518 assert(!empty, "Can't pop front of an empty range"); 519 ids = ids[1 .. $]; 520 } 521 522 bool empty() @safe pure nothrow const @nogc { 523 return ids.empty; 524 } 525 } 526 527 /// Extract the mutants that are part of the schema. 528 InjectIdResult mutantsFromSchema(ref Database db, const SchemataId id, const Mutation.Kind[] kinds) { 529 InjectIdBuilder builder; 530 foreach (mutant; spinSql!(() => db.schemaApi.getSchemataMutants(id, kinds))) { 531 auto cs = spinSql!(() => db.mutantApi.getChecksum(mutant)); 532 if (!cs.isNull) 533 builder.put(mutant, cs.get); 534 } 535 debug logger.trace(builder); 536 537 return builder.finalize; 538 } 539 540 @("shall detect a collision and make sure it is never part of the result") 541 unittest { 542 InjectIdBuilder builder; 543 builder.put(MutationStatusId(1), Checksum(1, 2)); 544 builder.put(MutationStatusId(2), Checksum(3, 4)); 545 builder.put(MutationStatusId(3), Checksum(1, 2)); 546 auto r = builder.finalize; 547 548 assert(r.front.statusId == MutationStatusId(2)); 549 r.popFront; 550 assert(r.empty); 551 } 552 553 Edit[] makeRootImpl(ulong end) { 554 import dextool.plugin.mutate.backend.resource : schemataImpl; 555 556 return [ 557 makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl) 558 ]; 559 } 560 561 Edit[] makeHdr() { 562 import dextool.plugin.mutate.backend.resource : schemataHeader; 563 564 return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)]; 565 } 566 567 /** Injects the schema and runtime. 568 * 569 * Uses exceptions to signal failure. 570 */ 571 struct CodeInject { 572 FilesysIO fio; 573 574 SchemataId schemataId; 575 576 Set!AbsolutePath roots; 577 578 bool logSchema; 579 580 this(FilesysIO fio, ConfigSchema conf, SchemataId id) { 581 this.fio = fio; 582 this.schemataId = id; 583 this.logSchema = conf.log; 584 585 foreach (a; conf.userRuntimeCtrl) { 586 auto p = fio.toAbsoluteRoot(a.file); 587 roots.add(p); 588 } 589 } 590 591 /// Throws an error on failure. 592 /// Returns: modified files. 593 AbsolutePath[] inject(ref Database db) { 594 auto schemata = spinSql!(() => db.schemaApi.getSchemata(schemataId)).get; 595 auto modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file)) 596 .toSet.toRange.array; 597 598 void initRoots(ref Database db) { 599 if (roots.empty) { 600 auto allRoots = () { 601 AbsolutePath[] tmp; 602 try { 603 tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get) 604 .map!(a => fio.toAbsoluteRoot(a)) 605 .array; 606 if (tmp.empty) { 607 // no root found. Inject the runtime in all files and "hope for 608 // the best". it will be less efficient but the weak symbol 609 // should still mean that it link correctly. 610 tmp = modifiedFiles; 611 } 612 } catch (Exception e) { 613 logger.error(e.msg).collectException; 614 } 615 return tmp; 616 }(); 617 618 foreach (r; allRoots) { 619 roots.add(r); 620 } 621 } 622 623 auto mods = modifiedFiles.toSet; 624 foreach (r; roots.toRange) { 625 if (r !in mods) 626 modifiedFiles ~= r; 627 } 628 629 if (roots.empty) 630 throw new Exception("No root file found to inject the schemata runtime in"); 631 } 632 633 void injectCode() { 634 import std.path : extension, stripExtension; 635 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 636 637 Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) { 638 auto edits = appender!(Edit[])(); 639 edits.put(extra); 640 foreach (a; fragments) { 641 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text); 642 } 643 auto m = merge(original, edits.data); 644 return change(new Blob(original.uri, original.content), m.edits); 645 } 646 647 SchemataFragment[] fragments(Path p) { 648 return schemata.fragments.filter!(a => a.file == p).array; 649 } 650 651 foreach (fname; modifiedFiles) { 652 auto f = fio.makeInput(fname); 653 auto extra = () { 654 if (fname in roots) { 655 logger.trace("Injecting schemata runtime in ", fname); 656 return makeRootImpl(f.content.length); 657 } 658 return makeHdr; 659 }(); 660 661 logger.info("Injecting schema in ", fname); 662 663 // writing the schemata. 664 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra); 665 fio.makeOutput(fname).write(s); 666 667 if (logSchema) { 668 const ext = fname.toString.extension; 669 fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension, 670 schemataId.get, ext).Path)).write(s); 671 } 672 } 673 } 674 675 initRoots(db); 676 injectCode; 677 678 return modifiedFiles; 679 } 680 681 void compile(ShellCommand buildCmd, Duration buildCmdTimeout) { 682 import dextool.plugin.mutate.backend.test_mutant.common : compile; 683 684 logger.infof("Compile schema %s", schemataId.get).collectException; 685 686 compile(buildCmd, buildCmdTimeout, PrintCompileOnFailure(true)).match!((Mutation.Status a) { 687 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow) 688 .toString); 689 }, (bool success) { 690 if (!success) { 691 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow) 692 .toString); 693 } 694 }); 695 696 logger.info("Ok".color(Color.green)).collectException; 697 } 698 } 699 700 // Check that the test suite successfully execute "passed". 701 // Returns: true on success. 702 Tuple!(bool, "isOk", Duration, "runtime") sanityCheck(ref TestRunner runner) { 703 auto sw = StopWatch(AutoStart.yes); 704 auto res = runner.run; 705 return typeof(return)(res.status == TestResult.Status.passed, sw.peek); 706 } 707 708 /// Round robin scheduling of mutants for testing from the worker pool. 709 struct ScheduleTest { 710 TestMutantActor.Address[] testers; 711 Vector!size_t free; 712 713 this(TestMutantActor.Address[] testers) { 714 this.testers = testers; 715 foreach (size_t i; 0 .. testers.length) 716 free.put(i); 717 } 718 719 bool empty() @safe pure nothrow const @nogc { 720 return free.empty; 721 } 722 723 size_t pop() 724 in (free.length <= testers.length) { 725 scope (exit) 726 free.popFront(); 727 return free.front; 728 } 729 730 void put(size_t x) 731 in (x < testers.length) 732 out (; free.length <= testers.length)do { 733 free.put(x); 734 } 735 736 TestMutantActor.Address get(size_t x) 737 in (free.length <= testers.length) 738 in (x < testers.length) { 739 return testers[x]; 740 } 741 } 742 743 struct SchemaTestResult { 744 MutationTestResult result; 745 Duration testTime; 746 TestCase[] unstable; 747 } 748 749 alias TestMutantActor = typedActor!(SchemaTestResult function(InjectIdResult.InjectId id)); 750 751 auto spawnTestMutant(TestMutantActor.Impl self, TestRunner runner, TestCaseAnalyzer analyzer) { 752 static struct State { 753 TestRunner runner; 754 TestCaseAnalyzer analyzer; 755 } 756 757 auto st = tuple!("self", "state")(self, refCounted(State(runner, analyzer))); 758 alias Ctx = typeof(st); 759 760 static SchemaTestResult run(ref Ctx ctx, InjectIdResult.InjectId id) @safe nothrow { 761 import std.datetime.stopwatch : StopWatch, AutoStart; 762 import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey; 763 764 SchemaTestResult analyzeForTestCase(SchemaTestResult rval, 765 ref DrainElement[][ShellCommand] output) @safe nothrow { 766 foreach (testCmd; output.byKeyValue) { 767 try { 768 auto analyze = ctx.state.get.analyzer.analyze(testCmd.key, testCmd.value); 769 770 analyze.match!((TestCaseAnalyzer.Success a) { 771 rval.result.testCases ~= a.failed ~ a.testCmd; 772 }, (TestCaseAnalyzer.Unstable a) { 773 rval.unstable ~= a.unstable; 774 // must re-test the mutant 775 rval.result.status = Mutation.Status.unknown; 776 }, (TestCaseAnalyzer.Failed a) { 777 logger.tracef("The parsers that analyze the output from %s failed", 778 testCmd.key); 779 }); 780 } catch (Exception e) { 781 logger.warning(e.msg).collectException; 782 } 783 } 784 return rval; 785 } 786 787 auto sw = StopWatch(AutoStart.yes); 788 789 SchemaTestResult rval; 790 791 rval.result.id = id.statusId; 792 793 auto env = ctx.state.get.runner.getDefaultEnv; 794 env[schemataMutantEnvKey] = id.injectId.to!string; 795 796 auto res = runTester(ctx.state.get.runner, env); 797 rval.result.status = res.status; 798 rval.result.exitStatus = res.exitStatus; 799 800 if (!ctx.state.get.analyzer.empty) 801 rval = analyzeForTestCase(rval, res.output); 802 803 rval.testTime = sw.peek; 804 return rval; 805 } 806 807 self.name = "testMutant"; 808 return impl(self, &run, st); 809 }