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, Nullable; 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, FileId; 37 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 38 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ, SchemaSizeQ, SchemaStatus; 39 import dextool.plugin.mutate.backend.test_mutant.common; 40 import dextool.plugin.mutate.backend.test_mutant.common_actors : DbSaveActor, StatActor; 41 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult; 42 import dextool.plugin.mutate.backend.test_mutant.timeout : TimeoutFsm, TimeoutConfig; 43 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum; 44 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand, 45 UserRuntime, SchemaRuntime; 46 import dextool.plugin.mutate.config : ConfigSchema; 47 48 @safe: 49 50 private { 51 struct Init { 52 } 53 54 struct GenSchema { 55 } 56 57 struct RunSchema { 58 } 59 60 struct UpdateWorkList { 61 } 62 63 struct MarkMsg { 64 } 65 66 struct InjectAndCompile { 67 } 68 69 struct ScheduleTestMsg { 70 } 71 72 struct RestoreMsg { 73 } 74 75 struct StartTestMsg { 76 } 77 78 struct CheckStopCondMsg { 79 } 80 81 struct Stop { 82 } 83 } 84 85 struct IsDone { 86 } 87 88 struct GetDoneStatus { 89 } 90 91 struct FinalResult { 92 enum Status { 93 fatalError, 94 invalidSchema, 95 ok 96 } 97 98 Status status; 99 int alive; 100 } 101 102 struct ConfTesters { 103 } 104 105 // dfmt off 106 alias SchemaActor = typedActor!( 107 void function(Init, AbsolutePath database, ShellCommand, Duration), 108 /// Generate a schema, if possible 109 void function(GenSchema), 110 void function(RunSchema, SchemataBuilder.ET, InjectIdResult), 111 /// Quary the schema actor to see if it is done 112 bool function(IsDone), 113 /// Update the list of mutants that are still in the worklist. 114 void function(UpdateWorkList, bool), 115 FinalResult function(GetDoneStatus), 116 /// Save the result of running the schema to the DB. 117 void function(SchemaTestResult), 118 void function(MarkMsg, FinalResult.Status), 119 /// Inject the schema in the source code and compile it. 120 void function(InjectAndCompile), 121 /// Restore the source code. 122 void function(RestoreMsg), 123 /// Start running the schema. 124 void function(StartTestMsg), 125 void function(ScheduleTestMsg), 126 void function(CheckStopCondMsg), 127 void function(ConfTesters), 128 // Queue up a msg that set isRunning to false. Convenient to ensure that a 129 // RestoreMsg has been processed before setting to false. 130 void function(Stop), 131 ); 132 // dfmt on 133 134 auto spawnSchema(SchemaActor.Impl self, FilesysIO fio, ref TestRunner runner, 135 AbsolutePath dbPath, TestCaseAnalyzer testCaseAnalyzer, 136 ConfigSchema conf, TestStopCheck stopCheck, ShellCommand buildCmd, Duration buildCmdTimeout, 137 DbSaveActor.Address dbSave, StatActor.Address stat, TimeoutConfig timeoutConf) @trusted { 138 139 static struct State { 140 TestStopCheck stopCheck; 141 DbSaveActor.Address dbSave; 142 StatActor.Address stat; 143 TimeoutConfig timeoutConf; 144 FilesysIO fio; 145 TestRunner runner; 146 TestCaseAnalyzer analyzer; 147 ConfigSchema conf; 148 149 Database db; 150 151 GenSchemaActor.Address genSchema; 152 SchemaSizeQUpdateActor.Address sizeQUpdater; 153 154 ShellCommand buildCmd; 155 Duration buildCmdTimeout; 156 157 SchemataBuilder.ET activeSchema; 158 enum ActiveSchemaCheck { 159 noMutantTested, 160 testing, 161 triggerRestoreOnce, 162 } 163 // used to detect a corner case which is that no mutant in the schema is in the whitelist. 164 ActiveSchemaCheck activeSchemaCheck; 165 Set!Checksum usedScheman; 166 167 AbsolutePath[] modifiedFiles; 168 169 InjectIdResult injectIds; 170 171 ScheduleTest scheduler; 172 173 Set!MutationStatusId whiteList; 174 175 Duration compileTime; 176 177 int alive; 178 179 bool hasFatalError; 180 181 bool isRunning; 182 } 183 184 auto st = tuple!("self", "state")(self, refCounted(State(stopCheck, dbSave, 185 stat, timeoutConf, fio.dup, runner.dup, testCaseAnalyzer, conf))); 186 alias Ctx = typeof(st); 187 188 static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath, 189 ShellCommand buildCmd, Duration buildCmdTimeout) nothrow { 190 import dextool.plugin.mutate.backend.database : dbOpenTimeout; 191 192 try { 193 ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout); 194 ctx.state.get.buildCmd = buildCmd; 195 ctx.state.get.buildCmdTimeout = buildCmdTimeout; 196 197 ctx.state.get.timeoutConf.timeoutScaleFactor = ctx.state.get.conf.timeoutScaleFactor; 198 logger.tracef("Timeout Scale Factor: %s", ctx.state.get.timeoutConf.timeoutScaleFactor); 199 200 ctx.state.get.scheduler = () { 201 TestMutantActor.Address[] testers; 202 foreach (_0; 0 .. ctx.state.get.conf.parallelMutants) { 203 auto a = ctx.self.homeSystem.spawn(&spawnTestMutant, 204 ctx.state.get.runner.dup, ctx.state.get.analyzer); 205 a.linkTo(ctx.self.address); 206 testers ~= a; 207 } 208 return ScheduleTest(testers); 209 }(); 210 211 ctx.state.get.sizeQUpdater = ctx.self.homeSystem.spawn(&spawnSchemaSizeQ, 212 getSchemaSizeQ(ctx.state.get.db, ctx.state.get.conf.mutantsPerSchema.get, 213 ctx.state.get.conf.minMutantsPerSchema.get), ctx.state.get.dbSave); 214 linkTo(ctx.self, ctx.state.get.sizeQUpdater); 215 216 ctx.state.get.genSchema = ctx.self.homeSystem.spawn(&spawnGenSchema, 217 dbPath, ctx.state.get.conf, ctx.state.get.sizeQUpdater); 218 linkTo(ctx.self, ctx.state.get.genSchema); 219 220 send(ctx.self, UpdateWorkList.init, true); 221 send(ctx.self, CheckStopCondMsg.init); 222 send(ctx.self, GenSchema.init); 223 ctx.state.get.isRunning = true; 224 } catch (Exception e) { 225 ctx.state.get.hasFatalError = true; 226 logger.error(e.msg).collectException; 227 } 228 } 229 230 static void generateSchema(ref Ctx ctx, GenSchema _) @trusted nothrow { 231 try { 232 ctx.state.get.activeSchema = typeof(ctx.state.get.activeSchema).init; 233 ctx.state.get.injectIds = typeof(ctx.state.get.injectIds).init; 234 235 ctx.self.request(ctx.state.get.genSchema, infTimeout) 236 .send(GenSchema.init).capture(ctx).then((ref Ctx ctx, GenSchemaResult result) nothrow{ 237 if (result.noMoreScheman) { 238 ctx.state.get.isRunning = false; 239 } else { 240 try { 241 send(ctx.self, RunSchema.init, result.schema, result.injectIds); 242 } catch (Exception e) { 243 ctx.state.get.isRunning = false; 244 logger.error(e.msg).collectException; 245 } 246 } 247 }); 248 } catch (Exception e) { 249 logger.warning(e.msg).collectException; 250 } 251 } 252 253 static void runSchema(ref Ctx ctx, RunSchema _, SchemataBuilder.ET schema, 254 InjectIdResult injectIds) @safe nothrow { 255 try { 256 if (!ctx.state.get.isRunning) { 257 return; 258 } 259 if (schema.checksum.value in ctx.state.get.usedScheman) { 260 // discard, already used 261 send(ctx.self, GenSchema.init); 262 return; 263 } 264 265 ctx.state.get.usedScheman.add(schema.checksum.value); 266 267 logger.trace("schema generated ", schema.checksum); 268 logger.trace(schema.fragments.map!"a.file"); 269 logger.trace(schema.mutants); 270 logger.trace(injectIds); 271 272 if (injectIds.empty || injectIds.length < ctx.state.get.conf.minMutantsPerSchema.get) { 273 send(ctx.self, GenSchema.init); 274 } else { 275 ctx.state.get.activeSchema = schema; 276 ctx.state.get.injectIds = injectIds; 277 send(ctx.self, InjectAndCompile.init); 278 } 279 } catch (Exception e) { 280 logger.error(e.msg).collectException; 281 } 282 } 283 284 static void confTesters(ref Ctx ctx, ConfTesters _) { 285 foreach (a; ctx.state.get.scheduler.testers) { 286 send(a, ctx.state.get.timeoutConf); 287 } 288 } 289 290 static bool isDone(ref Ctx ctx, IsDone _) { 291 return !ctx.state.get.isRunning; 292 } 293 294 static void mark(ref Ctx ctx, MarkMsg _, FinalResult.Status status) @safe nothrow { 295 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ; 296 297 static void updateSchemaQ(ref SchemaQ sq, ref SchemataBuilder.ET schema, 298 const SchemaStatus status) @trusted nothrow { 299 import my.hash : Checksum64; 300 import my.set; 301 302 auto paths = schema.fragments.map!"a.file".toSet.toRange.array; 303 Set!Checksum64 latestFiles; 304 305 foreach (path; paths) { 306 scope getPath = (SchemaStatus s) { 307 return (s == status) ? schema.mutants.map!"a.mut.kind".toSet.toRange.array 308 : null; 309 }; 310 try { 311 sq.update(path, getPath); 312 } catch (Exception e) { 313 logger.warning(e.msg).collectException; 314 } 315 latestFiles.add(sq.pathCache[path]); 316 debug logger.tracef("updating %s %s", path, sq.pathCache[path]); 317 } 318 319 // TODO: remove prob for non-existing files 320 sq.scatterTick; 321 } 322 323 SchemaStatus schemaStatus = () { 324 final switch (status) with (FinalResult.Status) { 325 case fatalError: 326 goto case; 327 case invalidSchema: 328 return SchemaStatus.broken; 329 case ok: 330 // TODO: remove SchemaStatus.allKilled 331 return SchemaStatus.ok; 332 } 333 }(); 334 335 try { 336 auto schemaQ = spinSql!(() => SchemaQ(ctx.state.get.db.schemaApi.getMutantProbability)); 337 updateSchemaQ(schemaQ, ctx.state.get.activeSchema, schemaStatus); 338 send(ctx.state.get.dbSave, schemaQ); 339 340 send(ctx.state.get.sizeQUpdater, SchemaGenStatusMsg.init, 341 schemaStatus, cast(long) ctx.state.get.activeSchema.mutants.length); 342 } catch (Exception e) { 343 logger.trace(e.msg).collectException; 344 } 345 } 346 347 static void updateWlist(ref Ctx ctx, UpdateWorkList _, bool repeat) @safe nothrow { 348 if (!ctx.state.get.isRunning) 349 return; 350 351 try { 352 if (repeat) 353 delayedSend(ctx.self, 1.dur!"minutes".delay, UpdateWorkList.init, true); 354 355 ctx.state.get.whiteList = spinSql!(() => ctx.state.get.db.worklistApi.getAll) 356 .map!"a.id".toSet; 357 send(ctx.state.get.genSchema, ctx.state.get.whiteList.toArray); 358 359 logger.trace("update schema worklist: ", ctx.state.get.whiteList.length); 360 debug logger.trace("update schema worklist: ", ctx.state.get.whiteList.toRange); 361 } catch (Exception e) { 362 logger.trace(e.msg).collectException; 363 } 364 } 365 366 static FinalResult doneStatus(ref Ctx ctx, GetDoneStatus _) @safe nothrow { 367 FinalResult.Status status = () { 368 if (ctx.state.get.hasFatalError) 369 return FinalResult.Status.fatalError; 370 return FinalResult.Status.ok; 371 }(); 372 373 return FinalResult(status, ctx.state.get.alive); 374 } 375 376 static void save(ref Ctx ctx, SchemaTestResult data) { 377 import dextool.plugin.mutate.backend.test_mutant.common_actors : GetMutantsLeft, 378 UnknownMutantTested; 379 380 void update(MutationTestResult a) { 381 final switch (a.status) with (Mutation.Status) { 382 case skipped: 383 goto case; 384 case unknown: 385 goto case; 386 case equivalent: 387 goto case; 388 case noCoverage: 389 goto case; 390 case alive: 391 ctx.state.get.alive++; 392 ctx.state.get.stopCheck.incrAliveMutants(1); 393 return; 394 case killed: 395 goto case; 396 case timeout: 397 goto case; 398 case memOverload: 399 goto case; 400 case killedByCompiler: 401 break; 402 } 403 } 404 405 debug logger.trace(data); 406 407 if (!data.unstable.empty) { 408 logger.warningf("Unstable test cases found: [%-(%s, %)]", data.unstable); 409 logger.info( 410 "As configured the result is ignored which will force the mutant to be re-tested"); 411 return; 412 } 413 414 update(data.result); 415 416 auto result = data.result; 417 result.profile = MutantTimeProfile(ctx.state.get.compileTime, data.testTime); 418 ctx.state.get.compileTime = Duration.zero; 419 420 logger.infof("%s:%s (%s)", data.result.status, 421 data.result.exitStatus.get, result.profile).collectException; 422 logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`, 423 data.result.testCases.sort.map!"a.name").collectException; 424 425 send(ctx.state.get.dbSave, result, ctx.state.get.timeoutConf.iter); 426 send(ctx.state.get.stat, UnknownMutantTested.init, 1L); 427 428 // an error handler is required because the stat actor can be held up 429 // for more than a minute. 430 ctx.self.request(ctx.state.get.stat, delay(5.dur!"seconds")) 431 .send(GetMutantsLeft.init).then((long x) { 432 logger.infof("%s mutants left to test.", x); 433 }, (ref Actor self, ErrorMsg) {}); 434 435 if (ctx.state.get.injectIds.empty && ctx.state.get.scheduler.full) { 436 logger.trace("done saving result for schema ", 437 ctx.state.get.activeSchema.checksum).collectException; 438 send(ctx.self, MarkMsg.init, FinalResult.Status.ok); 439 send(ctx.self, UpdateWorkList.init, false); 440 send(ctx.self, RestoreMsg.init).collectException; 441 } 442 } 443 444 static void injectAndCompile(ref Ctx ctx, InjectAndCompile _) @safe nothrow { 445 try { 446 auto sw = StopWatch(AutoStart.yes); 447 scope (exit) 448 ctx.state.get.compileTime = sw.peek; 449 450 logger.infof("Using schema with %s mutants", ctx.state.get.injectIds.length); 451 452 auto codeInject = CodeInject(ctx.state.get.fio, ctx.state.get.conf); 453 ctx.state.get.modifiedFiles = codeInject.inject(ctx.state.get.db, 454 ctx.state.get.activeSchema); 455 codeInject.compile(ctx.state.get.buildCmd, ctx.state.get.buildCmdTimeout); 456 457 if (ctx.state.get.conf.sanityCheckSchemata) { 458 logger.info("Sanity check of the generated schemata"); 459 const sanity = sanityCheck(ctx.state.get.runner); 460 if (sanity.isOk) { 461 if (ctx.state.get.timeoutConf.base < sanity.runtime) { 462 ctx.state.get.timeoutConf.set(sanity.runtime); 463 send(ctx.self, ConfTesters.init); 464 } 465 466 logger.info("Ok".color(Color.green), ". Using test suite timeout ", 467 ctx.state.get.timeoutConf.value).collectException; 468 send(ctx.self, StartTestMsg.init); 469 } else { 470 logger.info("Skipping the schemata because the test suite failed".color(Color.yellow) 471 .toString); 472 send(ctx.self, MarkMsg.init, FinalResult.Status.invalidSchema); 473 send(ctx.self, RestoreMsg.init).collectException; 474 } 475 } else { 476 send(ctx.self, StartTestMsg.init); 477 } 478 } catch (Exception e) { 479 send(ctx.self, MarkMsg.init, FinalResult.Status.invalidSchema).collectException; 480 send(ctx.self, RestoreMsg.init).collectException; 481 logger.warning(e.msg).collectException; 482 } 483 } 484 485 static void restore(ref Ctx ctx, RestoreMsg _) @safe nothrow { 486 import dextool.plugin.mutate.backend.test_mutant.common : restoreFiles; 487 488 try { 489 logger.trace("restore ", ctx.state.get.modifiedFiles); 490 restoreFiles(ctx.state.get.modifiedFiles, ctx.state.get.fio); 491 ctx.state.get.modifiedFiles = null; 492 send(ctx.self, GenSchema.init); 493 } catch (Exception e) { 494 ctx.state.get.hasFatalError = true; 495 ctx.state.get.isRunning = false; 496 logger.error(e.msg).collectException; 497 } 498 } 499 500 static void startTest(ref Ctx ctx, StartTestMsg _) @safe nothrow { 501 ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.noMutantTested; 502 503 try { 504 foreach (_0; 0 .. ctx.state.get.scheduler.testers.length) 505 send(ctx.self, ScheduleTestMsg.init); 506 logger.tracef("sent %s ScheduleTestMsg", ctx.state.get.scheduler.testers.length); 507 } catch (Exception e) { 508 ctx.state.get.hasFatalError = true; 509 ctx.state.get.isRunning = false; 510 logger.error(e.msg).collectException; 511 } 512 } 513 514 static void test(ref Ctx ctx, ScheduleTestMsg _) nothrow { 515 // TODO: move this printer to another thread because it perform 516 // significant DB lookup and can potentially slow down the testing. 517 void print(MutationStatusId statusId) { 518 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 519 520 auto entry_ = spinSql!(() => ctx.state.get.db.mutantApi.getMutation(statusId)); 521 if (entry_.isNull) 522 return; 523 auto entry = entry_.get; 524 525 try { 526 const file = ctx.state.get.fio.toAbsoluteRoot(entry.file); 527 auto txt = makeMutationText(ctx.state.get.fio.makeInput(file), 528 entry.mp.offset, entry.mp.mutations[0].kind, entry.lang); 529 debug logger.trace(entry); 530 logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original, 531 txt.mutation, file, entry.sloc.line, entry.sloc.column); 532 } catch (Exception e) { 533 logger.info(e.msg).collectException; 534 } 535 } 536 537 if (!ctx.state.get.isRunning) 538 return; 539 540 try { 541 if (ctx.state.get.injectIds.empty) { 542 logger.trace("no mutants left to test ", ctx.state.get.scheduler.free.length); 543 if (ctx.state.get.activeSchemaCheck == State.ActiveSchemaCheck.noMutantTested 544 && ctx.state.get.scheduler.full) { 545 // no mutant has been tested in the schema thus the restore in save is never triggered. 546 send(ctx.self, RestoreMsg.init); 547 ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.triggerRestoreOnce; 548 } 549 return; 550 } 551 552 if (ctx.state.get.scheduler.empty) { 553 logger.trace("no free worker"); 554 delayedSend(ctx.self, 1.dur!"seconds".delay, ScheduleTestMsg.init); 555 return; 556 } 557 558 if (ctx.state.get.stopCheck.isOverloaded) { 559 logger.info(ctx.state.get.stopCheck.overloadToString).collectException; 560 delayedSend(ctx.self, 30.dur!"seconds".delay, ScheduleTestMsg.init); 561 return; 562 } 563 564 auto m = ctx.state.get.injectIds.front; 565 ctx.state.get.injectIds.popFront; 566 567 if (m.statusId in ctx.state.get.whiteList) { 568 ctx.state.get.activeSchemaCheck = State.ActiveSchemaCheck.testing; 569 auto testerId = ctx.state.get.scheduler.pop; 570 auto tester = ctx.state.get.scheduler.get(testerId); 571 print(m.statusId); 572 ctx.self.request(tester, infTimeout).send(m).capture(ctx, 573 testerId).then((ref Capture!(Ctx, size_t) ctx, SchemaTestResult x) { 574 ctx[0].state.get.scheduler.put(ctx[1]); 575 send(ctx[0].self, x); 576 send(ctx[0].self, ScheduleTestMsg.init); 577 }); 578 } else { 579 debug logger.tracef("%s not in whitelist. Skipping", m); 580 send(ctx.self, ScheduleTestMsg.init); 581 } 582 } catch (Exception e) { 583 ctx.state.get.hasFatalError = true; 584 ctx.state.get.isRunning = false; 585 logger.error(e.msg).collectException; 586 } 587 } 588 589 static void checkHaltCond(ref Ctx ctx, CheckStopCondMsg _) @safe nothrow { 590 if (!ctx.state.get.isRunning) 591 return; 592 593 try { 594 delayedSend(ctx.self, 5.dur!"seconds".delay, CheckStopCondMsg.init).collectException; 595 596 const halt = ctx.state.get.stopCheck.isHalt; 597 if (halt == TestStopCheck.HaltReason.overloaded) 598 ctx.state.get.stopCheck.startBgShutdown; 599 600 if (halt != TestStopCheck.HaltReason.none) { 601 send(ctx.self, RestoreMsg.init); 602 send(ctx.self, Stop.init); 603 logger.info(ctx.state.get.stopCheck.overloadToString).collectException; 604 } 605 } catch (Exception e) { 606 ctx.state.get.isRunning = false; 607 logger.error(e.msg).collectException; 608 } 609 } 610 611 static void stop(ref Ctx ctx, Stop _) @safe nothrow { 612 ctx.state.get.isRunning = false; 613 } 614 615 import std.functional : toDelegate; 616 617 self.name = "schemaDriver"; 618 self.exceptionHandler = toDelegate(&logExceptionHandler); 619 try { 620 send(self, Init.init, dbPath, buildCmd, buildCmdTimeout); 621 } catch (Exception e) { 622 logger.error(e.msg).collectException; 623 self.shutdown; 624 } 625 626 return impl(self, &init_, st, &isDone, st, &updateWlist, st, 627 &doneStatus, st, &save, st, &mark, st, &injectAndCompile, st, 628 &restore, st, &startTest, st, &test, st, &checkHaltCond, st, 629 &confTesters, st, &generateSchema, st, &runSchema, st, &stop, st); 630 } 631 632 private SchemaSizeQ getSchemaSizeQ(ref Database db, const long userInit, const long minSize) @trusted nothrow { 633 // 1.1 is a magic number. it feels good. the purpose is to be a little 634 // leniant with the size to demonstrate to the user that it is OK to 635 // raise the max size. At the same time it shouldn't be too much 636 // because the user may have configured it to a low value for a reason. 637 auto sq = SchemaSizeQ.make(minSize, cast(long)(userInit * 1.1)); 638 sq.updateSize(spinSql!(() => db.schemaApi.getSchemaSize(userInit))); 639 sq.testMutantsSize = spinSql!(() => db.worklistApi.getCount); 640 return sq; 641 } 642 643 private { 644 struct GenSchemaResult { 645 bool noMoreScheman; 646 SchemataBuilder.ET schema; 647 InjectIdResult injectIds; 648 } 649 } 650 651 // dfmt off 652 alias GenSchemaActor = typedActor!( 653 void function(Init, AbsolutePath database, ConfigSchema conf), 654 GenSchemaResult function(GenSchema), 655 void function(MutationStatusId[] whiteList), 656 ); 657 // dfmt on 658 659 private auto spawnGenSchema(GenSchemaActor.Impl self, AbsolutePath dbPath, 660 ConfigSchema conf, SchemaSizeQUpdateActor.Address sizeQUpdater) @trusted { 661 static struct State { 662 ConfigSchema conf; 663 SchemaSizeQUpdateActor.Address sizeQUpdater; 664 Database db; 665 SchemaBuildState schemaBuild; 666 Set!MutationStatusId whiteList; 667 } 668 669 auto st = tuple!("self", "state")(self, refCounted(State(conf, sizeQUpdater))); 670 alias Ctx = typeof(st); 671 672 static void init_(ref Ctx ctx, Init _, AbsolutePath dbPath, ConfigSchema conf) nothrow { 673 import dextool.plugin.mutate.backend.database : dbOpenTimeout; 674 675 try { 676 ctx.state.get.db = spinSql!(() => Database.make(dbPath), logger.trace)(dbOpenTimeout); 677 678 ctx.state.get.schemaBuild.minMutantsPerSchema = ctx.state.get.conf.minMutantsPerSchema; 679 ctx.state.get.schemaBuild.mutantsPerSchema.get = ctx.state.get.conf 680 .mutantsPerSchema.get; 681 ctx.state.get.schemaBuild.initFiles( 682 spinSql!(() => ctx.state.get.db.fileApi.getFileIds)); 683 } catch (Exception e) { 684 logger.error(e.msg).collectException; 685 // TODO: should terminate? 686 } 687 } 688 689 static void updateWhiteList(ref Ctx ctx, MutationStatusId[] whiteList) @trusted nothrow { 690 try { 691 ctx.state.get.whiteList = whiteList.toSet; 692 send(ctx.state.get.sizeQUpdater, MutantsToTestMsg.init, 693 cast(long) ctx.state.get.whiteList.length); 694 695 ctx.state.get.schemaBuild.builder.schemaQ = spinSql!( 696 () => SchemaQ(ctx.state.get.db.schemaApi.getMutantProbability)); 697 698 ctx.self.request(ctx.state.get.sizeQUpdater, infTimeout) 699 .send(GetSchemaSizeMsg.init).capture(ctx).then((ref Ctx ctx, long sz) nothrow{ 700 ctx.state.get.schemaBuild.mutantsPerSchema.get = sz; 701 }); 702 } catch (Exception e) { 703 } 704 } 705 706 static GenSchemaResult genSchema(ref Ctx ctx, GenSchema _) nothrow { 707 static void process(ref Ctx ctx, ref GenSchemaResult result, 708 ref Set!MutationStatusId whiteList) @safe { 709 auto value = ctx.state.get.schemaBuild.process; 710 value.match!((Some!(SchemataBuilder.ET) a) { 711 result.schema = a; 712 result.injectIds = mutantsFromSchema(a, whiteList); 713 }, (None a) {}); 714 } 715 716 static void processFile(ref Ctx ctx, ref Set!MutationStatusId whiteList) @trusted nothrow { 717 if (ctx.state.get.schemaBuild.files.isDone) 718 return; 719 720 size_t frags; 721 while (frags == 0 && ctx.state.get.schemaBuild.files.filesLeft != 0) { 722 logger.trace("Files left ", 723 ctx.state.get.schemaBuild.files.filesLeft).collectException; 724 frags = spinSql!(() { 725 auto trans = ctx.state.get.db.transaction; 726 return ctx.state.get.schemaBuild.updateFiles(whiteList, 727 (FileId id) => spinSql!(() => ctx.state.get.db.schemaApi.getFragments(id)), 728 (FileId id) => spinSql!(() => ctx.state.get.db.getFile(id)), 729 (MutationStatusId id) => spinSql!( 730 () => ctx.state.get.db.mutantApi.getKind(id))); 731 }); 732 } 733 } 734 735 GenSchemaResult result; 736 737 logger.trace("Generate schema").collectException; 738 while (ctx.state.get.schemaBuild.st != SchemaBuildState.State.done) { 739 ctx.state.get.schemaBuild.tick; 740 741 final switch (ctx.state.get.schemaBuild.st) { 742 case SchemaBuildState.State.none: 743 break; 744 case SchemaBuildState.State.processFiles: 745 try { 746 processFile(ctx, ctx.state.get.whiteList); 747 process(ctx, result, ctx.state.get.whiteList); 748 } catch (Exception e) { 749 logger.trace(e.msg).collectException; 750 return GenSchemaResult(true); 751 } 752 break; 753 case SchemaBuildState.State.prepareReduction: 754 send(ctx.state.get.sizeQUpdater, 755 FullSchemaGenDoneMsg.init).collectException; 756 goto case; 757 case SchemaBuildState.State.prepareFinalize: 758 try { 759 ctx.state.get.schemaBuild.files.reset; 760 } catch (Exception e) { 761 logger.trace(e.msg).collectException; 762 return GenSchemaResult(true); 763 } 764 break; 765 case SchemaBuildState.State.reduction: 766 goto case; 767 case SchemaBuildState.State.finalize1: 768 goto case; 769 case SchemaBuildState.State.finalize2: 770 try { 771 processFile(ctx, ctx.state.get.whiteList); 772 process(ctx, result, ctx.state.get.whiteList); 773 } catch (Exception e) { 774 logger.trace(e.msg).collectException; 775 return GenSchemaResult(true); 776 } 777 break; 778 case SchemaBuildState.State.done: 779 ctx.state.get.schemaBuild.files.clear; 780 return GenSchemaResult(true); 781 } 782 783 if (!result.injectIds.empty) 784 return result; 785 } 786 787 return GenSchemaResult(true); 788 } 789 790 self.name = "generateSchema"; 791 792 try { 793 send(self, Init.init, dbPath, conf); 794 } catch (Exception e) { 795 logger.error(e.msg).collectException; 796 self.shutdown; 797 } 798 799 return impl(self, &init_, st, &genSchema, st, &updateWhiteList, st); 800 } 801 802 private { 803 struct FullSchemaGenDoneMsg { 804 } 805 806 struct MutantsToTestMsg { 807 } 808 809 struct SchemaGenStatusMsg { 810 } 811 812 struct GetSchemaSizeMsg { 813 } 814 815 struct SaveSizeQMsg { 816 } 817 } 818 819 // dfmt off 820 alias SchemaSizeQUpdateActor = typedActor!( 821 // Signal that no more full scheman are generated. 822 void function(FullSchemaGenDoneMsg), 823 // mutants to test when the scheman where generated 824 void function(MutantsToTestMsg, long number), 825 // if the generation where successfull 826 void function(SchemaGenStatusMsg, SchemaStatus, long mutantsInSchema), 827 /// The currently state of the size to use for scheman. 828 long function(GetSchemaSizeMsg), 829 ); 830 // dfmt on 831 832 private auto spawnSchemaSizeQ(SchemaSizeQUpdateActor.Impl self, 833 SchemaSizeQ sizeQ, DbSaveActor.Address dbSave) @trusted { 834 static struct State { 835 DbSaveActor.Address dbSave; 836 // state of the sizeq algorithm. 837 SchemaSizeQ sizeQ; 838 // number of scheman that has been generated. 839 long genCount; 840 } 841 842 auto st = tuple!("self", "state")(self, refCounted(State(dbSave, sizeQ))); 843 alias Ctx = typeof(st); 844 845 static void updateMutantsNumber(ref Ctx ctx, MutantsToTestMsg _, long number) @safe nothrow { 846 ctx.state.get.sizeQ.testMutantsSize = number; 847 } 848 849 static void genStatus(ref Ctx ctx, SchemaGenStatusMsg, 850 SchemaStatus status, long mutantsInSchema) @safe nothrow { 851 ctx.state.get.genCount++; 852 try { 853 ctx.state.get.sizeQ.update(status, mutantsInSchema); 854 send(ctx.state.get.dbSave, ctx.state.get.sizeQ); 855 logger.trace(ctx.state.get.sizeQ); 856 } catch (Exception e) { 857 logger.info(e.msg).collectException; 858 } 859 } 860 861 static void fullGenDone(ref Ctx ctx, FullSchemaGenDoneMsg _) @safe nothrow { 862 if (ctx.state.get.genCount == 0) { 863 try { 864 ctx.state.get.sizeQ.noCurrentSize; 865 send(ctx.state.get.dbSave, ctx.state.get.sizeQ); 866 logger.trace(ctx.state.get.sizeQ); 867 } catch (Exception e) { 868 logger.info(e.msg).collectException; 869 } 870 } 871 } 872 873 static long getSize(ref Ctx ctx, GetSchemaSizeMsg _) @safe nothrow { 874 return ctx.state.get.sizeQ.currentSize; 875 } 876 877 self.name = "schemaSizeQUpdater"; 878 879 return impl(self, &updateMutantsNumber, st, &getSize, st, &genStatus, 880 st, &fullGenDone, st); 881 } 882 883 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit). 884 * 885 * There is a possibility that an injection ID result in a collision because 886 * they are only 32 bit. If that happens the mutant is discarded as unfeasable 887 * to use for schemata. 888 * 889 * TODO: if this is changed to being order dependent then it can handle all 890 * mutants. But I can't see how that can be done easily both because of how the 891 * schemas are generated and how the database is setup. 892 */ 893 struct InjectIdBuilder { 894 private { 895 alias InjectId = InjectIdResult.InjectId; 896 897 InjectId[uint] result; 898 Set!uint collisions; 899 } 900 901 void put(MutationStatusId id, Checksum cs) @safe pure nothrow { 902 import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId; 903 904 const injectId = checksumToId(cs); 905 debug logger.tracef("%s %s %s", id, cs, injectId).collectException; 906 907 if (injectId in collisions) { 908 } else if (injectId in result) { 909 collisions.add(injectId); 910 result.remove(injectId); 911 } else { 912 result[injectId] = InjectId(id, injectId); 913 } 914 } 915 916 InjectIdResult finalize() @safe nothrow { 917 import std.array : array; 918 import std.random : randomCover; 919 920 return InjectIdResult(result.byValue.array.randomCover.array); 921 } 922 } 923 924 struct InjectIdResult { 925 struct InjectId { 926 MutationStatusId statusId; 927 uint injectId; 928 } 929 930 InjectId[] ids; 931 932 InjectId front() @safe pure nothrow { 933 assert(!empty, "Can't get front of an empty range"); 934 return ids[0]; 935 } 936 937 void popFront() @safe pure nothrow { 938 assert(!empty, "Can't pop front of an empty range"); 939 ids = ids[1 .. $]; 940 } 941 942 bool empty() @safe pure nothrow const @nogc { 943 return ids.empty; 944 } 945 946 size_t length() @safe pure nothrow const @nogc scope { 947 return ids.length; 948 } 949 } 950 951 /// Extract the mutants that are part of the schema. 952 InjectIdResult mutantsFromSchema(ref SchemataBuilder.ET schema, ref Set!MutationStatusId whiteList) { 953 import dextool.plugin.mutate.backend.database.type : toMutationStatusId; 954 955 InjectIdBuilder builder; 956 foreach (mutant; schema.mutants.filter!(a => a.id.toMutationStatusId in whiteList)) { 957 builder.put(mutant.id.toMutationStatusId, mutant.id); 958 } 959 960 return builder.finalize; 961 } 962 963 @("shall detect a collision and make sure it is never part of the result") 964 unittest { 965 InjectIdBuilder builder; 966 builder.put(MutationStatusId(1), Checksum(1)); 967 builder.put(MutationStatusId(2), Checksum(2)); 968 builder.put(MutationStatusId(3), Checksum(1)); 969 auto r = builder.finalize; 970 971 assert(r.front.statusId == MutationStatusId(2)); 972 r.popFront; 973 assert(r.empty); 974 } 975 976 Edit[] makeRootImpl(ulong end) { 977 import dextool.plugin.mutate.backend.resource : schemataImpl; 978 979 return [ 980 makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl) 981 ]; 982 } 983 984 Edit[] makeHdr() { 985 import dextool.plugin.mutate.backend.resource : schemataHeader; 986 987 return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)]; 988 } 989 990 /** Injects the schema and runtime. 991 * 992 * Uses exceptions to signal failure. 993 */ 994 struct CodeInject { 995 FilesysIO fio; 996 997 Set!AbsolutePath roots; 998 999 /// Unique checksum for the schema. 1000 Checksum checksum; 1001 1002 bool logSchema; 1003 1004 this(FilesysIO fio, ConfigSchema conf) { 1005 this.fio = fio; 1006 this.logSchema = conf.log; 1007 1008 foreach (a; conf.userRuntimeCtrl) { 1009 auto p = fio.toAbsoluteRoot(a.file); 1010 roots.add(p); 1011 } 1012 } 1013 1014 /// Throws an error on failure. 1015 /// Returns: modified files. 1016 AbsolutePath[] inject(ref Database db, SchemataBuilder.ET schemata) { 1017 checksum = schemata.checksum.value; 1018 auto modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file)) 1019 .toSet.toRange.array; 1020 1021 void initRoots(ref Database db) { 1022 if (roots.empty) { 1023 auto allRoots = () { 1024 AbsolutePath[] tmp; 1025 try { 1026 tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get) 1027 .map!(a => fio.toAbsoluteRoot(a)) 1028 .array; 1029 if (tmp.empty) { 1030 // no root found. Inject the runtime in all files and "hope for 1031 // the best". it will be less efficient but the weak symbol 1032 // should still mean that it link correctly. 1033 tmp = modifiedFiles; 1034 } 1035 } catch (Exception e) { 1036 logger.error(e.msg).collectException; 1037 } 1038 return tmp; 1039 }(); 1040 1041 foreach (r; allRoots) { 1042 roots.add(r); 1043 } 1044 } 1045 1046 auto mods = modifiedFiles.toSet; 1047 foreach (r; roots.toRange) { 1048 if (r !in mods) 1049 modifiedFiles ~= r; 1050 } 1051 1052 if (roots.empty) 1053 throw new Exception("No root file found to inject the schemata runtime in"); 1054 } 1055 1056 void injectCode() { 1057 import std.path : extension, stripExtension; 1058 1059 alias SchemataFragment = SchemataBuilder.SchemataFragment; 1060 1061 Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) { 1062 auto edits = appender!(Edit[])(); 1063 edits.put(extra); 1064 foreach (a; fragments) { 1065 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text); 1066 } 1067 auto m = merge(original, edits.data); 1068 return change(new Blob(original.uri, original.content), m.edits); 1069 } 1070 1071 SchemataFragment[] fragments(Path p) { 1072 return schemata.fragments.filter!(a => a.file == p).array; 1073 } 1074 1075 foreach (fname; modifiedFiles) { 1076 auto f = fio.makeInput(fname); 1077 auto extra = () { 1078 if (fname in roots) { 1079 logger.trace("Injecting schemata runtime in ", fname); 1080 return makeRootImpl(f.content.length); 1081 } 1082 return makeHdr; 1083 }(); 1084 1085 logger.info("Injecting schema in ", fname); 1086 1087 // writing the schemata. 1088 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra); 1089 fio.makeOutput(fname).write(s); 1090 1091 if (logSchema) { 1092 const ext = fname.toString.extension; 1093 fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension, 1094 checksum.c0, ext).Path)).write(s); 1095 } 1096 } 1097 } 1098 1099 initRoots(db); 1100 injectCode; 1101 1102 return modifiedFiles; 1103 } 1104 1105 void compile(ShellCommand buildCmd, Duration buildCmdTimeout) { 1106 import dextool.plugin.mutate.backend.test_mutant.common : compile; 1107 1108 logger.infof("Compile schema %s", checksum.c0).collectException; 1109 1110 compile(buildCmd, buildCmdTimeout, PrintCompileOnFailure(true)).match!((Mutation.Status a) { 1111 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow) 1112 .toString); 1113 }, (bool success) { 1114 if (!success) { 1115 throw new Exception("Skipping schema because it failed to compile".color(Color.yellow) 1116 .toString); 1117 } 1118 }); 1119 1120 logger.info("Ok".color(Color.green)).collectException; 1121 } 1122 } 1123 1124 // Check that the test suite successfully execute "passed". 1125 // Returns: true on success. 1126 Tuple!(bool, "isOk", Duration, "runtime") sanityCheck(ref TestRunner runner) { 1127 auto sw = StopWatch(AutoStart.yes); 1128 auto res = runner.run; 1129 return typeof(return)(res.status == TestResult.Status.passed, sw.peek); 1130 } 1131 1132 /// Round robin scheduling of mutants for testing from the worker pool. 1133 struct ScheduleTest { 1134 TestMutantActor.Address[] testers; 1135 Vector!size_t free; 1136 1137 this(TestMutantActor.Address[] testers) { 1138 this.testers = testers; 1139 foreach (size_t i; 0 .. testers.length) 1140 free.put(i); 1141 } 1142 1143 /// Returns: if the tester is full, no worker used. 1144 bool full() @safe pure nothrow const @nogc { 1145 return testers.length == free.length; 1146 } 1147 1148 bool empty() @safe pure nothrow const @nogc { 1149 return free.empty; 1150 } 1151 1152 size_t pop() 1153 in (free.length <= testers.length) { 1154 scope (exit) 1155 free.popFront(); 1156 return free.front; 1157 } 1158 1159 void put(size_t x) 1160 in (x < testers.length) 1161 out (; free.length <= testers.length) 1162 do { 1163 free.put(x); 1164 } 1165 1166 TestMutantActor.Address get(size_t x) 1167 in (free.length <= testers.length) 1168 in (x < testers.length) { 1169 return testers[x]; 1170 } 1171 } 1172 1173 struct SchemaTestResult { 1174 MutationTestResult result; 1175 Duration testTime; 1176 TestCase[] unstable; 1177 } 1178 1179 alias TestMutantActor = typedActor!( 1180 SchemaTestResult function(InjectIdResult.InjectId id), void function(TimeoutConfig)); 1181 1182 auto spawnTestMutant(TestMutantActor.Impl self, TestRunner runner, TestCaseAnalyzer analyzer) { 1183 static struct State { 1184 TestRunner runner; 1185 TestCaseAnalyzer analyzer; 1186 } 1187 1188 auto st = tuple!("self", "state")(self, refCounted(State(runner, analyzer))); 1189 alias Ctx = typeof(st); 1190 1191 static SchemaTestResult run(ref Ctx ctx, InjectIdResult.InjectId id) @safe nothrow { 1192 import std.datetime.stopwatch : StopWatch, AutoStart; 1193 import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey; 1194 1195 SchemaTestResult analyzeForTestCase(SchemaTestResult rval, 1196 ref DrainElement[][ShellCommand] output) @safe nothrow { 1197 foreach (testCmd; output.byKeyValue) { 1198 try { 1199 auto analyze = ctx.state.get.analyzer.analyze(testCmd.key, testCmd.value); 1200 1201 analyze.match!((TestCaseAnalyzer.Success a) { 1202 rval.result.testCases ~= a.failed ~ a.testCmd; 1203 }, (TestCaseAnalyzer.Unstable a) { 1204 rval.unstable ~= a.unstable; 1205 // must re-test the mutant 1206 rval.result.status = Mutation.Status.unknown; 1207 }, (TestCaseAnalyzer.Failed a) { 1208 logger.tracef("The parsers that analyze the output from %s failed", 1209 testCmd.key); 1210 }); 1211 } catch (Exception e) { 1212 logger.warning(e.msg).collectException; 1213 } 1214 } 1215 return rval; 1216 } 1217 1218 auto sw = StopWatch(AutoStart.yes); 1219 1220 SchemaTestResult rval; 1221 1222 rval.result.id = id.statusId; 1223 1224 auto env = ctx.state.get.runner.getDefaultEnv; 1225 env[schemataMutantEnvKey] = id.injectId.to!string; 1226 1227 auto res = runTester(ctx.state.get.runner, env); 1228 rval.result.status = res.status; 1229 rval.result.exitStatus = res.exitStatus; 1230 rval.result.testCmds = res.output.byKey.array; 1231 1232 if (!ctx.state.get.analyzer.empty) 1233 rval = analyzeForTestCase(rval, res.output); 1234 1235 rval.testTime = sw.peek; 1236 return rval; 1237 } 1238 1239 static void doConf(ref Ctx ctx, TimeoutConfig conf) @safe nothrow { 1240 ctx.state.get.runner.timeout = conf.value; 1241 } 1242 1243 self.name = "testMutant"; 1244 return impl(self, &run, st, &doConf, st); 1245 } 1246 1247 // private: 1248 1249 import std.algorithm : sum; 1250 import std.format : formattedWrite, format; 1251 1252 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 1253 import dextool.plugin.mutate.backend.type : Language, SourceLoc, Offset, 1254 SourceLocRange, CodeMutant, SchemataChecksum; 1255 import dextool.plugin.mutate.backend.analyze.utility; 1256 1257 /// Language generic schemata result. 1258 class SchemataResult { 1259 static struct Fragment { 1260 Offset offset; 1261 const(ubyte)[] text; 1262 CodeMutant[] mutants; 1263 } 1264 1265 static struct Fragments { 1266 // TODO: change to using appender 1267 Fragment[] fragments; 1268 } 1269 1270 private { 1271 Fragments[AbsolutePath] fragments; 1272 } 1273 1274 /// Returns: all fragments containing mutants per file. 1275 Fragments[AbsolutePath] getFragments() @safe { 1276 return fragments; 1277 } 1278 1279 /// Assuming that all fragments for a file should be merged to one huge. 1280 private void putFragment(AbsolutePath file, Fragment sf) { 1281 fragments.update(file, () => Fragments([sf]), (ref Fragments a) { 1282 a.fragments ~= sf; 1283 }); 1284 } 1285 1286 override string toString() @safe { 1287 import std.range : put; 1288 import std.utf : byUTF; 1289 1290 auto w = appender!string(); 1291 1292 void toBuf(Fragments s) { 1293 foreach (f; s.fragments) { 1294 formattedWrite(w, " %s: %s\n", f.offset, 1295 (cast(const(char)[]) f.text).byUTF!(const(char))); 1296 formattedWrite(w, "%( %s\n%)\n", f.mutants); 1297 } 1298 } 1299 1300 foreach (k; fragments.byKey.array.sort) { 1301 try { 1302 formattedWrite(w, "%s:\n", k); 1303 toBuf(fragments[k]); 1304 } catch (Exception e) { 1305 } 1306 } 1307 1308 return w.data; 1309 } 1310 } 1311 1312 /** Build scheman from the fragments. 1313 * 1314 * TODO: optimize the implementation. A lot of redundant memory allocations 1315 * etc. 1316 * 1317 * Conservative to only allow up to <user defined> mutants per schemata but it 1318 * reduces the chance that one failing schemata is "fatal", loosing too many 1319 * muntats. 1320 */ 1321 struct SchemataBuilder { 1322 import std.algorithm : any, all; 1323 import my.container.vector; 1324 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ; 1325 import dextool.plugin.mutate.backend.database.type : SchemaFragmentV2; 1326 1327 static struct SchemataFragment { 1328 Path file; 1329 Offset offset; 1330 const(ubyte)[] text; 1331 } 1332 1333 static struct Fragment { 1334 SchemataFragment fragment; 1335 CodeMutant[] mutants; 1336 } 1337 1338 static struct ET { 1339 SchemataFragment[] fragments; 1340 CodeMutant[] mutants; 1341 SchemataChecksum checksum; 1342 } 1343 1344 // TODO: remove SchemataChecksum? 1345 1346 /// Controls the probability that a mutant is part of the currently generating schema. 1347 SchemaQ schemaQ; 1348 1349 /// use probability for if a mutant is injected or not 1350 bool useProbability; 1351 1352 /// if the probability should also influence if the scheam is smaller. 1353 bool useProbablitySmallSize; 1354 1355 // if fragments that are part of scheman that didn't reach the min 1356 // threshold should be discarded. 1357 bool discardMinScheman; 1358 1359 /// The threshold start at this value. 1360 double thresholdStartValue = 0.0; 1361 1362 /// Max mutants per schema. 1363 long mutantsPerSchema = 1000; 1364 1365 /// Minimal mutants that a schema must contain for it to be valid. 1366 long minMutantsPerSchema = 3; 1367 1368 Vector!Fragment current; 1369 Vector!Fragment rest; 1370 1371 /// Size in bytes of the cache of fragments. 1372 size_t cacheSize; 1373 1374 /** Merge analyze fragments into larger schemata fragments. If a schemata 1375 * fragment is large enough it is converted to a schemata. Otherwise kept 1376 * for pass2. 1377 * 1378 * Schematan from this pass only contain one kind and only affect one file. 1379 */ 1380 void put(Fragment[] fragments) { 1381 foreach (a; fragments) { 1382 current.put(a); 1383 incrCache(a.fragment); 1384 } 1385 } 1386 1387 private void incrCache(ref SchemataFragment a) @safe pure nothrow @nogc { 1388 cacheSize += a.text.length + (cast(const(ubyte)[]) a.file.toString).length + typeof(a) 1389 .sizeof; 1390 } 1391 1392 bool empty() @safe pure nothrow const @nogc { 1393 return current.length == 0 && rest.length == 0; 1394 } 1395 1396 auto stats() @safe pure nothrow const { 1397 static struct Stats { 1398 double cacheSizeMb; 1399 size_t current; 1400 size_t rest; 1401 } 1402 1403 return Stats(cast(double) cacheSize / (1024 * 1024), current.length, rest.length); 1404 } 1405 1406 /** Merge schemata fragments to schemas. A schemata from this pass may may 1407 * contain multiple mutation kinds and span over multiple files. 1408 */ 1409 Optional!ET next() { 1410 import std.algorithm : max; 1411 1412 Index!Path index; 1413 auto app = appender!(Fragment[])(); 1414 Set!CodeMutant local; 1415 auto threshold = () => max(thresholdStartValue, 1416 cast(double) local.length / cast(double) mutantsPerSchema); 1417 1418 while (!current.empty) { 1419 if (local.length >= mutantsPerSchema) { 1420 // done now so woop 1421 break; 1422 } 1423 1424 auto a = current.front; 1425 current.popFront; 1426 1427 if (a.mutants.empty) 1428 continue; 1429 1430 if (index.intersect(a.fragment.file, a.fragment.offset)) { 1431 rest.put(a); 1432 continue; 1433 } 1434 1435 // if any of the mutants in the schema has already been included. 1436 if (any!(a => a in local)(a.mutants)) { 1437 rest.put(a); 1438 continue; 1439 } 1440 1441 // if any of the mutants fail the probability to be included 1442 if (useProbability && any!(b => !schemaQ.use(a.fragment.file, 1443 b.mut.kind, threshold()))(a.mutants)) { 1444 // TODO: remove this line of code in the future. used for now, 1445 // ugly, to see that it behavies as expected. 1446 //log.tracef("probability postpone fragment with mutants %s %s", 1447 // a.mutants.length, a.mutants.map!(a => a.mut.kind)); 1448 rest.put(a); 1449 continue; 1450 } 1451 1452 // no use in using a mutant that has zero probability because then, it will always fail. 1453 if (any!(b => schemaQ.isZero(a.fragment.file, b.mut.kind))(a.mutants)) { 1454 continue; 1455 } 1456 1457 app.put(a); 1458 local.add(a.mutants); 1459 index.put(a.fragment.file, a.fragment.offset); 1460 1461 if (useProbablitySmallSize && local.length > minMutantsPerSchema 1462 && any!(b => !schemaQ.use(a.fragment.file, b.mut.kind, threshold()))(a.mutants)) { 1463 break; 1464 } 1465 } 1466 1467 if (local.length == 0 || local.length < minMutantsPerSchema) { 1468 if (discardMinScheman) { 1469 logger.tracef("discarding %s fragments with %s mutants", 1470 app.data.length, app.data.map!(a => a.mutants.length).sum); 1471 } else { 1472 rest.put(app.data); 1473 } 1474 return none!ET; 1475 } 1476 1477 ET v; 1478 v.fragments = app.data.map!(a => a.fragment).array; 1479 v.mutants = local.toArray; 1480 v.checksum = toSchemataChecksum(v.mutants); 1481 1482 return some(v); 1483 } 1484 1485 bool isDone() @safe pure nothrow const @nogc { 1486 return current.empty; 1487 } 1488 1489 void restart() @safe pure nothrow @nogc { 1490 current = rest; 1491 rest.clear; 1492 1493 cacheSize = 0; 1494 foreach (a; current[]) 1495 incrCache(a.fragment); 1496 } 1497 } 1498 1499 /** A schema is uniquely identified by the mutants it contains. 1500 * 1501 * The order of the mutants are irrelevant because they are always sorted by 1502 * their value before the checksum is calculated. 1503 */ 1504 SchemataChecksum toSchemataChecksum(CodeMutant[] mutants) { 1505 import dextool.plugin.mutate.backend.utility : BuildChecksum, toChecksum, toBytes; 1506 1507 BuildChecksum h; 1508 foreach (a; mutants.sort!((a, b) => a.id.value < b.id.value) 1509 .map!(a => a.id.value)) { 1510 h.put(a.c0.toBytes); 1511 } 1512 1513 return SchemataChecksum(toChecksum(h)); 1514 } 1515 1516 /** The total state for building schemas in runtime. 1517 * 1518 * The intention isn't to perfectly travers and handle all mutants in the 1519 * worklist if the worklist is manipulated while the schema generation is 1520 * running. It is just "good enough" to generate schemas for those mutants when 1521 * it was started. 1522 */ 1523 struct SchemaBuildState { 1524 import sumtype; 1525 import my.optional; 1526 import dextool.plugin.mutate.backend.database.type : FileId, SchemaFragmentV2; 1527 1528 enum State : ubyte { 1529 none, 1530 processFiles, 1531 prepareReduction, 1532 reduction, 1533 prepareFinalize, 1534 finalize1, 1535 finalize2, 1536 done, 1537 } 1538 1539 static struct ProcessFiles { 1540 FileId[] files; 1541 size_t idx; 1542 1543 FileId pop() @safe pure nothrow scope { 1544 if (idx == files.length) 1545 return FileId.init; 1546 return files[idx++]; 1547 } 1548 1549 bool isDone() @safe pure nothrow const @nogc scope { 1550 return idx == files.length; 1551 } 1552 1553 size_t filesLeft() @safe pure nothrow const @nogc scope { 1554 return files.length - idx; 1555 } 1556 1557 void reset() @safe pure nothrow @nogc scope { 1558 idx = 0; 1559 } 1560 1561 void clear() @safe pure nothrow @nogc scope { 1562 files = null; 1563 reset; 1564 } 1565 } 1566 1567 // State of the schema building 1568 State st; 1569 private int reducedTicks; 1570 1571 // Files to use when generating schemas. 1572 ProcessFiles files; 1573 1574 SchemataBuilder builder; 1575 1576 // User configuration. 1577 typeof(ConfigSchema.minMutantsPerSchema) minMutantsPerSchema = 3; 1578 typeof(ConfigSchema.mutantsPerSchema) mutantsPerSchema = 1000; 1579 1580 void initFiles(FileId[] files) @safe nothrow { 1581 import std.random : randomCover; 1582 1583 try { 1584 // improve the schemas non-determinism between each `test` run. 1585 this.files.files = files.randomCover.array; 1586 } catch (Exception e) { 1587 this.files.files = files; 1588 } 1589 } 1590 1591 /// Step through the schema building. 1592 void tick() @safe nothrow { 1593 logger.tracef("state_pre: %s %s", st, builder.stats).collectException; 1594 final switch (st) { 1595 case State.none: 1596 st = State.processFiles; 1597 try { 1598 setIntermediate; 1599 } catch (Exception e) { 1600 st = State.done; 1601 } 1602 break; 1603 case State.processFiles: 1604 if (files.isDone) 1605 st = State.prepareReduction; 1606 try { 1607 setIntermediate; 1608 } catch (Exception e) { 1609 st = State.done; 1610 } 1611 break; 1612 case State.prepareReduction: 1613 st = State.reduction; 1614 break; 1615 case State.reduction: 1616 immutable magic = 10; // reduce the size until it is 1/10 of the original 1617 immutable magic2 = 5; // if it goes <95% then it is too high probability to fail 1618 1619 if (builder.empty) 1620 st = State.prepareFinalize; 1621 else if (++reducedTicks > (magic * magic2)) 1622 st = State.prepareFinalize; 1623 1624 try { 1625 setReducedIntermediate(1 + reducedTicks / magic, reducedTicks % magic2); 1626 } catch (Exception e) { 1627 st = State.done; 1628 } 1629 break; 1630 case State.prepareFinalize: 1631 st = State.finalize1; 1632 break; 1633 case State.finalize1: 1634 st = State.finalize2; 1635 try { 1636 finalize; 1637 } catch (Exception e) { 1638 st = State.done; 1639 } 1640 break; 1641 case State.finalize2: 1642 if (builder.isDone) 1643 st = State.done; 1644 break; 1645 case State.done: 1646 break; 1647 } 1648 logger.trace("state_post: ", st).collectException; 1649 } 1650 1651 /// Add all fragments from one of the files to process to those to be 1652 /// incorporated into future schemas. 1653 /// Returns: number of fragments added. 1654 size_t updateFiles(ref Set!MutationStatusId whiteList, scope SchemaFragmentV2[]delegate( 1655 FileId) @safe fragmentsFn, scope Nullable!Path delegate(FileId) @safe fnameFn, 1656 scope Mutation.Kind delegate(MutationStatusId) @safe kindFn) @safe nothrow { 1657 import dextool.plugin.mutate.backend.type : CodeChecksum, Mutation; 1658 import dextool.plugin.mutate.backend.database : toChecksum; 1659 1660 if (files.isDone) 1661 return 0; 1662 auto id = files.pop; 1663 try { 1664 const fname = fnameFn(id); 1665 if (fname.isNull) 1666 return 0; 1667 1668 auto app = appender!(SchemataBuilder.Fragment[])(); 1669 auto frags = fragmentsFn(id); 1670 foreach (a; frags) { 1671 auto cm = a.mutants 1672 .filter!(a => a in whiteList) 1673 .map!(a => CodeMutant(CodeChecksum(a.toChecksum), 1674 Mutation(kindFn(a), Mutation.Status.unknown))) 1675 .array; 1676 if (!cm.empty) { 1677 app.put(SchemataBuilder.Fragment(SchemataBuilder.SchemataFragment(fname.get, 1678 a.offset, a.text), cm)); 1679 } 1680 } 1681 1682 builder.put(app.data); 1683 return app.data.length; 1684 } catch (Exception e) { 1685 logger.trace(e.msg).collectException; 1686 } 1687 return 0; 1688 } 1689 1690 Optional!(SchemataBuilder.ET) process() { 1691 auto rval = builder.next; 1692 builder.restart; 1693 return rval; 1694 } 1695 1696 void setMinMutants(long desiredValue) { 1697 // seems like 200 Mbyte is large enough to generate scheman with >1000 1698 // mutants easily when running on LLVM. 1699 enum MaxCache = 200 * 1024 * 1024; 1700 if (builder.cacheSize > MaxCache) { 1701 // panic mode, just empty it as fast as possible. 1702 logger.infof( 1703 "Schema cache is %s bytes (limit %s). Producing as many schemas as possible to flush the cache.", 1704 builder.cacheSize, MaxCache); 1705 builder.minMutantsPerSchema = minMutantsPerSchema.get; 1706 } else { 1707 builder.minMutantsPerSchema = desiredValue; 1708 } 1709 } 1710 1711 void setIntermediate() { 1712 logger.trace("schema generator phase: intermediate"); 1713 builder.discardMinScheman = false; 1714 builder.useProbability = true; 1715 builder.useProbablitySmallSize = false; 1716 builder.mutantsPerSchema = mutantsPerSchema.get; 1717 builder.thresholdStartValue = 1.0; 1718 1719 setMinMutants(mutantsPerSchema.get); 1720 } 1721 1722 void setReducedIntermediate(long sizeDiv, long threshold) { 1723 import std.algorithm : max; 1724 1725 logger.tracef("schema generator phase: reduced size:%s threshold:%s", sizeDiv, threshold); 1726 builder.discardMinScheman = false; 1727 builder.useProbability = true; 1728 builder.useProbablitySmallSize = false; 1729 builder.mutantsPerSchema = mutantsPerSchema.get; 1730 // TODO: interresting effect. this need to be studied. I think this 1731 // is the behavior that is "best". 1732 builder.thresholdStartValue = 1.0 - (cast(double) threshold / 100.0); 1733 1734 setMinMutants(max(minMutantsPerSchema.get, mutantsPerSchema.get / sizeDiv)); 1735 } 1736 1737 /// Consume all fragments or discard. 1738 void finalize() { 1739 logger.trace("schema generator phase: finalize"); 1740 builder.discardMinScheman = true; 1741 builder.useProbability = false; 1742 builder.useProbablitySmallSize = true; 1743 builder.mutantsPerSchema = mutantsPerSchema.get; 1744 builder.minMutantsPerSchema = minMutantsPerSchema.get; 1745 builder.thresholdStartValue = 0; 1746 } 1747 }