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; 17 import std.datetime.stopwatch : StopWatch, AutoStart; 18 import std.exception : collectException; 19 import std.format : format; 20 import std.typecons : Tuple; 21 22 import proc : DrainElement; 23 import sumtype; 24 import blob_model; 25 26 import my.fsm : Fsm, next, act, get, TypeDataMap; 27 import my.path; 28 import my.set; 29 static import my.fsm; 30 31 import dextool.plugin.mutate.backend.database : MutationStatusId, Database, 32 spinSql, SchemataId, Schemata; 33 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 34 import dextool.plugin.mutate.backend.test_mutant.common; 35 import dextool.plugin.mutate.backend.test_mutant.test_cmd_runner : TestRunner, TestResult; 36 import dextool.plugin.mutate.backend.type : Mutation, TestCase, Checksum; 37 import dextool.plugin.mutate.type : TestCaseAnalyzeBuiltin, ShellCommand, 38 UserRuntime, SchemaRuntime; 39 import dextool.plugin.mutate.config : ConfigSchema; 40 41 @safe: 42 43 struct SchemataTestDriver { 44 private { 45 /// True as long as the schemata driver is running. 46 bool isRunning_ = true; 47 bool hasFatalError_; 48 bool isInvalidSchema_; 49 50 FilesysIO fio; 51 52 Database* db; 53 54 /// Runs the test commands. 55 TestRunner* runner; 56 57 Mutation.Kind[] kinds; 58 59 SchemataId schemataId; 60 61 /// Result of testing the mutants. 62 MutationTestResult[] result_; 63 64 /// Time it took to compile the schemata. 65 Duration compileTime; 66 StopWatch swCompile; 67 68 // Write the instrumented source code to .cov.<ext> for separate 69 // inspection. 70 const bool log; 71 72 ShellCommand buildCmd; 73 Duration buildCmdTimeout; 74 75 /// The full schemata that is used.. 76 Schemata schemata; 77 78 AbsolutePath[] modifiedFiles; 79 80 Set!AbsolutePath roots; 81 82 TestStopCheck stopCheck; 83 84 SchemaRuntime runtime; 85 } 86 87 static struct None { 88 } 89 90 static struct Initialize { 91 bool error; 92 } 93 94 static struct InitializeRoots { 95 bool hasRoot; 96 } 97 98 static struct InjectSchema { 99 bool error; 100 } 101 102 static struct Compile { 103 bool error; 104 } 105 106 static struct Done { 107 } 108 109 static struct Restore { 110 bool error; 111 } 112 113 static struct NextMutantData { 114 /// Mutants to test. 115 InjectIdResult mutants; 116 } 117 118 static struct NextMutant { 119 bool done; 120 InjectIdResult.InjectId inject; 121 } 122 123 static struct TestMutantData { 124 /// If the user has configured that the test cases should be analyzed. 125 bool hasTestCaseOutputAnalyzer; 126 } 127 128 static struct TestMutant { 129 InjectIdResult.InjectId inject; 130 131 MutationTestResult result; 132 bool hasTestOutput; 133 // if there are mutants status id's related to a file but the mutants 134 // have been removed. 135 bool mutantIdError; 136 } 137 138 static struct TestCaseAnalyzeData { 139 TestCaseAnalyzer* testCaseAnalyzer; 140 DrainElement[][ShellCommand] output; 141 } 142 143 static struct TestCaseAnalyze { 144 MutationTestResult result; 145 bool unstableTests; 146 } 147 148 static struct StoreResult { 149 MutationTestResult result; 150 } 151 152 static struct OverloadCheck { 153 bool halt; 154 bool sleep; 155 } 156 157 alias Fsm = my.fsm.Fsm!(None, Initialize, InitializeRoots, Done, NextMutant, TestMutant, 158 TestCaseAnalyze, StoreResult, InjectSchema, Compile, Restore, OverloadCheck); 159 alias LocalStateDataT = Tuple!(TestMutantData, TestCaseAnalyzeData, NextMutantData); 160 161 private { 162 Fsm fsm; 163 TypeDataMap!(LocalStateDataT, TestMutant, TestCaseAnalyze, NextMutant) local; 164 } 165 166 this(FilesysIO fio, TestRunner* runner, Database* db, TestCaseAnalyzer* testCaseAnalyzer, 167 ConfigSchema conf, SchemataId id, TestStopCheck stopCheck, 168 Mutation.Kind[] kinds, ShellCommand buildCmd, Duration buildCmdTimeout) { 169 this.fio = fio; 170 this.runner = runner; 171 this.db = db; 172 this.schemataId = id; 173 this.stopCheck = stopCheck; 174 this.kinds = kinds; 175 this.buildCmd = buildCmd; 176 this.buildCmdTimeout = buildCmdTimeout; 177 this.log = conf.log; 178 this.runtime = conf.runtime; 179 180 this.local.get!TestCaseAnalyze.testCaseAnalyzer = testCaseAnalyzer; 181 this.local.get!TestMutant.hasTestCaseOutputAnalyzer = !testCaseAnalyzer.empty; 182 183 foreach (a; conf.userRuntimeCtrl) { 184 auto p = fio.toAbsoluteRoot(a.file); 185 roots.add(p); 186 } 187 188 if (logger.globalLogLevel.among(logger.LogLevel.trace, logger.LogLevel.all)) 189 fsm.logger = (string s) { logger.trace(s); }; 190 } 191 192 static void execute_(ref SchemataTestDriver self) @trusted { 193 self.fsm.next!((None a) => fsm(Initialize.init), (Initialize a) { 194 if (a.error) 195 return fsm(Done.init); 196 if (self.runtime == SchemaRuntime.inject) 197 return fsm(InitializeRoots.init); 198 return fsm(InjectSchema.init); 199 }, (InitializeRoots a) { 200 if (a.hasRoot) 201 return fsm(InjectSchema.init); 202 return fsm(Done.init); 203 }, (InjectSchema a) { 204 if (a.error) 205 return fsm(Restore.init); 206 return fsm(Compile.init); 207 }, (Compile a) { 208 if (a.error) 209 return fsm(Restore.init); 210 return fsm(OverloadCheck.init); 211 }, (OverloadCheck a) { 212 if (a.halt) 213 return fsm(Restore.init); 214 if (a.sleep) 215 return fsm(OverloadCheck.init); 216 return fsm(NextMutant.init); 217 }, (NextMutant a) { 218 if (a.done) 219 return fsm(Restore.init); 220 return fsm(TestMutant(a.inject)); 221 }, (TestMutant a) { 222 if (a.mutantIdError) 223 return fsm(OverloadCheck.init); 224 if (a.result.status == Mutation.Status.killed 225 && self.local.get!TestMutant.hasTestCaseOutputAnalyzer && a.hasTestOutput) { 226 return fsm(TestCaseAnalyze(a.result)); 227 } 228 return fsm(StoreResult(a.result)); 229 }, (TestCaseAnalyze a) { 230 if (a.unstableTests) 231 return fsm(OverloadCheck.init); 232 return fsm(StoreResult(a.result)); 233 }, (StoreResult a) => fsm(OverloadCheck.init), (Restore a) => Done.init, (Done a) => a); 234 235 self.fsm.act!(self); 236 } 237 238 nothrow: 239 240 MutationTestResult[] popResult() { 241 auto tmp = result_; 242 result_ = null; 243 return tmp; 244 } 245 246 void execute() { 247 try { 248 execute_(this); 249 } catch (Exception e) { 250 logger.warning(e.msg).collectException; 251 } 252 } 253 254 bool hasFatalError() { 255 return hasFatalError_; 256 } 257 258 bool isInvalidSchema() { 259 return isInvalidSchema_; 260 } 261 262 bool isRunning() { 263 return isRunning_; 264 } 265 266 void opCall(None data) { 267 } 268 269 void opCall(ref Initialize data) { 270 swCompile = StopWatch(AutoStart.yes); 271 272 InjectIdBuilder builder; 273 foreach (mutant; spinSql!(() => db.getSchemataMutants(schemataId, kinds))) { 274 auto cs = spinSql!(() { return db.getChecksum(mutant); }); 275 if (!cs.isNull) { 276 builder.put(mutant, cs.get); 277 } 278 } 279 debug logger.trace(builder).collectException; 280 281 local.get!NextMutant.mutants = builder.finalize; 282 283 schemata = spinSql!(() => db.getSchemata(schemataId)).get; 284 285 try { 286 modifiedFiles = schemata.fragments.map!(a => fio.toAbsoluteRoot(a.file)) 287 .toSet.toRange.array; 288 } catch (Exception e) { 289 logger.warning(e.msg).collectException; 290 hasFatalError_ = true; 291 data.error = true; 292 } 293 } 294 295 void opCall(ref InitializeRoots data) { 296 if (roots.empty) { 297 auto allRoots = () { 298 AbsolutePath[] tmp; 299 try { 300 tmp = spinSql!(() => db.getRootFiles).map!(a => db.getFile(a).get) 301 .map!(a => fio.toAbsoluteRoot(a)) 302 .array; 303 if (tmp.empty) { 304 // no root found. Inject the runtime in all files and "hope for 305 // the best". it will be less efficient but the weak symbol 306 // should still mean that it link correctly. 307 tmp = modifiedFiles; 308 } 309 } catch (Exception e) { 310 logger.error(e.msg).collectException; 311 } 312 return tmp; 313 }(); 314 315 foreach (r; allRoots) { 316 roots.add(r); 317 } 318 } 319 320 auto mods = modifiedFiles.toSet; 321 foreach (r; roots.toRange) { 322 if (r !in mods) 323 modifiedFiles ~= r; 324 } 325 326 data.hasRoot = !roots.empty; 327 328 if (roots.empty) { 329 logger.warning("No root file found to inject the schemata runtime in").collectException; 330 } 331 } 332 333 void opCall(Done data) { 334 isRunning_ = false; 335 } 336 337 void opCall(ref InjectSchema data) { 338 import std.path : extension, stripExtension; 339 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 340 341 scope (exit) 342 schemata = Schemata.init; // release the memory back to the GC 343 344 Blob makeSchemata(Blob original, SchemataFragment[] fragments, Edit[] extra) { 345 auto edits = appender!(Edit[])(); 346 edits.put(extra); 347 foreach (a; fragments) { 348 edits ~= new Edit(Interval(a.offset.begin, a.offset.end), a.text); 349 } 350 auto m = merge(original, edits.data); 351 return change(new Blob(original.uri, original.content), m.edits); 352 } 353 354 SchemataFragment[] fragments(Path p) { 355 return schemata.fragments.filter!(a => a.file == p).array; 356 } 357 358 try { 359 foreach (fname; modifiedFiles) { 360 auto f = fio.makeInput(fname); 361 auto extra = () { 362 if (fname in roots) { 363 logger.trace("Injecting schemata runtime in ", fname); 364 return makeRootImpl(f.content.length); 365 } 366 return makeHdr; 367 }(); 368 369 logger.info("Injecting schema in ", fname); 370 371 // writing the schemata. 372 auto s = makeSchemata(f, fragments(fio.toRelativeRoot(fname)), extra); 373 fio.makeOutput(fname).write(s); 374 375 if (log) { 376 const ext = fname.toString.extension; 377 fio.makeOutput(AbsolutePath(format!"%s.%s.schema%s"(fname.toString.stripExtension, 378 schemataId.get, ext).Path)).write(s); 379 380 fio.makeOutput(AbsolutePath(format!"%s.%s.kinds.txt"(fname, 381 schemataId.get).Path)).write(format("%s", kinds)); 382 } 383 } 384 } catch (Exception e) { 385 logger.warning(e.msg).collectException; 386 data.error = true; 387 } 388 } 389 390 void opCall(ref Compile data) { 391 import colorlog; 392 import dextool.plugin.mutate.backend.test_mutant.common : compile; 393 394 logger.infof("Compile schema %s", schemataId.get).collectException; 395 396 compile(buildCmd, buildCmdTimeout, PrintCompileOnFailure(true)).match!((Mutation.Status a) { 397 data.error = true; 398 }, (bool success) { data.error = !success; }); 399 400 if (data.error) { 401 isInvalidSchema_ = true; 402 403 logger.info("Skipping schema because it failed to compile".color(Color.yellow)) 404 .collectException; 405 spinSql!(() { db.markUsed(schemataId); }); 406 return; 407 } 408 409 logger.info("Ok".color(Color.green)).collectException; 410 411 try { 412 logger.info("Sanity check of the generated schemata"); 413 auto res = runner.run; 414 data.error = res.status != TestResult.Status.passed; 415 } catch (Exception e) { 416 logger.warning(e.msg).collectException; 417 } 418 419 if (data.error) { 420 logger.info("Skipping the schemata because the test suite failed".color(Color.yellow)) 421 .collectException; 422 spinSql!(() { db.markUsed(schemataId); }); 423 } else { 424 logger.info("Ok".color(Color.green)).collectException; 425 } 426 427 compileTime = swCompile.peek; 428 } 429 430 void opCall(ref NextMutant data) { 431 data.done = local.get!NextMutant.mutants.empty; 432 433 if (!data.done) { 434 data.inject = local.get!NextMutant.mutants.front; 435 local.get!NextMutant.mutants.popFront; 436 } 437 } 438 439 void opCall(ref TestMutant data) { 440 import std.datetime.stopwatch : StopWatch, AutoStart; 441 import dextool.plugin.mutate.backend.analyze.pass_schemata : schemataMutantEnvKey, 442 checksumToId; 443 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 444 445 auto sw = StopWatch(AutoStart.yes); 446 447 data.result.id = data.inject.statusId; 448 449 auto id = spinSql!(() { return db.getMutationId(data.inject.statusId); }); 450 if (id.isNull) { 451 data.mutantIdError = true; 452 return; 453 } 454 auto entry_ = spinSql!(() { return db.getMutation(id.get); }); 455 if (entry_.isNull) { 456 data.mutantIdError = true; 457 return; 458 } 459 auto entry = entry_.get; 460 461 try { 462 const file = fio.toAbsoluteRoot(entry.file); 463 auto txt = makeMutationText(fio.makeInput(file), entry.mp.offset, 464 entry.mp.mutations[0].kind, entry.lang); 465 debug logger.trace(entry); 466 logger.infof("from '%s' to '%s' in %s:%s:%s", txt.original, 467 txt.mutation, file, entry.sloc.line, entry.sloc.column); 468 } catch (Exception e) { 469 logger.info(e.msg).collectException; 470 } 471 472 auto env = runner.getDefaultEnv; 473 env[schemataMutantEnvKey] = data.inject.injectId.to!string; 474 475 auto res = runTester(*runner, env); 476 data.result.profile = MutantTimeProfile(compileTime, sw.peek); 477 // the first tested mutant also get the compile time of the schema. 478 compileTime = Duration.zero; 479 480 data.result.mutId = id.get; 481 data.result.status = res.status; 482 data.result.exitStatus = res.exitStatus; 483 data.hasTestOutput = !res.output.empty; 484 local.get!TestCaseAnalyze.output = res.output; 485 486 logger.infof("%s:%s (%s)", data.result.status, 487 data.result.exitStatus.get, data.result.profile).collectException; 488 logger.tracef("%s %s injectId:%s", id, data.result.id, 489 data.inject.injectId).collectException; 490 } 491 492 void opCall(ref TestCaseAnalyze data) { 493 scope (exit) 494 local.get!TestCaseAnalyze.output = null; 495 496 foreach (testCmd; local.get!TestCaseAnalyze.output.byKeyValue) { 497 try { 498 auto analyze = local.get!TestCaseAnalyze.testCaseAnalyzer.analyze(testCmd.key, 499 testCmd.value); 500 501 analyze.match!((TestCaseAnalyzer.Success a) { 502 data.result.testCases ~= a.failed ~ a.testCmd; 503 }, (TestCaseAnalyzer.Unstable a) { 504 logger.warningf("Unstable test cases found: [%-(%s, %)]", a.unstable); 505 logger.info( 506 "As configured the result is ignored which will force the mutant to be re-tested"); 507 data.unstableTests = true; 508 }, (TestCaseAnalyzer.Failed a) { 509 logger.warning("The parser that analyze the output from test case(s) failed"); 510 }); 511 } catch (Exception e) { 512 logger.warning(e.msg).collectException; 513 } 514 } 515 516 logger.infof(!data.result.testCases.empty, `killed by [%-(%s, %)]`, 517 data.result.testCases.sort.map!"a.name").collectException; 518 } 519 520 void opCall(StoreResult data) { 521 result_ ~= data.result; 522 } 523 524 void opCall(ref OverloadCheck data) { 525 data.halt = stopCheck.isHalt != TestStopCheck.HaltReason.none; 526 data.sleep = stopCheck.isOverloaded; 527 528 if (data.sleep) { 529 logger.info(stopCheck.overloadToString).collectException; 530 stopCheck.pause; 531 } 532 } 533 534 void opCall(ref Restore data) { 535 try { 536 restoreFiles(modifiedFiles, fio); 537 } catch (Exception e) { 538 logger.error(e.msg).collectException; 539 data.error = true; 540 hasFatalError_ = true; 541 } 542 } 543 } 544 545 /** Generate schemata injection IDs (32bit) from mutant checksums (128bit). 546 * 547 * There is a possibility that an injection ID result in a collision because 548 * they are only 32 bit. If that happens the mutant is discarded as unfeasable 549 * to use for schemata. 550 * 551 * TODO: if this is changed to being order dependent then it can handle all 552 * mutants. But I can't see how that can be done easily both because of how the 553 * schemas are generated and how the database is setup. 554 */ 555 struct InjectIdBuilder { 556 private { 557 alias InjectId = InjectIdResult.InjectId; 558 559 InjectId[uint] result; 560 Set!uint collisions; 561 } 562 563 void put(MutationStatusId id, Checksum cs) @safe pure nothrow { 564 import dextool.plugin.mutate.backend.analyze.pass_schemata : checksumToId; 565 566 const injectId = checksumToId(cs); 567 debug logger.tracef("%s %s %s", id, cs, injectId).collectException; 568 569 if (injectId in collisions) { 570 } else if (injectId in result) { 571 collisions.add(injectId); 572 result.remove(injectId); 573 } else { 574 result[injectId] = InjectId(id, injectId); 575 } 576 } 577 578 InjectIdResult finalize() @safe pure nothrow { 579 import std.array : array; 580 581 return InjectIdResult(result.byValue.array); 582 } 583 } 584 585 struct InjectIdResult { 586 alias InjectId = Tuple!(MutationStatusId, "statusId", uint, "injectId"); 587 InjectId[] ids; 588 589 InjectId front() @safe pure nothrow { 590 assert(!empty, "Can't get front of an empty range"); 591 return ids[0]; 592 } 593 594 void popFront() @safe pure nothrow { 595 assert(!empty, "Can't pop front of an empty range"); 596 ids = ids[1 .. $]; 597 } 598 599 bool empty() @safe pure nothrow const @nogc { 600 return ids.empty; 601 } 602 } 603 604 @("shall detect a collision and make sure it is never part of the result") 605 unittest { 606 InjectIdBuilder builder; 607 builder.put(MutationStatusId(1), Checksum(1, 2)); 608 builder.put(MutationStatusId(2), Checksum(3, 4)); 609 builder.put(MutationStatusId(3), Checksum(1, 2)); 610 auto r = builder.finalize; 611 612 assert(r.front.statusId == MutationStatusId(2)); 613 r.popFront; 614 assert(r.empty); 615 } 616 617 Edit[] makeRootImpl(ulong end) { 618 import dextool.plugin.mutate.backend.resource : schemataImpl; 619 620 return [ 621 makeHdr[0], new Edit(Interval(end, end), cast(const(ubyte)[]) schemataImpl) 622 ]; 623 } 624 625 Edit[] makeHdr() { 626 import dextool.plugin.mutate.backend.resource : schemataHeader; 627 628 return [new Edit(Interval(0, 0), cast(const(ubyte)[]) schemataHeader)]; 629 }