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