1 /** 2 Copyright: Copyright (c) 2017, 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 #SPC-analyzer 11 12 TODO cache the checksums. They are *heavy*. 13 */ 14 module dextool.plugin.mutate.backend.analyze; 15 16 import core.thread : Thread; 17 import logger = std.experimental.logger; 18 import std.algorithm : map, filter, joiner, cache, max; 19 import std.array : array, appender, empty; 20 import std.concurrency; 21 import std.datetime : dur, Duration; 22 import std.exception : collectException; 23 import std.functional : toDelegate; 24 import std.parallelism : TaskPool, totalCPUs; 25 import std.range : tee, enumerate; 26 import std.typecons : tuple; 27 28 import colorlog; 29 import my.actor.utility.limiter; 30 import my.actor; 31 import my.filter : GlobFilter; 32 import my.gc.refc; 33 import my.named_type; 34 import my.optional; 35 import my.set; 36 37 static import colorlog; 38 39 import dextool.utility : dextoolBinaryId; 40 41 import dextool.compilation_db : CompileCommandFilter, defaultCompilerFlagFilter, CompileCommandDB, 42 ParsedCompileCommandRange, ParsedCompileCommand, ParseFlags, SystemIncludePath; 43 import dextool.plugin.mutate.backend.analyze.schema_ml : SchemaQ; 44 import dextool.plugin.mutate.backend.analyze.internal : TokenStream; 45 import dextool.plugin.mutate.backend.analyze.pass_schemata : SchemataResult; 46 import dextool.plugin.mutate.backend.database : Database, LineMetadata, 47 MutationPointEntry2, DepFile; 48 import dextool.plugin.mutate.backend.database.type : MarkedMutant, TestFile, 49 TestFilePath, TestFileChecksum, ToolVersion; 50 import dextool.plugin.mutate.backend.diff_parser : Diff; 51 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO; 52 import dextool.plugin.mutate.backend.report.utility : statusToString, Table; 53 import dextool.plugin.mutate.backend.utility : checksum, Checksum, getProfileResult, Profile; 54 import dextool.plugin.mutate.backend.type : Mutation; 55 import dextool.plugin.mutate.type : MutationKind, MutantIdGeneratorConfig; 56 import dextool.plugin.mutate.config : ConfigCompiler, ConfigAnalyze, ConfigSchema, ConfigCoverage; 57 import dextool.type : ExitStatusType, AbsolutePath, Path; 58 59 version (unittest) { 60 import unit_threaded.assertions; 61 } 62 63 alias log = colorlog.log!"analyze"; 64 65 /** Analyze the files in `frange` for mutations. 66 */ 67 ExitStatusType runAnalyzer(const AbsolutePath dbPath, const AbsolutePath confFile, 68 const MutationKind[] userKinds, ConfigAnalyze analyzeConf, 69 ConfigCompiler compilerConf, ConfigSchema schemaConf, 70 ConfigCoverage covConf, ParsedCompileCommandRange frange, ValidateLoc valLoc, FilesysIO fio) @trusted { 71 import dextool.plugin.mutate.backend.diff_parser : diffFromStdin, Diff; 72 import dextool.plugin.mutate.backend.mutation_type : toInternal; 73 74 auto fileFilter = () { 75 try { 76 return FileFilter(fio.getOutputDir, analyzeConf.unifiedDiffFromStdin, 77 analyzeConf.unifiedDiffFromStdin ? diffFromStdin : Diff.init); 78 } catch (Exception e) { 79 log.info(e.msg); 80 log.warning("Unable to parse diff"); 81 } 82 return FileFilter.init; 83 }(); 84 85 bool shouldAnalyze(AbsolutePath p) { 86 return analyzeConf.fileMatcher.match(p.toString) && fileFilter.shouldAnalyze(p); 87 } 88 89 auto sys = makeSystem; 90 91 auto flowCtrl = sys.spawn(&spawnFlowControl, () { 92 const x = analyzeConf.poolSize == 0 ? (totalCPUs + 1) : analyzeConf.poolSize; 93 // TODO: investigate further why <4 lead to a livelock of the analyzer. 94 return max(x, 4); 95 }()); 96 97 auto db = refCounted(Database.make(dbPath)); 98 99 auto needFullAnalyzeRes = needFullAnalyze(db.get, confFile); 100 101 // if a dependency of a root file has been changed. 102 auto changedDeps = dependencyAnalyze(db.get, needFullAnalyzeRes.status, fio); 103 auto schemaQ = SchemaQ(db.get.schemaApi.getMutantProbability); 104 105 auto store = sys.spawn(&spawnStoreActor, flowCtrl, db, 106 StoreConfig(analyzeConf, schemaConf, covConf), fio, changedDeps.byKeyValue 107 .filter!(a => !a.value) 108 .map!(a => a.key) 109 .array, needFullAnalyzeRes); 110 db.release; 111 // it crashes if the store actor try to call dextoolBinaryId. I don't know 112 // why... TLS store trashed? But it works, somehow, if I put some writeln 113 // inside dextoolBinaryId. 114 send(store, Start.init, ToolVersion(dextoolBinaryId)); 115 116 sys.spawn(&spawnTestPathActor, store, analyzeConf.testPaths, analyzeConf.testFileMatcher, fio); 117 118 auto kinds = toInternal(userKinds); 119 120 foreach (f; frange.filter!(a => shouldAnalyze(a.cmd.absoluteFile))) { 121 try { 122 if (auto v = fio.toRelativeRoot(f.cmd.absoluteFile) in changedDeps) { 123 if (!(*v || analyzeConf.forceSaveAnalyze)) 124 continue; 125 } 126 127 // TODO: how to "slow down" if store is working too slow. 128 129 // must dup schemaQ or we run into multithreaded bugs because a 130 // SchemaQ have mutable caches internally. also must allocate on 131 // the GC because otherwise they share the same associative array. 132 // Don't ask me how that happens because `.dup` should have created 133 // a unique one. If you print the address here of `.state` and the 134 // receiving end you will see that they are re-used between actors! 135 auto sq = new SchemaQ(schemaQ.dup.state); 136 auto a = sys.spawn(&spawnAnalyzer, flowCtrl, store, kinds, f, valLoc.dup, 137 fio.dup, AnalyzeConfig(compilerConf, analyzeConf, covConf, sq)); 138 send(store, StartedAnalyzer.init); 139 } catch (Exception e) { 140 log.trace(e); 141 log.warning(e.msg); 142 } 143 } 144 145 send(store, DoneStartingAnalyzers.init); 146 147 changedDeps = typeof(changedDeps).init; // free the memory 148 149 auto self = scopedActor; 150 bool waiting = true; 151 while (waiting) { 152 try { 153 self.request(store, infTimeout).send(IsDone.init).then((bool x) { 154 waiting = !x; 155 }); 156 } catch (ScopedActorException e) { 157 logger.warning(e.error); 158 return ExitStatusType.Errors; 159 } 160 () @trusted { Thread.sleep(100.dur!"msecs"); }(); 161 } 162 163 if (analyzeConf.profile) 164 try { 165 import std.stdio : writeln; 166 167 writeln(getProfileResult.toString); 168 } catch (Exception e) { 169 log.warning("Unable to print the profile data: ", e.msg).collectException; 170 } 171 172 return ExitStatusType.Ok; 173 } 174 175 @safe: 176 177 /** Filter function for files. Either all or those in stdin. 178 * 179 * The matching ignores the file extension in order to lessen the problem of a 180 * file that this approach skip headers because they do not exist in 181 * `compile_commands.json`. It means that e.g. "foo.hpp" would match `true` if 182 * `foo.cpp` is in `compile_commands.json`. 183 * 184 * TODO: this may create problems for header only libraries because only the 185 * unittest would include the header which mean that for this to work the 186 * unittest would have to reside in the same directory as the header file. 187 * Which they normally never do. This then lead to a diff of a header only lib 188 * lead to "no files analyzed". 189 */ 190 struct FileFilter { 191 import std.path : stripExtension; 192 193 Set!string files; 194 bool useFileFilter; 195 AbsolutePath root; 196 197 this(AbsolutePath root, bool fromStdin, Diff diff) { 198 this.root = root; 199 this.useFileFilter = fromStdin; 200 foreach (a; diff.toRange(root)) { 201 files.add(a.key.stripExtension); 202 } 203 } 204 205 bool shouldAnalyze(AbsolutePath p) { 206 import std.path : relativePath; 207 208 if (!useFileFilter) { 209 return true; 210 } 211 212 return relativePath(p, root).stripExtension in files; 213 } 214 } 215 216 struct StartedAnalyzer { 217 } 218 219 struct DoneStartingAnalyzers { 220 } 221 222 /// Number of analyze tasks that has been spawned that the `storeActor` should wait for. 223 struct AnalyzeCntMsg { 224 int value; 225 } 226 227 /// The main thread is waiting for storeActor to send this message. 228 struct StoreDoneMsg { 229 } 230 231 struct AnalyzeConfig { 232 ConfigCompiler compiler; 233 ConfigAnalyze analyze; 234 ConfigCoverage coverage; 235 SchemaQ* sq; 236 } 237 238 struct WaitForToken { 239 } 240 241 struct RunAnalyze { 242 } 243 244 alias AnalyzeActor = typedActor!(void function(WaitForToken), void function(RunAnalyze)); 245 246 /// Start an analyze of a file 247 auto spawnAnalyzer(AnalyzeActor.Impl self, FlowControlActor.Address flowCtrl, StoreActor.Address storeAddr, 248 Mutation.Kind[] kinds, ParsedCompileCommand fileToAnalyze, 249 ValidateLoc vloc, FilesysIO fio, AnalyzeConfig conf) { 250 auto st = tuple!("self", "flowCtrl", "storeAddr", "kinds", "fileToAnalyze", 251 "vloc", "fio", "conf")(self, flowCtrl, storeAddr, kinds, 252 fileToAnalyze, vloc, fio.dup, conf); 253 alias Ctx = typeof(st); 254 255 static void wait(ref Ctx ctx, WaitForToken) { 256 ctx.self.request(ctx.flowCtrl, infTimeout).send(TakeTokenMsg.init) 257 .capture(ctx).then((ref Ctx ctx, Token _) => send(ctx.self, RunAnalyze.init)); 258 } 259 260 static void run(ref Ctx ctx, RunAnalyze) @safe { 261 auto profile = Profile("analyze file " ~ ctx.fileToAnalyze.cmd.absoluteFile); 262 263 bool onlyValidFiles = true; 264 265 try { 266 log.tracef("%s begin", ctx.fileToAnalyze.cmd.absoluteFile); 267 auto analyzer = Analyze(ctx.kinds, ctx.vloc, ctx.fio, 268 Analyze.Config(ctx.conf.compiler.forceSystemIncludes, 269 ctx.conf.coverage.use, ctx.conf.compiler.allowErrors.get, *ctx.conf.sq)); 270 analyzer.process(ctx.fileToAnalyze, ctx.conf.analyze.idGenConfig); 271 272 foreach (a; analyzer.result.idFile.byKey) { 273 if (!isFileSupported(ctx.fio, a)) { 274 log.warningf( 275 "%s: file not supported. It must be in utf-8 format without a BOM marker"); 276 onlyValidFiles = false; 277 break; 278 } 279 } 280 281 if (onlyValidFiles) 282 send(ctx.storeAddr, analyzer.result, Token.init); 283 log.tracef("%s end", ctx.fileToAnalyze.cmd.absoluteFile); 284 } catch (Exception e) { 285 onlyValidFiles = false; 286 log.error(e.msg).collectException; 287 } 288 289 if (!onlyValidFiles) { 290 log.tracef("%s failed", ctx.fileToAnalyze.cmd.absoluteFile).collectException; 291 send(ctx.storeAddr, Token.init); 292 } 293 294 ctx.self.shutdown; 295 } 296 297 self.name = "analyze"; 298 send(self, WaitForToken.init); 299 return impl(self, &run, capture(st), &wait, capture(st)); 300 } 301 302 class TestFileResult { 303 Duration time; 304 TestFile[Checksum] files; 305 } 306 307 alias TestPathActor = typedActor!(void function(Start, StoreActor.Address)); 308 309 auto spawnTestPathActor(TestPathActor.Impl self, StoreActor.Address store, 310 AbsolutePath[] userPaths, GlobFilter matcher, FilesysIO fio) { 311 import std.datetime : Clock; 312 import std.datetime.stopwatch : StopWatch, AutoStart; 313 import std.file : isDir, isFile, dirEntries, SpanMode; 314 import my.container.vector; 315 316 auto st = tuple!("self", "matcher", "fio", "userPaths")(self, matcher, fio.dup, userPaths); 317 alias Ctx = typeof(st); 318 319 static void start(ref Ctx ctx, Start, StoreActor.Address store) { 320 auto profile = Profile("checksum test files"); 321 322 auto sw = StopWatch(AutoStart.yes); 323 324 TestFile makeTestFile(const AbsolutePath file) { 325 auto cs = checksum(ctx.fio.makeInput(file).content[]); 326 return TestFile(TestFilePath(ctx.fio.toRelativeRoot(file)), 327 TestFileChecksum(cs), Clock.currTime); 328 } 329 330 auto paths = vector(ctx.userPaths); 331 332 auto tfiles = new TestFileResult; 333 scope (exit) 334 tfiles.time = sw.peek; 335 336 while (!paths.empty) { 337 try { 338 if (isDir(paths.front)) { 339 log.trace(" Test directory ", paths.front); 340 foreach (a; dirEntries(paths.front, SpanMode.shallow).map!( 341 a => AbsolutePath(a.name))) { 342 paths.put(a); 343 } 344 } else if (isFile(paths.front) && ctx.matcher.match(paths.front)) { 345 log.trace(" Test saved ", paths.front); 346 auto t = makeTestFile(paths.front); 347 tfiles.files[t.checksum.get] = t; 348 } 349 } catch (Exception e) { 350 log.warning(e.msg).collectException; 351 } 352 353 paths.popFront; 354 } 355 356 log.infof("Found %s test files", tfiles.files.length).collectException; 357 send(store, tfiles); 358 ctx.self.shutdown; 359 } 360 361 self.name = "test path"; 362 send(self, Start.init, store); 363 return impl(self, &start, capture(st)); 364 } 365 366 struct Start { 367 } 368 369 struct IsDone { 370 } 371 372 struct SetDone { 373 } 374 375 // Check if it is time to post process 376 struct CheckPostProcess { 377 } 378 // Run the post processning. 379 struct PostProcess { 380 } 381 382 struct StoreConfig { 383 ConfigAnalyze analyze; 384 ConfigSchema schema; 385 ConfigCoverage coverage; 386 } 387 388 alias StoreActor = typedActor!(void function(Start, ToolVersion), bool function(IsDone), 389 void function(StartedAnalyzer), void function(Analyze.Result, Token), // failed to analyze the file, but still returning the token. 390 void function(Token), 391 void function(DoneStartingAnalyzers), void function(TestFileResult), 392 void function(CheckPostProcess), void function(PostProcess),); 393 394 /// Store the result of the analyze. 395 auto spawnStoreActor(StoreActor.Impl self, FlowControlActor.Address flowCtrl, RefCounted!(Database) db, 396 StoreConfig conf, FilesysIO fio, Path[] rootFiles, NeedFullAnalyzeResult needFullAnalyze) @trusted { 397 static struct State { 398 import dextool.plugin.mutate.backend.type : CodeMutant; 399 400 NeedFullAnalyzeResult needFullAnalyze; 401 402 // conditions governing when the analyze is done 403 // if all analyze workers have been started and thus it is time to 404 // start checking if startedAnalyzers == savedResult. 405 bool doneStarting; 406 // number of analyze workers that have been started. 407 int startedAnalyzers; 408 // number of saved results. 409 int savedResult; 410 // if checksums of all test files have been saved to disk 411 bool savedTestFileResult; 412 413 // if a file is modified then the timeout context need to be reset 414 bool resetTimeoutCtx; 415 416 /// Set when the whole analyze process is done and all results are saved to the database. 417 bool isDone; 418 419 // only save new mutants. assuming that it is faster to check if the 420 // mutants have been saved before than to go through multiple sql 421 // queries. 422 Set!CodeMutant saved; 423 424 // files that have been saved to the database. 425 Set!AbsolutePath savedFiles; 426 // clearing a file should only happen once. 427 Set!AbsolutePath clearedFiles; 428 } 429 430 auto st = tuple!("self", "db", "state", "fio", "conf", "rootFiles", "flowCtrl")(self, 431 db, refCounted(State(needFullAnalyze)), fio.dup, conf, rootFiles, flowCtrl); 432 alias Ctx = typeof(st); 433 434 static void start(ref Ctx ctx, Start, ToolVersion toolVersion) { 435 log.trace("starting store actor"); 436 437 if (ctx.conf.analyze.fastDbStore) { 438 log.info( 439 "Turning OFF sqlite3 synchronization protection to improve the write performance"); 440 log.warning("Do NOT interrupt dextool in any way because it may corrupt the database"); 441 ctx.db.get.run("PRAGMA synchronous = OFF"); 442 ctx.db.get.run("PRAGMA journal_mode = MEMORY"); 443 } 444 445 send(ctx.self, CheckPostProcess.init); 446 log.trace("store actor active"); 447 } 448 449 static bool isDone(ref Ctx ctx, IsDone) { 450 return ctx.state.get.isDone; 451 } 452 453 static void startedAnalyzers(ref Ctx ctx, StartedAnalyzer) { 454 ctx.state.get.startedAnalyzers++; 455 } 456 457 static void doneStartAnalyzers(ref Ctx ctx, DoneStartingAnalyzers) { 458 ctx.state.get.doneStarting = true; 459 } 460 461 static void failedFileAnalyze(ref Ctx ctx, Token) { 462 send(ctx.flowCtrl, ReturnTokenMsg.init); 463 // a failed file has to count as well. 464 ctx.state.get.savedResult++; 465 } 466 467 static void checkPostProcess(ref Ctx ctx, CheckPostProcess) { 468 if (ctx.state.get.doneStarting && ctx.state.get.savedTestFileResult 469 && (ctx.state.get.startedAnalyzers == ctx.state.get.savedResult)) 470 send(ctx.self, PostProcess.init); 471 else 472 delayedSend(ctx.self, delay(500.dur!"msecs"), CheckPostProcess.init); 473 } 474 475 static void savedTestFileResult(ref Ctx ctx, TestFileResult result) { 476 auto profile = Profile("save test files"); 477 478 ctx.state.get.savedTestFileResult = true; 479 480 Set!Checksum old; 481 482 auto t = ctx.db.get.transaction; 483 484 foreach (a; ctx.db.get.testFileApi.getTestFiles) { 485 old.add(a.checksum.get); 486 if (a.checksum.get !in result.files) { 487 log.info("Removed test file ", a.file.get.toString); 488 ctx.db.get.testFileApi.removeFile(a.file); 489 } 490 } 491 492 foreach (a; result.files.byValue.filter!(a => a.checksum.get !in old)) { 493 log.info("Saving test file ", a.file.get.toString); 494 ctx.db.get.testFileApi.put(a); 495 } 496 497 t.commit; 498 499 send(ctx.self, CheckPostProcess.init); 500 } 501 502 static void save(ref Ctx ctx, Analyze.Result result, Token) { 503 import dextool.cachetools : nullableCache; 504 import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut; 505 import dextool.plugin.mutate.backend.type : Language; 506 507 auto profile = Profile("save " ~ result.root); 508 509 // by returning the token now another file analyze can start while we 510 // are saving the current one. 511 send(ctx.flowCtrl, ReturnTokenMsg.init); 512 513 ctx.state.get.savedResult++; 514 log.infof("Analyzed %s/%s %s", ctx.state.get.savedResult, 515 ctx.state.get.startedAnalyzers, result.root); 516 517 auto getFileId = nullableCache!(string, FileId, (string p) => ctx.db.get.getFileId(p.Path))(256, 518 10.dur!"seconds"); 519 auto getFileDbChecksum = nullableCache!(string, Checksum, 520 (string p) => ctx.db.get.getFileChecksum(p.Path))(256, 30.dur!"seconds"); 521 auto getFileFsChecksum = nullableCache!(string, Checksum, (string p) { 522 return checksum(ctx.fio.makeInput(AbsolutePath(Path(p))).content[]); 523 })(256, 10.dur!"seconds"); 524 525 static struct Files { 526 Checksum[Path] value; 527 528 this(ref Database db) { 529 foreach (a; db.getDetailedFiles) { 530 value[a.file] = a.fileChecksum; 531 } 532 } 533 } 534 535 auto trans = ctx.db.get.transaction; 536 537 // keeps both absolute and relative because then less transformations 538 // are needed. mutation points use relative... 539 Set!Path skipFile; 540 541 // mark files that have an unchanged checksum as "already saved" 542 foreach (f; result.idFile.byKey.filter!(a => a !in ctx.state.get.clearedFiles)) { 543 const relp = ctx.fio.toRelativeRoot(f); 544 545 if (getFileDbChecksum(relp) != getFileFsChecksum(f) 546 || ctx.conf.analyze.forceSaveAnalyze || ctx.state.get.needFullAnalyze.status) { 547 // this is critical in order to remove old data about a file. 548 if (f !in ctx.state.get.clearedFiles) { 549 ctx.db.get.removeFile(relp); 550 ctx.state.get.clearedFiles.add(f); 551 } 552 } else { 553 log.info("Unchanged ".color(Color.yellow), f); 554 ctx.state.get.savedFiles.add(f); 555 skipFile.add(f); 556 skipFile.add(relp); 557 } 558 } 559 560 { 561 bool isChanged = ctx.state.get.needFullAnalyze.status; 562 563 foreach (f; result.idFile.byKey.filter!(a => a !in skipFile 564 && a !in ctx.state.get.savedFiles)) { 565 isChanged = true; 566 log.info("Saving ".color(Color.green), f); 567 568 const relp = ctx.fio.toRelativeRoot(f); 569 const info = result.infoId[result.idFile[f]]; 570 ctx.db.get.fileApi.put(relp, info.checksum, info.language, f == result.root); 571 572 ctx.state.get.savedFiles.add(f); 573 } 574 575 if (result.root !in ctx.state.get.savedFiles) { 576 // this occurs when the file is e.g. a unittest that uses a 577 // header only library. The unittests are not mutated thus 578 // no mutation points exists in them but we want dextool to 579 // still, if possible, track the unittests for changes. 580 isChanged = true; 581 const relp = ctx.fio.toRelativeRoot(result.root); 582 ctx.db.get.removeFile(relp); 583 // the language do not matter because it is a file without 584 // any mutants. 585 ctx.db.get.fileApi.put(relp, result.rootCs, Language.init, true); 586 ctx.state.get.savedFiles.add(ctx.fio.toAbsoluteRoot(result.root)); 587 } 588 589 { 590 auto app = appender!(MutationPointEntry2[])(); 591 foreach (mp; result.mutationPoints.filter!(a => a.file !in skipFile 592 && a.cm !in ctx.state.get.saved)) { 593 app.put(mp); 594 } 595 // only block new mutants of the same source code change after 596 // a whole "pass" because the same mutant kind can result in 597 // the same CodeChecksum. 598 ctx.state.get.saved.add(app.data.map!(a => a.cm)); 599 ctx.db.get.mutantApi.put(app.data, ctx.fio.getOutputDir); 600 } 601 602 // must always update dependencies because they may not contain 603 // mutants. Only files that are changed and contain mutants 604 // trigger isChanged to be true. 605 try { 606 // not all files are tracked thus this may throw an exception. 607 ctx.db.get.dependencyApi.set(ctx.fio.toRelativeRoot(result.root), 608 result.dependencies); 609 } catch (Exception e) { 610 } 611 612 ctx.state.get.resetTimeoutCtx = ctx.state.get.resetTimeoutCtx || isChanged; 613 614 if (isChanged) { 615 foreach (a; result.coverage.byKeyValue) { 616 const fid = getFileId(ctx.fio.toRelativeRoot(result.fileId[a.key])); 617 if (!fid.isNull) { 618 ctx.db.get.coverageApi.clearCoverageMap(fid.get); 619 ctx.db.get.coverageApi.putCoverageMap(fid.get, a.value); 620 } 621 } 622 623 saveSchemaFragments(ctx.db.get, ctx.fio, result.schematas); 624 } 625 } 626 627 { 628 Set!long printed; 629 auto app = appender!(LineMetadata[])(); 630 foreach (md; result.metadata) { 631 const localId = Analyze.Result.LocalFileId(md.id.get); 632 // transform the ID from local to global. 633 const fid = getFileId(ctx.fio.toRelativeRoot(result.fileId[localId])); 634 if (fid.isNull && !printed.contains(md.id.get)) { 635 printed.add(md.id.get); 636 log.info("File with suppressed mutants (// NOMUT) not in the database: ", 637 result.fileId[localId]).collectException; 638 } else if (!fid.isNull) { 639 app.put(LineMetadata(fid.get, md.line, md.attr)); 640 } 641 } 642 ctx.db.get.metaDataApi.put(app.data); 643 } 644 645 trans.commit; 646 647 send(ctx.self, CheckPostProcess.init); 648 } 649 650 static void postProcess(ref Ctx ctx, PostProcess) { 651 import dextool.plugin.mutate.backend.test_mutant.timeout : resetTimeoutContext; 652 653 if (ctx.state.get.isDone) 654 return; 655 656 ctx.state.get.isDone = true; 657 658 void fastDbOff() { 659 if (!ctx.conf.analyze.fastDbStore) 660 return; 661 ctx.db.get.run("PRAGMA synchronous = ON"); 662 ctx.db.get.run("PRAGMA journal_mode = DELETE"); 663 } 664 665 void pruneFiles() { 666 import std.path : buildPath; 667 668 auto profile = Profile("prune files"); 669 670 log.info("Pruning the database of dropped files"); 671 auto files = ctx.db.get.getFiles.map!(a => ctx.fio.toAbsoluteRoot(a)).toSet; 672 673 foreach (f; files.setDifference(ctx.state.get.savedFiles).toRange) { 674 log.info("Removing ".color(Color.red), f); 675 ctx.db.get.removeFile(ctx.fio.toRelativeRoot(f)); 676 } 677 } 678 679 void addRoots() { 680 if (ctx.conf.analyze.forceSaveAnalyze || ctx.state.get.needFullAnalyze.status) 681 return; 682 683 // add root files and their dependencies that has not been analyzed because nothing has changed. 684 // By adding them they are not removed. 685 686 auto profile = Profile("add roots and dependencies"); 687 foreach (a; ctx.rootFiles) { 688 auto p = ctx.fio.toAbsoluteRoot(a); 689 if (p !in ctx.state.get.savedFiles) { 690 ctx.state.get.savedFiles.add(p); 691 // fejk text for the user to tell them that yes, the files have 692 // been analyzed. 693 log.info("Analyzing ", a); 694 log.info("Unchanged ".color(Color.yellow), a); 695 } 696 } 697 foreach (a; ctx.rootFiles.map!(a => ctx.db.get.dependencyApi.get(a)).joiner) { 698 ctx.state.get.savedFiles.add(ctx.fio.toAbsoluteRoot(a)); 699 } 700 } 701 702 void pruneSchemaMl() { 703 auto profile = Profile("prune schema_ml model"); 704 log.info("Prune schema ML model"); 705 706 Set!Checksum files; 707 foreach (a; ctx.db.get.getFiles) 708 files.add(checksum(cast(const(ubyte)[]) a.toString)); 709 710 foreach (a; ctx.db.get.schemaApi.getMutantProbability.byKey.filter!(a => a !in files)) { 711 logger.trace("schema model. Dropping ", a); 712 ctx.db.get.schemaApi.removeMutantProbability(a); 713 } 714 } 715 716 auto trans = ctx.db.get.transaction; 717 718 addRoots; 719 720 if (ctx.state.get.resetTimeoutCtx) { 721 log.info("Resetting timeout context"); 722 resetTimeoutContext(ctx.db.get); 723 } 724 725 log.info("Updating metadata"); 726 ctx.db.get.metaDataApi.updateMetadata; 727 728 if (ctx.conf.analyze.prune) { 729 pruneFiles(); 730 { 731 auto profile = Profile("prune dependencies"); 732 log.info("Prune dependencies"); 733 ctx.db.get.dependencyApi.cleanup; 734 } 735 { 736 auto profile = Profile("remove orphaned mutants"); 737 log.info("Removing orphaned mutants"); 738 auto progress = (size_t i, size_t total, const Duration avgRemoveTime, 739 const Duration timeLeft, SysTime predDoneAt) { 740 logger.infof("%s/%s removed (average %s) (%s) (%s)", i, 741 total, avgRemoveTime, timeLeft, predDoneAt.toSimpleString); 742 }; 743 auto done = (size_t total) { 744 logger.infof(total > 0, "%1$s/%1$s removed", total); 745 }; 746 ctx.db.get.mutantApi.removeOrphanedMutants(progress.toDelegate, done.toDelegate); 747 } 748 try { 749 pruneSchemaMl; 750 } catch (Exception e) { 751 logger.warning(e.msg); 752 logger.warning("Unable to prune schema ML model"); 753 } 754 } 755 756 log.info("Updating manually marked mutants"); 757 updateMarkedMutants(ctx.db.get); 758 printLostMarkings(ctx.db.get.markMutantApi.getLostMarkings); 759 760 if (ctx.state.get.needFullAnalyze.status) { 761 log.info("Updating tool version"); 762 ctx.db.get.miscApi.setToolVersion(ToolVersion(dextoolBinaryId)); 763 log.info("Update config version"); 764 ctx.db.get.miscApi.setConfigVersion(ctx.state.get.needFullAnalyze.cs); 765 } 766 767 log.info("Committing changes"); 768 trans.commit; 769 log.info("Ok".color(Color.green)); 770 771 fastDbOff(); 772 773 if (ctx.state.get.needFullAnalyze.status) { 774 auto profile = Profile("compact"); 775 log.info("Compacting the database"); 776 ctx.db.get.vacuum; 777 } 778 } 779 780 self.name = "store"; 781 782 auto s = impl(self, &start, capture(st), &isDone, capture(st), 783 &startedAnalyzers, capture(st), &save, capture(st), &doneStartAnalyzers, 784 capture(st), &savedTestFileResult, capture(st), &checkPostProcess, 785 capture(st), &postProcess, capture(st), &failedFileAnalyze, capture(st)); 786 s.exceptionHandler = toDelegate(&logExceptionHandler); 787 return s; 788 } 789 790 /// Analyze a file for mutants. 791 struct Analyze { 792 import std.regex : Regex, regex, matchFirst; 793 import std.typecons : Yes; 794 import libclang_ast.context : ClangContext; 795 796 static struct Config { 797 bool forceSystemIncludes; 798 bool saveCoverage; 799 bool allowErrors; 800 SchemaQ sq; 801 } 802 803 private { 804 static immutable rawReNomut = `^((//)|(/\*+))\s*NOMUT(?P<type>\w*)\s*(\((?P<tag>.*)\))?\s*((?P<comment>.*)\*/|(?P<comment>.*))?`; 805 806 Regex!char re_nomut; 807 ValidateLoc valLoc; 808 FilesysIO fio; 809 810 Result result; 811 812 Config conf; 813 814 Mutation.Kind[] kinds; 815 } 816 817 this(Mutation.Kind[] kinds, ValidateLoc valLoc, FilesysIO fio, Config conf) @trusted { 818 this.kinds = kinds; 819 this.valLoc = valLoc; 820 this.fio = fio; 821 this.re_nomut = regex(rawReNomut); 822 this.result = new Result; 823 this.conf = conf; 824 } 825 826 void process(ParsedCompileCommand commandsForFileToAnalyze, MutantIdGeneratorConfig idGenConf) @safe { 827 import std.file : exists; 828 829 commandsForFileToAnalyze.flags.forceSystemIncludes = conf.forceSystemIncludes; 830 831 try { 832 if (!exists(commandsForFileToAnalyze.cmd.absoluteFile)) { 833 log.warningf("Failed to analyze %s. Do not exist", 834 commandsForFileToAnalyze.cmd.absoluteFile); 835 return; 836 } 837 } catch (Exception e) { 838 log.warning(e.msg); 839 return; 840 } 841 842 result.root = commandsForFileToAnalyze.cmd.absoluteFile; 843 844 try { 845 result.rootCs = checksum(result.root); 846 847 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 848 scope tstream = new TokenStreamImpl(ctx); 849 850 analyzeForMutants(commandsForFileToAnalyze, result.root, ctx, tstream, idGenConf); 851 foreach (f; result.fileId.byValue) 852 analyzeForComments(f, tstream); 853 } catch (Exception e) { 854 () @trusted { log.trace(e); }(); 855 log.info(e.msg); 856 log.error("failed analyze of ", 857 commandsForFileToAnalyze.cmd.absoluteFile).collectException; 858 } 859 } 860 861 void analyzeForMutants(ParsedCompileCommand commandsForFileToAnalyze, AbsolutePath fileToAnalyze, 862 ref ClangContext ctx, scope TokenStream tstream, MutantIdGeneratorConfig idGenConf) @safe { 863 import my.gc.refc : RefCounted; 864 import dextool.plugin.mutate.backend.analyze.ast : Ast; 865 import dextool.plugin.mutate.backend.analyze.pass_clang; 866 import dextool.plugin.mutate.backend.analyze.pass_coverage; 867 import dextool.plugin.mutate.backend.analyze.pass_filter; 868 import dextool.plugin.mutate.backend.analyze.pass_mutant; 869 import dextool.plugin.mutate.backend.analyze.pass_schemata; 870 import libclang_ast.check_parse_result : hasParseErrors, logDiagnostic; 871 872 log.info("Analyzing ", fileToAnalyze); 873 RefCounted!(Ast) ast; 874 { 875 auto tu = ctx.makeTranslationUnit(fileToAnalyze, 876 commandsForFileToAnalyze.flags.completeFlags); 877 if (tu.hasParseErrors) { 878 logDiagnostic(tu); 879 log.warningf("Compile error in %s", fileToAnalyze); 880 if (!conf.allowErrors) { 881 log.warning("Skipping"); 882 return; 883 } 884 } 885 886 auto res = toMutateAst(tu.cursor, fio, valLoc); 887 ast = res.ast; 888 saveDependencies(commandsForFileToAnalyze.flags, result.root, res.dependencies); 889 log!"analyze.pass_clang".trace(ast.get.toString); 890 } 891 892 auto codeMutants = () { 893 auto mutants = toMutants(ast.ptr, fio, valLoc, kinds); 894 log!"analyze.pass_mutant".trace(mutants); 895 896 log!"analyze.pass_filter".trace("filter mutants"); 897 mutants = filterMutants(fio, mutants); 898 log!"analyze.pass_filter".trace(mutants); 899 900 return toCodeMutants(mutants, fio, tstream, idGenConf); 901 }(); 902 debug logger.trace(codeMutants); 903 904 { 905 auto schemas = toSchemata(ast.ptr, fio, codeMutants, conf.sq); 906 log!"analyze.pass_schema".trace(schemas); 907 log.tracef("path dedup count:%s length_acc:%s", 908 ast.get.paths.count, ast.get.paths.lengthAccum); 909 910 result.schematas = schemas.getFragments; 911 } 912 913 { 914 auto app = appender!(MutationPointEntry2[])(); 915 foreach (a; codeMutants.points.byKeyValue) { 916 foreach (b; a.value) { 917 app.put(MutationPointEntry2(fio.toRelativeRoot(a.key), 918 b.offset, b.sloc.begin, b.sloc.end, b.mutant)); 919 } 920 } 921 result.mutationPoints = app.data; 922 } 923 foreach (f; codeMutants.points.byKey) { 924 const id = Result.LocalFileId(result.idFile.length); 925 result.idFile[f] = id; 926 result.fileId[id] = f; 927 result.infoId[id] = Result.FileInfo(codeMutants.csFiles[f], codeMutants.lang); 928 } 929 930 if (conf.saveCoverage) { 931 auto cov = toCoverage(ast.ptr, fio, valLoc); 932 debug logger.trace(cov); 933 934 foreach (a; cov.points.byKeyValue) { 935 if (auto id = a.key in result.idFile) { 936 result.coverage[*id] = a.value; 937 } 938 } 939 } 940 } 941 942 /** Tokens are always from the same file. 943 * 944 * TODO: move this to pass_clang. 945 */ 946 void analyzeForComments(AbsolutePath file, scope TokenStream tstream) @safe { 947 import std.algorithm : filter; 948 import clang.c.Index : CXTokenKind; 949 import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut; 950 951 if (auto localId = file in result.idFile) { 952 const fid = FileId(localId.get); 953 954 auto mdata = appender!(LineMetadata[])(); 955 956 int sectionStart = -1; 957 LineMetadata sectionData; 958 959 foreach (t; tstream.getTokens(file).filter!(a => a.kind == CXTokenKind.comment)) { 960 auto m = matchFirst(t.spelling, re_nomut); 961 962 if (m.whichPattern == 0) 963 continue; 964 965 switch (m["type"]) { 966 case "BEGIN": 967 if (sectionStart == -1) { 968 sectionStart = t.loc.line; 969 sectionData = LineMetadata(fid, t.loc.line + 1, 970 LineAttr(NoMut(m["tag"], m["comment"]))); 971 } else { 972 logger.warningf("NOMUT: Found multiple NOMUTBEGIN in a row! Will use the first one on line %s", 973 sectionStart); 974 } 975 break; 976 case "END": 977 if (sectionStart == -1) { 978 logger.warningf("NOMUT: Found a NOMUTEND without a NOMUTBEGIN on line %s! Ignoring", 979 t.loc.line); 980 } else { 981 foreach (const i; sectionStart .. t.loc.line) { 982 sectionData.line = i; 983 () @trusted { mdata.put(sectionData); }(); 984 log.tracef("NOMUT found at %s:%s:%s", file, t.loc.line, t.loc.column); 985 } 986 987 sectionStart = -1; 988 sectionData = LineMetadata.init; 989 } 990 break; 991 case "NEXT": 992 () @trusted { 993 mdata.put(LineMetadata(fid, t.loc.line + 1, 994 LineAttr(NoMut(m["tag"], m["comment"])))); 995 }(); 996 log.tracef("NOMUT ON NEXT LINE found at %s:%s:%s", file, 997 t.loc.line, t.loc.column); 998 break; 999 default: 1000 () @trusted { 1001 mdata.put(LineMetadata(fid, t.loc.line, 1002 LineAttr(NoMut(m["tag"], m["comment"])))); 1003 }(); 1004 log.tracef("NOMUT found at %s:%s:%s", file, t.loc.line, t.loc.column); 1005 break; 1006 } 1007 } 1008 result.metadata ~= mdata.data; 1009 } 1010 } 1011 1012 void saveDependencies(ParseFlags flags, AbsolutePath root, Path[] dependencies) @trusted { 1013 import std.algorithm : cache; 1014 import std.mmfile; 1015 1016 auto rootDir = root.dirName; 1017 1018 foreach (p; dependencies.map!(a => toAbsolutePath(a, rootDir, 1019 flags.includes, flags.systemIncludes)) 1020 .cache 1021 .filter!(a => a.hasValue) 1022 .map!(a => a.orElse(AbsolutePath.init)) 1023 .filter!(a => valLoc.isInsideOutputDir(a))) { 1024 try { 1025 result.dependencies ~= DepFile(fio.toRelativeRoot(p), checksum(p)); 1026 } catch (Exception e) { 1027 log.trace(e.msg).collectException; 1028 } 1029 } 1030 1031 log.trace(result.dependencies); 1032 } 1033 1034 static class Result { 1035 import dextool.plugin.mutate.backend.analyze.ast : Interval; 1036 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 1037 import dextool.plugin.mutate.backend.type : Language, CodeChecksum, SchemataChecksum; 1038 1039 alias LocalFileId = NamedType!(long, Tag!"LocalFileId", long.init, 1040 TagStringable, Hashable); 1041 alias LocalSchemaId = NamedType!(long, Tag!"LocalSchemaId", long.init, 1042 TagStringable, Hashable); 1043 1044 MutationPointEntry2[] mutationPoints; 1045 1046 static struct FileInfo { 1047 Checksum checksum; 1048 Language language; 1049 } 1050 1051 /// The file that is analyzed, which is a root 1052 AbsolutePath root; 1053 Checksum rootCs; 1054 1055 /// The dependencies the root has. 1056 DepFile[] dependencies; 1057 1058 /// The key is the ID from idFile. 1059 FileInfo[LocalFileId] infoId; 1060 1061 /// The IDs is unique for *this* analyze, not globally. 1062 LocalFileId[AbsolutePath] idFile; 1063 AbsolutePath[LocalFileId] fileId; 1064 1065 // The FileID used in the metadata is local to this analysis. It has to 1066 // be remapped when added to the database. 1067 LineMetadata[] metadata; 1068 1069 /// Mutant schematas that has been generated. 1070 SchemataResult.Fragments[AbsolutePath] schematas; 1071 1072 /// Coverage intervals that can be instrumented. 1073 Interval[][LocalFileId] coverage; 1074 } 1075 } 1076 1077 @( 1078 "shall extract the tag and comment from the input following the pattern NOMUT with optional tag and comment") 1079 unittest { 1080 import std.algorithm : canFind; 1081 import std.format : format; 1082 import std.regex : regex, matchFirst; 1083 import unit_threaded.runner.io : writelnUt; 1084 1085 auto reNomut = regex(Analyze.rawReNomut); 1086 const types = ["NOMUT", "NOMUTBEGIN", "NOMUTEND", "NOMUTNEXT"]; 1087 auto okParseTypes = ["", "BEGIN", "END", "NEXT"]; 1088 // NOMUT in other type of comments should NOT match. 1089 foreach (line; [ 1090 "/// %s", "// stuff with %s in it", "/* stuff with %s in it */" 1091 ]) { 1092 foreach (type; types) { 1093 matchFirst(format(line, type), reNomut).whichPattern.shouldEqual(0); 1094 } 1095 } 1096 1097 foreach (line; ["//%s", "// %s", "/*%s*/", "/* %s */", "/**%s*/"]) { 1098 foreach (type; types) { 1099 auto m = matchFirst(format(line, type), reNomut); 1100 m.whichPattern.shouldEqual(1); 1101 m["comment"].shouldEqual(""); 1102 m["tag"].shouldEqual(""); 1103 } 1104 } 1105 1106 foreach (line; ["//%s (my tag)", "// %s (my tag)", "/* %s (my tag) */",]) { 1107 foreach (type; types) { 1108 auto m = matchFirst(format(line, type), reNomut); 1109 m.whichPattern.shouldEqual(1); 1110 m["comment"].shouldEqual(""); 1111 m["tag"].shouldEqual("my tag"); 1112 } 1113 } 1114 1115 // TODO: should work but doesn't.... : "/* %s my comment */" 1116 foreach (line; ["//%s my comment", "// %s my comment"]) { 1117 foreach (type; types) { 1118 auto m = matchFirst(format(line, type), reNomut); 1119 m.whichPattern.shouldEqual(1); 1120 okParseTypes.canFind(m["type"]).shouldBeGreaterThan(0); 1121 m["comment"].shouldEqual("my comment"); 1122 m["tag"].shouldEqual(""); 1123 } 1124 } 1125 1126 foreach (line; ["//%s (my tag) my comment", "// %s (my tag) my comment"]) { 1127 foreach (type; types) { 1128 auto m = matchFirst(format(line, type), reNomut); 1129 m.whichPattern.shouldEqual(1); 1130 okParseTypes.canFind(m["type"]).shouldBeGreaterThan(0); 1131 m["comment"].shouldEqual("my comment"); 1132 m["tag"].shouldEqual("my tag"); 1133 } 1134 } 1135 } 1136 1137 /// Stream of tokens excluding comment tokens. 1138 class TokenStreamImpl : TokenStream { 1139 import libclang_ast.context : ClangContext; 1140 import dextool.plugin.mutate.backend.type : Token; 1141 import dextool.plugin.mutate.backend.utility : tokenize; 1142 1143 ClangContext* ctx; 1144 1145 /// The context must outlive any instance of this class. 1146 // TODO remove @trusted when upgrading to dmd-fe 2.091.0+ and activate dip25 + 1000 1147 this(ref ClangContext ctx) @trusted { 1148 this.ctx = &ctx; 1149 } 1150 1151 Token[] getTokens(Path p) scope { 1152 return tokenize(*ctx, p); 1153 } 1154 1155 Token[] getFilteredTokens(Path p) scope { 1156 import clang.c.Index : CXTokenKind; 1157 1158 // Filter a stream of tokens for those that should affect the checksum. 1159 return tokenize(*ctx, p).filter!(a => a.kind != CXTokenKind.comment).array; 1160 } 1161 } 1162 1163 /** Update the connection between the marked mutants and their mutation status 1164 * id and mutation id. 1165 */ 1166 void updateMarkedMutants(ref Database db) @trusted { 1167 import dextool.plugin.mutate.backend.database.type : MutationStatusId, 1168 toMutationStatusId, toChecksum; 1169 import dextool.plugin.mutate.backend.type : ExitStatus; 1170 1171 void update(MarkedMutant m) { 1172 const stId = toMutationStatusId(m.statusChecksum); 1173 db.markMutantApi.remove(m.statusChecksum); 1174 db.markMutantApi.mark(m.path, m.sloc, stId, m.statusChecksum, 1175 m.toStatus, m.rationale, m.mutText); 1176 db.mutantApi.update(stId, m.toStatus, ExitStatus(0)); 1177 } 1178 1179 // find those marked mutants that have a checksum that is different from 1180 // the mutation status the marked mutant is related to. If possible change 1181 // the relation to the correct mutation status id. 1182 foreach (m; db.markMutantApi 1183 .getMarkedMutants 1184 .map!(a => tuple(a, toChecksum(a.statusId))) 1185 .filter!(a => a[0].statusChecksum != a[1])) { 1186 update(m[0]); 1187 } 1188 } 1189 1190 /// Prints a marked mutant that has become lost due to rerun of analyze 1191 void printLostMarkings(MarkedMutant[] lostMutants) { 1192 import std.algorithm : sort; 1193 import std.array : empty; 1194 import std.conv : to; 1195 import std.stdio : writeln; 1196 1197 if (lostMutants.empty) 1198 return; 1199 1200 Table!6 tbl = Table!6([ 1201 "ID", "File", "Line", "Column", "Status", "Rationale" 1202 ]); 1203 foreach (m; lostMutants) { 1204 typeof(tbl).Row r = [ 1205 m.statusId.get.to!string, m.path, m.sloc.line.to!string, 1206 m.sloc.column.to!string, m.toStatus.to!string, m.rationale.get 1207 ]; 1208 tbl.put(r); 1209 } 1210 log.warning("Marked mutants was lost"); 1211 writeln(tbl); 1212 } 1213 1214 @("shall only let files in the diff through") 1215 unittest { 1216 import std.string : lineSplitter; 1217 import dextool.plugin.mutate.backend.diff_parser; 1218 1219 immutable lines = `diff --git a/standalone2.d b/standalone2.d 1220 index 0123..2345 100644 1221 --- a/standalone.d 1222 +++ b/standalone2.d 1223 @@ -31,7 +31,6 @@ import std.algorithm : map; 1224 import std.array : Appender, appender, array; 1225 import std.datetime : SysTime; 1226 +import std.format : format; 1227 -import std.typecons : Tuple; 1228 1229 import d2sqlite3 : sqlDatabase = Database; 1230 1231 @@ -46,7 +45,7 @@ import dextool.plugin.mutate.backend.type : Language; 1232 struct Database { 1233 import std.conv : to; 1234 import std.exception : collectException; 1235 - import std.typecons : Nullable; 1236 + import std.typecons : Nullable, Flag, No; 1237 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum; 1238 1239 + sqlDatabase db;`; 1240 1241 UnifiedDiffParser p; 1242 foreach (line; lines.lineSplitter) 1243 p.process(line); 1244 auto diff = p.result; 1245 1246 auto files = FileFilter(".".Path.AbsolutePath, true, diff); 1247 1248 files.shouldAnalyze("standalone.d".Path.AbsolutePath).shouldBeFalse; 1249 files.shouldAnalyze("standalone2.d".Path.AbsolutePath).shouldBeTrue; 1250 } 1251 1252 /// Convert to an absolute path by finding the first match among the compiler flags 1253 Optional!AbsolutePath toAbsolutePath(Path file, AbsolutePath workDir, 1254 ParseFlags.Include[] includes, SystemIncludePath[] systemIncludes) @trusted nothrow { 1255 import std.algorithm : map, filter; 1256 import std.file : exists; 1257 import std.path : buildPath; 1258 1259 Optional!AbsolutePath lookup(string dir) nothrow { 1260 const p = buildPath(dir, file); 1261 try { 1262 if (exists(p)) 1263 return some(AbsolutePath(p)); 1264 } catch (Exception e) { 1265 } 1266 return none!AbsolutePath; 1267 } 1268 1269 { 1270 auto a = lookup(workDir.toString); 1271 if (a.hasValue) 1272 return a; 1273 } 1274 1275 foreach (a; includes.map!(a => lookup(a.payload)) 1276 .filter!(a => a.hasValue)) { 1277 return a; 1278 } 1279 1280 foreach (a; systemIncludes.map!(a => lookup(a.value)) 1281 .filter!(a => a.hasValue)) { 1282 return a; 1283 } 1284 1285 return none!AbsolutePath; 1286 } 1287 1288 /** Returns: the root files that need to be re-analyzed because either them or 1289 * their dependency has changed. 1290 */ 1291 bool[Path] dependencyAnalyze(ref Database db, const bool needFullAnalyze, FilesysIO fio) @trusted { 1292 import dextool.cachetools : nullableCache; 1293 import dextool.plugin.mutate.backend.database : FileId; 1294 1295 typeof(return) rval; 1296 1297 // pessimistic. Add all as needing to be analyzed. 1298 foreach (a; db.getRootFiles.map!(a => db.getFile(a).get)) { 1299 rval[a] = false; 1300 } 1301 1302 try { 1303 auto getFileId = nullableCache!(string, FileId, (string p) => db.getFileId(p.Path))(256, 1304 30.dur!"seconds"); 1305 auto getFileName = nullableCache!(FileId, Path, (FileId id) => db.getFile(id))(256, 1306 30.dur!"seconds"); 1307 auto getFileDbChecksum = nullableCache!(string, Checksum, 1308 (string p) => db.getFileChecksum(p.Path))(256, 30.dur!"seconds"); 1309 auto getFileFsChecksum = nullableCache!(AbsolutePath, Checksum, (AbsolutePath p) { 1310 return checksum(p); 1311 })(256, 30.dur!"seconds"); 1312 1313 Checksum[Path] dbDeps; 1314 foreach (a; db.dependencyApi.getAll) 1315 dbDeps[a.file] = a.checksum; 1316 1317 bool isChanged(T)(T f) { 1318 if (needFullAnalyze) { 1319 // because the tool version is updated then all files need to 1320 // be re-analyzed. an update can mean that scheman are 1321 // improved, mutants has been changed/removed etc. it is 1322 // unknown. the only way to be sure is to re-analyze all files. 1323 return true; 1324 } 1325 1326 if (f.rootCs != getFileFsChecksum(fio.toAbsoluteRoot(f.root))) 1327 return true; 1328 1329 foreach (a; f.deps.filter!(a => getFileFsChecksum(fio.toAbsoluteRoot(a)) != dbDeps[a])) { 1330 return true; 1331 } 1332 1333 return false; 1334 } 1335 1336 foreach (f; db.getRootFiles 1337 .map!(a => db.getFile(a).get) 1338 .map!(a => tuple!("root", "rootCs", "deps")(a, 1339 getFileDbChecksum(a), db.dependencyApi.get(a))) 1340 .cache 1341 .filter!(a => isChanged(a)) 1342 .map!(a => a.root)) { 1343 rval[f] = true; 1344 } 1345 } catch (Exception e) { 1346 log.warning(e.msg); 1347 } 1348 1349 log.trace("Dependency analyze: ", rval); 1350 1351 return rval; 1352 } 1353 1354 /// Only utf-8 files are supported 1355 bool isFileSupported(FilesysIO fio, AbsolutePath p) @safe { 1356 import std.algorithm : among; 1357 import std.encoding : getBOM, BOM; 1358 1359 auto entry = fio.makeInput(p).content.getBOM(); 1360 const res = entry.schema.among(BOM.utf8, BOM.none); 1361 1362 if (res == 1) 1363 log.warningf("%s has a utf-8 BOM marker. It will make all coverage and scheman fail to compile", 1364 p); 1365 1366 return res != 0; 1367 } 1368 1369 void saveSchemaFragments(ref Database db, FilesysIO fio, 1370 ref SchemataResult.Fragments[AbsolutePath] fragments) { 1371 import std.typecons : tuple; 1372 import dextool.plugin.mutate.backend.database.type : SchemaFragmentV2, toMutationStatusId; 1373 1374 foreach (a; fragments.byKeyValue 1375 .map!(a => tuple!("fileId", 1376 "fragments")(db.getFileId(fio.toRelativeRoot(a.key)), a.value)) 1377 .filter!(a => !a.fileId.isNull)) { 1378 // TODO: SchemaFragmentV2 and SchemataResult.Fragment are pretty 1379 // similare to each other. Only CodeMutant is different. 1380 db.schemaApi.putFragments(a.fileId.get, 1381 a.fragments.fragments.map!(a => SchemaFragmentV2(a.offset, 1382 a.text, a.mutants.map!(a => a.id.toMutationStatusId).array)).array); 1383 } 1384 } 1385 1386 struct NeedFullAnalyzeResult { 1387 Checksum cs; 1388 bool status; 1389 } 1390 1391 NeedFullAnalyzeResult needFullAnalyze(ref Database db, AbsolutePath config) @safe nothrow { 1392 try { 1393 const cs = checksum(config); 1394 const prevConfigCs = db.miscApi.getConfigVersion; 1395 const status = cs != prevConfigCs 1396 || db.miscApi.isToolVersionDifferent(ToolVersion(dextoolBinaryId)); 1397 logger.tracef("Config prev:%s curr:%s status:%s", prevConfigCs.c0, cs.c0, status); 1398 return typeof(return)(cs, status); 1399 } catch (Exception e) { 1400 logger.trace(e.msg).collectException; 1401 } 1402 return typeof(return)(Checksum.init, true); 1403 }