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