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 logger = std.experimental.logger; 17 import std.algorithm : map, filter, joiner, cache; 18 import std.array : array, appender, empty; 19 import std.concurrency; 20 import std.datetime : dur; 21 import std.exception : collectException; 22 import std.parallelism; 23 import std.range : tee, enumerate; 24 import std.typecons; 25 26 import colorlog; 27 28 import dextool.compilation_db : CompileCommandFilter, defaultCompilerFlagFilter, 29 CompileCommandDB, SearchResult; 30 import dextool.plugin.mutate.backend.analyze.internal : Cache, TokenStream; 31 import dextool.plugin.mutate.backend.database : Database, LineMetadata, MutationPointEntry2; 32 import dextool.plugin.mutate.backend.database.type : MarkedMutant; 33 import dextool.plugin.mutate.backend.diff_parser : Diff; 34 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO; 35 import dextool.plugin.mutate.backend.report.utility : statusToString, Table; 36 import dextool.plugin.mutate.backend.utility : checksum, trustedRelativePath, 37 Checksum, getProfileResult, Profile; 38 import dextool.plugin.mutate.config : ConfigCompiler, ConfigAnalyze; 39 import dextool.set; 40 import dextool.type : ExitStatusType, AbsolutePath, Path; 41 import dextool.user_filerange; 42 43 version (unittest) { 44 import unit_threaded.assertions; 45 } 46 47 /** Analyze the files in `frange` for mutations. 48 */ 49 ExitStatusType runAnalyzer(ref Database db, ConfigAnalyze conf_analyze, 50 ConfigCompiler conf_compiler, UserFileRange frange, ValidateLoc val_loc, FilesysIO fio) @trusted { 51 import dextool.plugin.mutate.backend.diff_parser : diffFromStdin, Diff; 52 53 auto fileFilter = () { 54 try { 55 return FileFilter(fio.getOutputDir, conf_analyze.unifiedDiffFromStdin, 56 conf_analyze.unifiedDiffFromStdin ? diffFromStdin : Diff.init); 57 } catch (Exception e) { 58 logger.info(e.msg); 59 logger.warning("Unable to parse diff"); 60 } 61 return FileFilter.init; 62 }(); 63 64 auto pool = () { 65 if (conf_analyze.poolSize == 0) 66 return new TaskPool(); 67 return new TaskPool(conf_analyze.poolSize); 68 }(); 69 70 // will only be used by one thread at a time. 71 auto store = spawn(&storeActor, cast(shared)&db, cast(shared) fio.dup, 72 conf_analyze.prune, conf_analyze.fastDbStore, 73 conf_analyze.poolSize, conf_analyze.forceSaveAnalyze); 74 75 int taskCnt; 76 Set!AbsolutePath alreadyAnalyzed; 77 // dfmt off 78 foreach (f; frange.filter!(a => !a.isNull) 79 .map!(a => a.get) 80 // The tool only supports analyzing a file one time. 81 // This optimize it in some cases where the same file occurs 82 // multiple times in the compile commands database. 83 .filter!(a => a.absoluteFile !in alreadyAnalyzed) 84 .tee!(a => alreadyAnalyzed.add(a.absoluteFile)) 85 .cache 86 .filter!(a => !isPathInsideAnyRoot(conf_analyze.exclude, a.absoluteFile)) 87 .filter!(a => fileFilter.shouldAnalyze(a.absoluteFile))) { 88 try { 89 pool.put(task!analyzeActor(f, val_loc.dup, fio.dup, conf_compiler, conf_analyze, store)); 90 taskCnt++; 91 } catch (Exception e) { 92 logger.trace(e); 93 logger.warning(e.msg); 94 } 95 } 96 // dfmt on 97 98 // inform the store actor of how many analyse results it should *try* to 99 // save. 100 send(store, AnalyzeCntMsg(taskCnt)); 101 // wait for all files to be analyzed 102 pool.finish(true); 103 // wait for the store actor to finish 104 receiveOnly!StoreDoneMsg; 105 106 if (conf_analyze.profile) 107 try { 108 import std.stdio : writeln; 109 110 writeln(getProfileResult.toString); 111 } catch (Exception e) { 112 logger.warning("Unable to print the profile data: ", e.msg).collectException; 113 } 114 115 return ExitStatusType.Ok; 116 } 117 118 @safe: 119 120 /** Filter function for files. Either all or those in stdin. 121 * 122 * The matching ignores the file extension in order to lessen the problem of a 123 * file that this approach skip headers because they do not exist in 124 * `compile_commands.json`. It means that e.g. "foo.hpp" would match `true` if 125 * `foo.cpp` is in `compile_commands.json`. 126 */ 127 struct FileFilter { 128 import std.path : stripExtension; 129 130 Set!string files; 131 bool useFileFilter; 132 AbsolutePath root; 133 134 this(AbsolutePath root, bool fromStdin, Diff diff) { 135 this.root = root; 136 this.useFileFilter = fromStdin; 137 foreach (a; diff.toRange(root)) { 138 files.add(a.key.stripExtension); 139 } 140 } 141 142 bool shouldAnalyze(AbsolutePath p) { 143 import std.path : relativePath; 144 145 if (!useFileFilter) { 146 return true; 147 } 148 149 return relativePath(p, root).stripExtension in files; 150 } 151 } 152 153 /// Number of analyze tasks that has been spawned that the `storeActor` should wait for. 154 struct AnalyzeCntMsg { 155 int value; 156 } 157 158 struct StoreDoneMsg { 159 } 160 161 /// Start an analyze of a file 162 void analyzeActor(SearchResult fileToAnalyze, ValidateLoc vloc, FilesysIO fio, 163 ConfigCompiler compilerConf, ConfigAnalyze analyzeConf, Tid storeActor) @trusted nothrow { 164 auto profile = Profile("analyze file " ~ fileToAnalyze.absoluteFile); 165 166 try { 167 auto analyzer = Analyze(vloc, fio, 168 Analyze.Config(compilerConf.forceSystemIncludes, analyzeConf.mutantsPerSchema)); 169 analyzer.process(fileToAnalyze); 170 send(storeActor, cast(immutable) analyzer.result); 171 return; 172 } catch (Exception e) { 173 } 174 175 // send a dummy result 176 try { 177 send(storeActor, cast(immutable) new Analyze.Result); 178 } catch (Exception e) { 179 } 180 } 181 182 /// Store the result of the analyze. 183 void storeActor(scope shared Database* dbShared, scope shared FilesysIO fioShared, 184 const bool prune, const bool fastDbStore, const long poolSize, const bool forceSave) @trusted nothrow { 185 import cachetools : CacheLRU; 186 import dextool.cachetools : nullableCache; 187 import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut; 188 189 Database* db = cast(Database*) dbShared; 190 FilesysIO fio = cast(FilesysIO) fioShared; 191 192 // A file is at most saved one time to the database. 193 Set!AbsolutePath savedFiles; 194 195 auto getFileId = nullableCache!(string, FileId, (string p) => db.getFileId(p.Path))(256, 196 30.dur!"seconds"); 197 auto getFileDbChecksum = nullableCache!(string, Checksum, 198 (string p) => db.getFileChecksum(p.Path))(256, 30.dur!"seconds"); 199 auto getFileFsChecksum = nullableCache!(string, Checksum, (string p) { 200 return checksum(fio.makeInput(AbsolutePath(Path(p))).content[]); 201 })(256, 30.dur!"seconds"); 202 203 static struct Files { 204 Checksum[Path] value; 205 206 this(ref Database db) { 207 foreach (a; db.getDetailedFiles) { 208 value[a.file] = a.fileChecksum; 209 } 210 } 211 } 212 213 void save(immutable Analyze.Result result) { 214 // mark files that have an unchanged checksum as "already saved" 215 foreach (f; result.idFile 216 .byKey 217 .filter!(a => a !in savedFiles) 218 .filter!(a => getFileDbChecksum(fio.toRelativeRoot(a)) == getFileFsChecksum(a) 219 && !forceSave)) { 220 logger.info("Unchanged ".color(Color.yellow), f); 221 savedFiles.add(f); 222 } 223 224 // only saves mutation points to a file one time. 225 { 226 auto app = appender!(MutationPointEntry2[])(); 227 foreach (mp; result.mutationPoints 228 .map!(a => tuple!("data", "file")(a, fio.toAbsoluteRoot(a.file))) 229 .filter!(a => a.file !in savedFiles)) { 230 app.put(mp.data); 231 } 232 foreach (f; result.idFile.byKey.filter!(a => a !in savedFiles)) { 233 logger.info("Saving ".color(Color.green), f); 234 const relp = fio.toRelativeRoot(f); 235 db.removeFile(relp); 236 const info = result.infoId[result.idFile[f]]; 237 db.put(relp, info.checksum, info.language); 238 savedFiles.add(f); 239 } 240 db.put(app.data, fio.getOutputDir); 241 } 242 243 foreach (s; result.schematas.enumerate) { 244 try { 245 auto mutants = result.schemataMutants[s.index].map!( 246 a => db.getMutationStatusId(a.value)) 247 .filter!(a => !a.isNull) 248 .map!(a => a.get) 249 .array; 250 if (!mutants.empty && !s.value.empty) { 251 const id = db.putSchemata(result.schemataChecksum[s.index], s.value, mutants); 252 logger.trace(!id.isNull, "Saving schemata ", id.get.value); 253 } 254 } catch (Exception e) { 255 logger.trace(e.msg); 256 logger.warning("Unable to save schemata ", s.index).collectException; 257 } 258 } 259 260 { 261 Set!long printed; 262 auto app = appender!(LineMetadata[])(); 263 foreach (md; result.metadata) { 264 // transform the ID from local to global. 265 const fid = getFileId(fio.toRelativeRoot(result.fileId[md.id])); 266 if (fid.isNull && !printed.contains(md.id)) { 267 printed.add(md.id); 268 logger.warningf("File with suppressed mutants (// NOMUT) not in the database: %s. Skipping...", 269 result.fileId[md.id]).collectException; 270 } else if (!fid.isNull) { 271 app.put(LineMetadata(fid.get, md.line, md.attr)); 272 } 273 } 274 db.put(app.data); 275 } 276 } 277 278 // listen for results from workers until the expected number is processed. 279 void recv() { 280 auto profile = Profile("updating files"); 281 logger.info("Updating files"); 282 283 int resultCnt; 284 Nullable!int maxResults; 285 bool running = true; 286 287 while (running) { 288 try { 289 receive((AnalyzeCntMsg a) { maxResults = a.value; }, (immutable Analyze.Result a) { 290 resultCnt++; 291 save(a); 292 },); 293 } catch (Exception e) { 294 logger.trace(e).collectException; 295 logger.warning(e.msg).collectException; 296 } 297 298 if (!maxResults.isNull && resultCnt >= maxResults.get) { 299 running = false; 300 } 301 } 302 } 303 304 void pruneFiles() { 305 import std.path : buildPath; 306 307 auto profile = Profile("prune files"); 308 309 logger.info("Pruning the database of dropped files"); 310 auto files = db.getFiles.map!(a => fio.toAbsoluteRoot(a)).toSet; 311 312 foreach (f; files.setDifference(savedFiles).toRange) { 313 logger.info("Removing ".color(Color.red), f); 314 db.removeFile(fio.toRelativeRoot(f)); 315 } 316 } 317 318 void fastDbOn() { 319 if (!fastDbStore) 320 return; 321 logger.info( 322 "Turning OFF sqlite3 synchronization protection to improve the write performance"); 323 logger.warning("Do NOT interrupt dextool in any way because it may corrupt the database"); 324 db.run("PRAGMA synchronous = OFF"); 325 db.run("PRAGMA journal_mode = MEMORY"); 326 } 327 328 void fastDbOff() { 329 if (!fastDbStore) 330 return; 331 db.run("PRAGMA synchronous = ON"); 332 db.run("PRAGMA journal_mode = DELETE"); 333 } 334 335 try { 336 import dextool.plugin.mutate.backend.test_mutant.timeout : resetTimeoutContext; 337 338 // by making the mailbox size follow the number of workers the overall 339 // behavior will slow down if saving to the database is too slow. This 340 // avoids excessive or even fatal memory usage. 341 setMaxMailboxSize(thisTid, poolSize + 2, OnCrowding.block); 342 343 fastDbOn(); 344 345 auto trans = db.transaction; 346 347 // TODO: only remove those files that are modified. 348 logger.info("Removing metadata"); 349 db.clearMetadata; 350 351 recv(); 352 353 // TODO: print what files has been updated. 354 logger.info("Resetting timeout context"); 355 resetTimeoutContext(*db); 356 357 logger.info("Updating metadata"); 358 db.updateMetadata; 359 360 if (prune) { 361 pruneFiles(); 362 { 363 auto profile = Profile("remove orphaned mutants"); 364 logger.info("Removing orphaned mutants"); 365 db.removeOrphanedMutants; 366 } 367 { 368 auto profile = Profile("prune schematas"); 369 logger.info("Prune schematas"); 370 db.pruneSchemas; 371 } 372 } 373 374 logger.info("Updating manually marked mutants"); 375 updateMarkedMutants(*db); 376 printLostMarkings(db.getLostMarkings); 377 378 logger.info("Committing changes"); 379 trans.commit; 380 logger.info("Ok".color(Color.green)); 381 382 fastDbOff(); 383 } catch (Exception e) { 384 logger.error(e.msg).collectException; 385 logger.error("Failed to save the result of the analyze to the database").collectException; 386 } 387 388 try { 389 send(ownerTid, StoreDoneMsg.init); 390 } catch (Exception e) { 391 logger.errorf("Fatal error. Unable to send %s to the main thread", 392 StoreDoneMsg.init).collectException; 393 } 394 } 395 396 /// Analyze a file for mutants. 397 struct Analyze { 398 import std.regex : Regex, regex, matchFirst; 399 import std.typecons : Yes; 400 import cpptooling.analyzer.clang.context : ClangContext; 401 import cpptooling.utility.virtualfilesystem; 402 import dextool.compilation_db : SearchResult; 403 import dextool.type : Exists, makeExists; 404 405 static struct Config { 406 bool forceSystemIncludes; 407 long mutantsPerSchema; 408 } 409 410 private { 411 static immutable raw_re_nomut = `^((//)|(/\*))\s*NOMUT\s*(\((?P<tag>.*)\))?\s*((?P<comment>.*)\*/|(?P<comment>.*))?`; 412 413 Regex!char re_nomut; 414 415 ValidateLoc val_loc; 416 FilesysIO fio; 417 bool forceSystemIncludes; 418 419 Cache cache; 420 421 Result result; 422 423 Config conf; 424 } 425 426 this(ValidateLoc val_loc, FilesysIO fio, Config conf) @trusted { 427 this.val_loc = val_loc; 428 this.fio = fio; 429 this.cache = new Cache; 430 this.re_nomut = regex(raw_re_nomut); 431 this.forceSystemIncludes = forceSystemIncludes; 432 this.result = new Result; 433 this.conf = conf; 434 } 435 436 void process(SearchResult in_file) @safe { 437 in_file.flags.forceSystemIncludes = conf.forceSystemIncludes; 438 439 // find the file and flags to analyze 440 Exists!AbsolutePath checked_in_file; 441 try { 442 checked_in_file = makeExists(in_file.absoluteFile); 443 } catch (Exception e) { 444 logger.warning(e.msg); 445 return; 446 } 447 448 try { 449 () @trusted { 450 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 451 auto tstream = new TokenStreamImpl(ctx); 452 453 analyzeForMutants(in_file, checked_in_file, ctx, tstream); 454 // TODO: filter files so they are only analyzed once for comments 455 foreach (f; result.fileId.byValue) 456 analyzeForComments(f, tstream); 457 }(); 458 } catch (Exception e) { 459 () @trusted { logger.trace(e); }(); 460 logger.info(e.msg); 461 logger.error("failed analyze of ", in_file).collectException; 462 } 463 } 464 465 void analyzeForMutants(SearchResult in_file, 466 Exists!AbsolutePath checked_in_file, ref ClangContext ctx, TokenStream tstream) @safe { 467 import dextool.plugin.mutate.backend.analyze.pass_clang; 468 import dextool.plugin.mutate.backend.analyze.pass_mutant; 469 import dextool.plugin.mutate.backend.analyze.pass_schemata; 470 import cpptooling.analyzer.clang.check_parse_result : hasParseErrors, logDiagnostic; 471 472 logger.infof("Analyzing %s", checked_in_file); 473 auto tu = ctx.makeTranslationUnit(checked_in_file, in_file.flags.completeFlags); 474 if (tu.hasParseErrors) { 475 logDiagnostic(tu); 476 logger.errorf("Compile error in %s. Skipping", checked_in_file); 477 return; 478 } 479 480 auto ast = toMutateAst(tu.cursor, fio); 481 debug logger.trace(ast); 482 auto mutants = toMutants(ast, fio, val_loc); 483 484 debug logger.trace(mutants); 485 auto codeMutants = toCodeMutants(mutants, fio, tstream); 486 debug logger.trace(codeMutants); 487 () @trusted { .destroy(mutants); }(); 488 489 auto schemas = toSchemata(ast, fio, codeMutants, conf.mutantsPerSchema); 490 debug logger.trace(schemas); 491 foreach (f; schemas.getSchematas.filter!(a => !(a.fragments.empty || a.mutants.empty))) { 492 const id = result.schematas.length; 493 result.schematas ~= f.fragments; 494 result.schemataMutants[id] = f.mutants.map!(a => a.id).array; 495 result.schemataChecksum[id] = f.checksum; 496 } 497 () @trusted { .destroy(schemas); }(); 498 499 .destroy(ast); 500 501 result.mutationPoints = codeMutants.points.byKeyValue.map!( 502 a => a.value.map!(b => MutationPointEntry2(fio.toRelativeRoot(a.key), 503 b.offset, b.sloc.begin, b.sloc.end, b.mutants))).joiner.array; 504 foreach (f; codeMutants.points.byKey) { 505 const id = result.idFile.length; 506 result.idFile[f] = id; 507 result.fileId[id] = f; 508 result.infoId[id] = Result.FileInfo(codeMutants.csFiles[f], codeMutants.lang); 509 } 510 511 () @trusted { .destroy(codeMutants); .destroy(schemas); }(); 512 } 513 514 /** Tokens are always from the same file. 515 * 516 * TODO: move this to pass_clang. 517 */ 518 void analyzeForComments(AbsolutePath file, TokenStream tstream) @trusted { 519 import std.algorithm : filter; 520 import clang.c.Index : CXTokenKind; 521 import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut; 522 523 const fid = result.idFile.require(file, result.fileId.length).FileId; 524 525 auto mdata = appender!(LineMetadata[])(); 526 foreach (t; cache.getTokens(AbsolutePath(file), tstream) 527 .filter!(a => a.kind == CXTokenKind.comment)) { 528 auto m = matchFirst(t.spelling, re_nomut); 529 if (m.whichPattern == 0) 530 continue; 531 532 mdata.put(LineMetadata(fid, t.loc.line, LineAttr(NoMut(m["tag"], m["comment"])))); 533 logger.tracef("NOMUT found at %s:%s:%s", file, t.loc.line, t.loc.column); 534 } 535 536 result.metadata ~= mdata.data; 537 } 538 539 static class Result { 540 import dextool.plugin.mutate.backend.type : Language, CodeChecksum, SchemataChecksum; 541 import dextool.plugin.mutate.backend.database.type : SchemataFragment; 542 543 MutationPointEntry2[] mutationPoints; 544 545 static struct FileInfo { 546 Checksum checksum; 547 Language language; 548 } 549 550 /// The key is the ID from idFile. 551 FileInfo[ulong] infoId; 552 553 /// The IDs is unique for *this* analyze, not globally. 554 long[AbsolutePath] idFile; 555 AbsolutePath[long] fileId; 556 557 // The FileID used in the metadata is local to this analysis. It has to 558 // be remapped when added to the database. 559 LineMetadata[] metadata; 560 561 /// Mutant schematas that has been generated. 562 SchemataFragment[][] schematas; 563 /// the mutants that are associated with a schemata. 564 CodeChecksum[][long] schemataMutants; 565 /// checksum for the schemata 566 SchemataChecksum[long] schemataChecksum; 567 } 568 } 569 570 @( 571 "shall extract the tag and comment from the input following the pattern NOMUT with optional tag and comment") 572 unittest { 573 import std.regex : regex, matchFirst; 574 import unit_threaded.runner.io : writelnUt; 575 576 auto re_nomut = regex(Analyze.raw_re_nomut); 577 // NOMUT in other type of comments should NOT match. 578 matchFirst("/// NOMUT", re_nomut).whichPattern.shouldEqual(0); 579 matchFirst("// stuff with NOMUT in it", re_nomut).whichPattern.shouldEqual(0); 580 matchFirst("/** NOMUT*/", re_nomut).whichPattern.shouldEqual(0); 581 matchFirst("/* stuff with NOMUT in it */", re_nomut).whichPattern.shouldEqual(0); 582 583 matchFirst("/*NOMUT*/", re_nomut).whichPattern.shouldEqual(1); 584 matchFirst("/*NOMUT*/", re_nomut)["comment"].shouldEqual(""); 585 matchFirst("//NOMUT", re_nomut).whichPattern.shouldEqual(1); 586 matchFirst("// NOMUT", re_nomut).whichPattern.shouldEqual(1); 587 matchFirst("// NOMUT (arch)", re_nomut)["tag"].shouldEqual("arch"); 588 matchFirst("// NOMUT smurf", re_nomut)["comment"].shouldEqual("smurf"); 589 auto m = matchFirst("// NOMUT (arch) smurf", re_nomut); 590 m["tag"].shouldEqual("arch"); 591 m["comment"].shouldEqual("smurf"); 592 } 593 594 /// Stream of tokens excluding comment tokens. 595 class TokenStreamImpl : TokenStream { 596 import cpptooling.analyzer.clang.context : ClangContext; 597 import dextool.plugin.mutate.backend.type : Token; 598 import dextool.plugin.mutate.backend.utility : tokenize; 599 600 ClangContext* ctx; 601 602 /// The context must outlive any instance of this class. 603 // TODO remove @trusted when upgrading to dmd-fe 2.091.0+ and activate dip25 + 1000 604 this(ref ClangContext ctx) @trusted { 605 this.ctx = &ctx; 606 } 607 608 Token[] getTokens(Path p) { 609 return tokenize(*ctx, p); 610 } 611 612 Token[] getFilteredTokens(Path p) { 613 import clang.c.Index : CXTokenKind; 614 615 // Filter a stream of tokens for those that should affect the checksum. 616 return tokenize(*ctx, p).filter!(a => a.kind != CXTokenKind.comment).array; 617 } 618 } 619 620 /// Returns: true if `f` is inside any `roots`. 621 bool isPathInsideAnyRoot(AbsolutePath[] roots, AbsolutePath f) @safe { 622 import dextool.utility : isPathInsideRoot; 623 624 foreach (root; roots) { 625 if (isPathInsideRoot(root, f)) 626 return true; 627 } 628 629 return false; 630 } 631 632 /** Update the connection between the marked mutants and their mutation status 633 * id and mutation id. 634 */ 635 void updateMarkedMutants(ref Database db) { 636 import dextool.plugin.mutate.backend.database.type : MutationStatusId; 637 638 void update(MarkedMutant m) { 639 const stId = db.getMutationStatusId(m.statusChecksum); 640 if (stId.isNull) 641 return; 642 const mutId = db.getMutationId(stId.get); 643 if (mutId.isNull) 644 return; 645 db.removeMarkedMutant(m.statusChecksum); 646 db.markMutant(mutId.get, m.path, m.sloc, stId.get, m.statusChecksum, 647 m.toStatus, m.rationale, m.mutText); 648 db.updateMutationStatus(stId.get, m.toStatus); 649 } 650 651 // find those marked mutants that have a checksum that is different from 652 // the mutation status the marked mutant is related to. If possible change 653 // the relation to the correct mutation status id. 654 foreach (m; db.getMarkedMutants 655 .map!(a => tuple(a, db.getChecksum(a.statusId))) 656 .filter!(a => !a[1].isNull) 657 .filter!(a => a[0].statusChecksum != a[1].get)) { 658 update(m[0]); 659 } 660 } 661 662 /// Prints a marked mutant that has become lost due to rerun of analyze 663 void printLostMarkings(MarkedMutant[] lostMutants) { 664 import std.algorithm : sort; 665 import std.array : empty; 666 import std.conv : to; 667 import std.stdio : writeln; 668 669 if (lostMutants.empty) 670 return; 671 672 Table!6 tbl = Table!6([ 673 "ID", "File", "Line", "Column", "Status", "Rationale" 674 ]); 675 foreach (m; lostMutants) { 676 typeof(tbl).Row r = [ 677 m.mutationId.to!string, m.path, m.sloc.line.to!string, 678 m.sloc.column.to!string, m.toStatus.to!string, m.rationale 679 ]; 680 tbl.put(r); 681 } 682 logger.warning("Marked mutants was lost"); 683 writeln(tbl); 684 } 685 686 @("shall only let files in the diff through") 687 unittest { 688 import std.string : lineSplitter; 689 import dextool.plugin.mutate.backend.diff_parser; 690 691 immutable lines = `diff --git a/standalone2.d b/standalone2.d 692 index 0123..2345 100644 693 --- a/standalone.d 694 +++ b/standalone2.d 695 @@ -31,7 +31,6 @@ import std.algorithm : map; 696 import std.array : Appender, appender, array; 697 import std.datetime : SysTime; 698 +import std.format : format; 699 -import std.typecons : Tuple; 700 701 import d2sqlite3 : sqlDatabase = Database; 702 703 @@ -46,7 +45,7 @@ import dextool.plugin.mutate.backend.type : Language; 704 struct Database { 705 import std.conv : to; 706 import std.exception : collectException; 707 - import std.typecons : Nullable; 708 + import std.typecons : Nullable, Flag, No; 709 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum; 710 711 + sqlDatabase db;`; 712 713 UnifiedDiffParser p; 714 foreach (line; lines.lineSplitter) 715 p.process(line); 716 auto diff = p.result; 717 718 auto files = FileFilter(".".Path.AbsolutePath, true, diff); 719 720 files.shouldAnalyze("standalone.d".Path.AbsolutePath).shouldBeFalse; 721 files.shouldAnalyze("standalone2.d".Path.AbsolutePath).shouldBeTrue; 722 }