1 /** 2 Copyright: Copyright (c) 2018, 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.report.html; 11 12 import logger = std.experimental.logger; 13 import std.exception : collectException; 14 import std.format : format; 15 import std.stdio : File; 16 17 import arsd.dom : Document, Element, require, Table, RawSource; 18 19 import dextool.plugin.mutate.backend.database : Database, FileRow, FileMutantRow, MutationId; 20 import dextool.plugin.mutate.backend.diff_parser : Diff; 21 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 22 import dextool.plugin.mutate.backend.report.type : FileReport, FilesReporter; 23 import dextool.plugin.mutate.backend.report.utility : toSections; 24 import dextool.plugin.mutate.backend.type : Mutation, Offset, SourceLoc, Token; 25 import dextool.plugin.mutate.config : ConfigReport; 26 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportLevel, ReportSection; 27 import dextool.type : AbsolutePath, Path, DirName; 28 29 import dextool.plugin.mutate.backend.report.html.constants; 30 import dextool.plugin.mutate.backend.report.html.js; 31 import dextool.plugin.mutate.backend.report.html.tmpl; 32 33 version (unittest) { 34 import unit_threaded : shouldEqual; 35 } 36 37 struct FileIndex { 38 Path path; 39 string display; 40 41 long aliveMutants; 42 long killedMutants; 43 long totalMutants; 44 // Nr of mutants that are alive but tagged with nomut. 45 long aliveNoMut; 46 } 47 48 @safe final class ReportHtml : FileReport, FilesReporter { 49 import std.array : Appender; 50 import std.stdio : File, writefln, writeln; 51 import std.xml : encode; 52 import dextool.set; 53 54 const Mutation.Kind[] kinds; 55 const ConfigReport conf; 56 57 /// The base directory of logdirs 58 const AbsolutePath logDir; 59 /// Reports for each file 60 const AbsolutePath logFilesDir; 61 62 /// What the user configured. 63 MutationKind[] humanReadableKinds; 64 Set!ReportSection sections; 65 66 FilesysIO fio; 67 68 // all files that have been produced. 69 Appender!(FileIndex[]) files; 70 71 // the context for the file that is currently being processed. 72 FileCtx ctx; 73 74 // Report alive mutants in this section 75 Diff diff; 76 77 this(const(Mutation.Kind)[] kinds, const ConfigReport conf, FilesysIO fio, ref Diff diff) { 78 import std.path : buildPath; 79 80 this.kinds = kinds; 81 this.fio = fio; 82 this.conf = conf; 83 this.logDir = buildPath(conf.logDir, htmlDir).Path.AbsolutePath; 84 this.logFilesDir = buildPath(this.logDir, htmlFileDir).Path.AbsolutePath; 85 this.diff = diff; 86 87 sections = (conf.reportSection.length == 0 ? conf.reportLevel.toSections 88 : conf.reportSection.dup).toSet; 89 } 90 91 override void mutationKindEvent(const MutationKind[] k) { 92 import std.file : mkdirRecurse; 93 94 humanReadableKinds = k.dup; 95 mkdirRecurse(this.logDir); 96 mkdirRecurse(this.logFilesDir); 97 } 98 99 override FileReport getFileReportEvent(ref Database db, const ref FileRow fr) { 100 import std.path : buildPath; 101 import std.stdio : File; 102 import dextool.plugin.mutate.backend.report.html.page_files; 103 import dextool.plugin.mutate.backend.report.analyzers : reportStatistics; 104 105 const original = fr.file.dup.pathToHtml; 106 const report = (original ~ htmlExt).Path; 107 108 auto stat = reportStatistics(db, kinds, fr.file); 109 110 files.put(FileIndex(report, fr.file, stat.alive, 111 stat.killed + stat.timeout + stat.aliveNoMut, stat.total, stat.aliveNoMut)); 112 113 const out_path = buildPath(logFilesDir, report).Path.AbsolutePath; 114 115 auto raw = fio.makeInput(AbsolutePath(fr.file, DirName(fio.getOutputDir))); 116 117 auto tc_info = db.getAllTestCaseInfo2(fr.id, kinds); 118 119 ctx = FileCtx.make(original, fr.id, raw, tc_info); 120 ctx.processFile = fr.file; 121 ctx.out_ = File(out_path, "w"); 122 ctx.span = Spanner(tokenize(fio.getOutputDir, fr.file)); 123 124 return this; 125 } 126 127 override void fileMutantEvent(const ref FileMutantRow fr) { 128 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 129 130 // TODO unnecessary to create the mutation text here. 131 // Move it to endFileEvent. This is inefficient. 132 133 // the mutation text has been found to contain '\0' characters when the 134 // mutant span multiple lines. These null characters render badly in 135 // the html report. 136 static string cleanup(const(char)[] raw) @safe nothrow { 137 import std.algorithm : filter; 138 import std.array : array; 139 import std.utf : byChar; 140 141 return raw.byChar.filter!(a => a != '\0').array.idup; 142 } 143 144 auto txt = makeMutationText(ctx.raw, fr.mutationPoint.offset, fr.mutation.kind, fr.lang); 145 ctx.span.put(FileMutant(fr.id, fr.mutationPoint.offset, 146 cleanup(txt.original), cleanup(txt.mutation), fr.mutation)); 147 } 148 149 override void endFileEvent(ref Database db) @trusted { 150 import std.algorithm : max, each, map, min, canFind, sort, filter; 151 import std.array : appender, empty; 152 import std.conv : to; 153 import std.range : repeat, enumerate; 154 import std.traits : EnumMembers; 155 import dextool.plugin.mutate.type : MutationKind; 156 import dextool.plugin.mutate.backend.database.type : MutantMetaData; 157 import dextool.plugin.mutate.backend.report.utility : window; 158 import dextool.plugin.mutate.backend.mutation_type : toUser; 159 160 static struct MData { 161 MutationId id; 162 FileMutant.Text txt; 163 Mutation mut; 164 MutantMetaData metaData; 165 } 166 167 auto root = ctx.doc.mainBody; 168 auto lines = root.addChild("table").setAttribute("id", "locs"); 169 auto line = lines.addChild("tr").addChild("td").setAttribute("id", "loc-1"); 170 line.addClass("loc"); 171 172 line.addChild("span", "1:").addClass("line_nr"); 173 auto mut_data = "var g_muts_data = {};\n"; 174 mut_data ~= "g_muts_data[-1] = {'kind' : null, 'status' : null, 'testCases' : null, 'orgText' : null, 'mutText' : null, 'meta' : null};\n"; 175 176 // used to make sure that metadata about a mutant is only written onces 177 // to the global arrays. 178 Set!MutationId ids; 179 auto muts = appender!(MData[])(); 180 181 // this is the last location. It is used to calculate the num of 182 // newlines, detect when a line changes etc. 183 auto lastLoc = SourceLoc(1, 1); 184 185 foreach (const s; ctx.span.toRange) { 186 if (s.tok.loc.line > lastLoc.line) { 187 lastLoc.column = 1; 188 } 189 auto meta = MetaSpan(s.muts); 190 191 foreach (const i; 0 .. max(0, s.tok.loc.line - lastLoc.line)) { 192 with (line = lines.addChild("tr").addChild("td")) { 193 setAttribute("id", format("%s-%s", "loc", lastLoc.line + i + 1)); 194 addClass("loc"); 195 addChild("span", format("%s:", lastLoc.line + i + 1)).addClass("line_nr"); 196 } 197 // force a newline in the generated html to improve readability 198 lines.appendText("\n"); 199 } 200 201 const spaces = max(0, s.tok.loc.column - lastLoc.column); 202 line.addChild(new RawSource(ctx.doc, format("%-(%s%)", " ".repeat(spaces)))); 203 auto d0 = line.addChild("div").setAttribute("style", "display: inline;"); 204 with (d0.addChild("span", s.tok.spelling)) { 205 addClass("original"); 206 addClass(s.tok.toName); 207 if (auto v = meta.status.toVisible) 208 addClass(v); 209 if (s.muts.length != 0) 210 addClass(format("%(mutid%s %)", s.muts.map!(a => a.id))); 211 if (meta.onClick.length != 0) 212 setAttribute("onclick", meta.onClick); 213 } 214 215 foreach (m; s.muts.filter!(m => !ids.contains(m.id))) { 216 ids.add(m.id); 217 218 muts.put(MData(m.id, m.txt, m.mut, db.getMutantationMetaData(m.id))); 219 with (d0.addChild("span", m.mutation)) { 220 addClass("mutant"); 221 addClass(s.tok.toName); 222 setAttribute("id", m.id.to!string); 223 } 224 d0.addChild("a").setAttribute("href", "#" ~ m.id.to!string); 225 226 auto testCases = ctx.getTestCaseInfo(m.id); 227 if (testCases.empty) { 228 mut_data ~= format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : null, 'orgText' : '%s', 'mutText' : '%s', 'meta' : '%s'};\n", 229 m.id, m.mut.kind.to!int, toUser(m.mut.kind) 230 .to!int, m.mut.status.to!ubyte, window(m.txt.original), 231 window(m.txt.mutation), db.getMutantationMetaData(m.id).kindToString); 232 } else { 233 mut_data ~= format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : [%('%s',%)'], 'orgText' : '%s', 'mutText' : '%s', 'meta' : '%s'};\n", 234 m.id, m.mut.kind.to!int, toUser(m.mut.kind) 235 .to!int, m.mut.status.to!ubyte, 236 testCases.map!(a => a.name), window(m.txt.original), 237 window(m.txt.mutation), db.getMutantationMetaData(m.id).kindToString); 238 } 239 } 240 lastLoc = s.tok.locEnd; 241 } 242 243 // make sure there is a newline before the script start to improve 244 // readability of the html document source. 245 root.appendText("\n"); 246 247 with (root.addChild("script")) { 248 // force a newline in the generated html to improve readability 249 appendText("\n"); 250 addChild(new RawSource(ctx.doc, format("const MAX_NUM_TESTCASES = %s;", 251 db.getDetectedTestCases().length))); 252 appendText("\n"); 253 addChild(new RawSource(ctx.doc, format("const g_mutids = [%(%s,%)];", 254 muts.data.map!(a => a.id)))); 255 appendText("\n"); 256 addChild(new RawSource(ctx.doc, format("const g_mut_st_map = [%('%s',%)'];", 257 [EnumMembers!(Mutation.Status)]))); 258 appendText("\n"); 259 addChild(new RawSource(ctx.doc, format("const g_mut_kind_map = [%('%s',%)'];", 260 [EnumMembers!(Mutation.Kind)]))); 261 appendText("\n"); 262 addChild(new RawSource(ctx.doc, format("const g_mut_kindGroup_map = [%('%s',%)'];", 263 [EnumMembers!(MutationKind)]))); 264 appendText("\n"); 265 266 // Creates a list of number of kills per testcase. 267 appendChild(new RawSource(ctx.doc, "var g_testcases_kills = {}")); 268 appendText("\n"); 269 foreach (tc; ctx.testCases) { 270 appendChild(new RawSource(ctx.doc, 271 format("g_testcases_kills['%s'] = [%s];", tc.name, tc.killed))); 272 appendText("\n"); 273 } 274 appendChild(new RawSource(ctx.doc, mut_data)); 275 appendText("\n"); 276 } 277 278 try { 279 ctx.out_.write(ctx.doc.toString); 280 } catch (Exception e) { 281 logger.error(e.msg).collectException; 282 logger.error("Unable to generate a HTML report for ", ctx.processFile).collectException; 283 } 284 } 285 286 override void postProcessEvent(ref Database db) @trusted { 287 import std.datetime : Clock; 288 import std.path : buildPath, baseName; 289 import dextool.plugin.mutate.backend.report.html.page_long_term_view; 290 import dextool.plugin.mutate.backend.report.html.page_minimal_set; 291 import dextool.plugin.mutate.backend.report.html.page_nomut; 292 import dextool.plugin.mutate.backend.report.html.page_diff; 293 import dextool.plugin.mutate.backend.report.html.page_stats; 294 import dextool.plugin.mutate.backend.report.html.page_test_case_similarity; 295 import dextool.plugin.mutate.backend.report.html.page_test_case_unique; 296 import dextool.plugin.mutate.backend.report.html.page_test_group_similarity; 297 import dextool.plugin.mutate.backend.report.html.page_test_groups; 298 import dextool.plugin.mutate.backend.report.html.page_tree_map; 299 300 auto index = tmplBasicPage; 301 index.title = format("Mutation Testing Report %(%s %) %s", 302 humanReadableKinds, Clock.currTime); 303 auto s = index.root.childElements("head")[0].addChild("script"); 304 s.addChild(new RawSource(index, js_index)); 305 306 void addSubPage(Fn)(Fn fn, string name, string link_txt) { 307 import std.functional : unaryFun; 308 309 const fname = buildPath(logDir, name ~ htmlExt); 310 index.mainBody.addChild("p").addChild("a", link_txt).href = fname.baseName; 311 logger.infof("Generating %s (%s)", link_txt, name); 312 File(fname, "w").write(fn()); 313 } 314 315 addSubPage(() => makeStats(db, conf, humanReadableKinds, kinds), "stats", "Statistics"); 316 if (!diff.empty) { 317 addSubPage(() => makeDiffView(db, conf, humanReadableKinds, kinds, 318 diff, fio.getOutputDir), "diff_view", "Diff View"); 319 } 320 addSubPage(() => makeLongTermView(db, conf, humanReadableKinds, kinds), 321 "long_term_view", "Long Term View"); 322 if (ReportSection.treemap in sections) { 323 addSubPage(() => makeTreeMapPage(files.data), "tree_map", "Treemap"); 324 } 325 if (ReportSection.tc_groups in sections) { 326 addSubPage(() => makeTestGroups(db, conf, humanReadableKinds, 327 kinds), "test_groups", "Test Groups"); 328 } 329 addSubPage(() => makeNomut(db, conf, humanReadableKinds, kinds), "nomut", "NoMut Details"); 330 if (ReportSection.tc_min_set in sections) { 331 addSubPage(() => makeMinimalSetAnalyse(db, conf, humanReadableKinds, 332 kinds), "minimal_set", "Minimal Test Set"); 333 } 334 if (ReportSection.tc_similarity in sections) { 335 addSubPage(() => makeTestCaseSimilarityAnalyse(db, conf, humanReadableKinds, 336 kinds), "test_case_similarity", "Test Case Similarity"); 337 } 338 if (ReportSection.tc_groups_similarity in sections) { 339 addSubPage(() => makeTestGroupSimilarityAnalyse(db, conf, humanReadableKinds, 340 kinds), "test_group_similarity", "Test Group Similarity"); 341 } 342 if (ReportSection.tc_unique in sections) { 343 addSubPage(() => makeTestCaseUnique(db, conf, humanReadableKinds, 344 kinds), "test_case_unique", "Test Case Uniqueness"); 345 } 346 347 files.data.toIndex(index.mainBody, htmlFileDir); 348 File(buildPath(logDir, "index" ~ htmlExt), "w").write(index.toPrettyString); 349 } 350 351 override void endEvent(ref Database) { 352 } 353 } 354 355 @safe: 356 private: 357 358 string toJson(string s) { 359 import std.json : JSONValue; 360 361 return JSONValue(s).toString; 362 } 363 364 struct FileCtx { 365 import std.stdio : File; 366 import blob_model : Blob; 367 import dextool.plugin.mutate.backend.database : FileId, TestCaseInfo2; 368 369 Path processFile; 370 File out_; 371 372 Spanner span; 373 374 Document doc; 375 376 // The text of the current file that is being processed. 377 Blob raw; 378 379 /// Database ID for this file. 380 FileId fileId; 381 382 /// Find the test cases that killed a mutant. They are sorted by most killed -> least killed. 383 TestCaseInfo[][MutationId] tcKilledMutant; 384 385 /// All test cases in the file. 386 TestCaseInfo[] testCases; 387 388 static FileCtx make(string title, FileId id, Blob raw, TestCaseInfo2[] tc_info) @trusted { 389 import std.algorithm : sort; 390 import std.array : array; 391 import dextool.plugin.mutate.backend.report.html.js; 392 import dextool.plugin.mutate.backend.report.html.tmpl; 393 394 auto r = FileCtx.init; 395 r.doc = tmplBasicPage; 396 r.doc.title = title; 397 r.doc.mainBody.setAttribute("onload", "javascript:init();"); 398 399 auto s = r.doc.root.childElements("head")[0].addChild("style"); 400 s.addChild(new RawSource(r.doc, tmplIndexStyle)); 401 402 s = r.doc.root.childElements("head")[0].addChild("script"); 403 s.addChild(new RawSource(r.doc, js_source)); 404 405 r.doc.mainBody.appendHtml(tmplIndexBody); 406 407 r.fileId = id; 408 409 r.raw = raw; 410 411 typeof(tcKilledMutant) tmp; 412 foreach (a; tc_info) { 413 foreach (mut; a.killed) { 414 if (auto v = mut in tmp) { 415 *v ~= TestCaseInfo(a.name, a.killed.length); 416 } else { 417 tmp[mut] = [TestCaseInfo(a.name, a.killed.length)]; 418 } 419 } 420 r.testCases ~= TestCaseInfo(a.name, a.killed.length); 421 } 422 foreach (kv; tmp.byKeyValue) { 423 r.tcKilledMutant[kv.key] = kv.value.sort.array; 424 } 425 426 return r; 427 } 428 429 TestCaseInfo[] getTestCaseInfo(MutationId mutationId) @safe pure nothrow { 430 if (auto v = mutationId in tcKilledMutant) 431 return *v; 432 return null; 433 } 434 435 static struct TestCaseInfo { 436 import dextool.plugin.mutate.backend.type : TestCase; 437 438 TestCase name; 439 long killed; 440 441 int opCmp(ref const typeof(this) s) @safe pure nothrow const @nogc scope { 442 if (killed < s.killed) 443 return -1; 444 else if (killed > s.killed) 445 return 1; 446 else if (name < s.name) 447 return -1; 448 else if (name > s.name) 449 return 1; 450 return 0; 451 } 452 453 bool opEquals(ref const typeof(this) s) @safe pure nothrow const @nogc scope { 454 return name == s.name; 455 } 456 457 size_t toHash() @safe nothrow const { 458 return name.toHash; 459 } 460 } 461 } 462 463 auto tokenize(AbsolutePath base_dir, Path f) @trusted { 464 import std.path : buildPath; 465 import std.typecons : Yes; 466 import cpptooling.analyzer.clang.context; 467 static import dextool.plugin.mutate.backend.utility; 468 469 const fpath = buildPath(base_dir, f).Path.AbsolutePath; 470 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 471 return dextool.plugin.mutate.backend.utility.tokenize!(Yes.splitMultiLineTokens)(ctx, fpath); 472 } 473 474 struct FileMutant { 475 nothrow: 476 static struct Text { 477 /// the original text that covers the offset. 478 string original; 479 /// The mutation text that covers the offset. 480 string mutation; 481 } 482 483 MutationId id; 484 Offset offset; 485 Text txt; 486 Mutation mut; 487 488 this(MutationId id, Offset offset, string original, string mutation, Mutation mut) { 489 import std.utf : validate; 490 import dextool.plugin.mutate.backend.type : invalidUtf8; 491 492 this.id = id; 493 this.offset = offset; 494 this.mut = mut; 495 496 try { 497 validate(original); 498 this.txt.original = original; 499 } catch (Exception e) { 500 this.txt.original = invalidUtf8; 501 } 502 503 try { 504 validate(mutation); 505 // users prefer being able to see what has been removed. 506 if (mutation.length == 0) 507 this.txt.mutation = "/* " ~ this.txt.original ~ " */"; 508 else 509 this.txt.mutation = mutation; 510 } catch (Exception e) { 511 this.txt.mutation = invalidUtf8; 512 } 513 } 514 515 this(MutationId id, Offset offset, string original) { 516 this(id, offset, original, null, Mutation.init); 517 } 518 519 string original() @safe pure nothrow const @nogc { 520 return txt.original; 521 } 522 523 string mutation() @safe pure nothrow const @nogc { 524 return txt.mutation; 525 } 526 527 int opCmp(ref const typeof(this) s) const @safe { 528 if (offset.begin > s.offset.begin) 529 return 1; 530 if (offset.begin < s.offset.begin) 531 return -1; 532 if (offset.end > s.offset.end) 533 return 1; 534 if (offset.end < s.offset.end) 535 return -1; 536 return 0; 537 } 538 } 539 540 @("shall be possible to construct a FileMutant in @safe") 541 @safe unittest { 542 auto fmut = FileMutant(MutationId(1), Offset(1, 2), "smurf"); 543 } 544 545 /* 546 I get a mutant that have a start/end offset. 547 I have all tokens. 548 I can't write the html before I have all mutants for the offset. 549 Hmm potentially this mean that I can't write any html until I have analyzed all mutants for the file. 550 This must be so.... 551 552 How to do it? 553 554 From reading https://stackoverflow.com/questions/11389627/span-overlapping-strings-in-a-paragraph 555 it seems that generating a <span..> for each token with multiple classes in them. A class for each mutant. 556 then they can be toggled on/off. 557 558 a <href> tag to the beginning to jump to the mutant. 559 */ 560 561 /** Provide an interface to travers the tokens and get the overlapping mutants. 562 */ 563 struct Spanner { 564 import std.container : RedBlackTree, redBlackTree; 565 import std.range : isOutputRange; 566 567 alias BTree(T) = RedBlackTree!(T, "a < b", true); 568 569 BTree!Token tokens; 570 BTree!FileMutant muts; 571 572 this(Token[] tokens) @trusted { 573 this.tokens = new typeof(this.tokens); 574 this.muts = new typeof(this.muts)(); 575 576 this.tokens.insert(tokens); 577 } 578 579 void put(const FileMutant fm) @trusted { 580 muts.insert(fm); 581 } 582 583 SpannerRange toRange() @safe { 584 return SpannerRange(tokens, muts); 585 } 586 587 string toString() @safe pure const { 588 import std.array : appender; 589 590 auto buf = appender!string; 591 this.toString(buf); 592 return buf.data; 593 } 594 595 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 596 import std.format : formattedWrite; 597 import std.range : zip, StoppingPolicy; 598 import std.string; 599 import std.algorithm : max; 600 import std.traits : Unqual; 601 602 ulong sz; 603 604 foreach (ref const t; zip(StoppingPolicy.longest, tokens[], muts[])) { 605 auto c0 = format("%s", cast(Unqual!(typeof(t[0]))) t[0]); 606 string c1; 607 if (t[1] != typeof(t[1]).init) 608 c1 = format("%s", cast(Unqual!(typeof(t[1]))) t[1]); 609 sz = max(sz, c0.length, c1.length); 610 formattedWrite(w, "%s | %s\n", c0.rightJustify(sz), c1); 611 } 612 } 613 } 614 615 @("shall be possible to construct a Spanner in @safe") 616 @safe unittest { 617 import std.algorithm; 618 import std.conv; 619 import std.range; 620 import clang.c.Index : CXTokenKind; 621 622 auto toks = zip(iota(10), iota(10, 20)).map!(a => Token(CXTokenKind.comment, 623 Offset(a[0], a[1]), SourceLoc.init, SourceLoc.init, a[0].to!string)).retro.array; 624 auto span = Spanner(toks); 625 626 span.put(FileMutant(MutationId(1), Offset(1, 10), "smurf")); 627 span.put(FileMutant(MutationId(1), Offset(9, 15), "donkey")); 628 629 // TODO add checks 630 } 631 632 /** 633 * 634 * # Overlap Cases 635 * 1. Perfekt overlap 636 * |--T--| 637 * |--M--| 638 * 639 * 2. Token enclosing mutant 640 * |---T--| 641 * |-M-| 642 * 643 * 3. Mutant beginning inside a token 644 * |---T--| 645 * |-M----| 646 * 647 * 4. Mutant overlapping multiple tokens. 648 * |--T--|--T--| 649 * |--M--------| 650 */ 651 struct SpannerRange { 652 alias BTree = Spanner.BTree; 653 654 BTree!Token tokens; 655 BTree!FileMutant muts; 656 657 this(BTree!Token tokens, BTree!FileMutant muts) @safe { 658 this.tokens = tokens; 659 this.muts = muts; 660 dropMutants; 661 } 662 663 Span front() @safe pure nothrow { 664 import std.array : appender; 665 666 assert(!empty, "Can't get front of an empty range"); 667 auto t = tokens.front; 668 if (muts.empty) 669 return Span(t); 670 671 auto app = appender!(FileMutant[])(); 672 foreach (m; muts) { 673 if (m.offset.begin < t.offset.end) 674 app.put(m); 675 else 676 break; 677 } 678 679 return Span(t, app.data); 680 } 681 682 void popFront() @safe { 683 assert(!empty, "Can't pop front of an empty range"); 684 tokens.removeFront; 685 dropMutants; 686 } 687 688 bool empty() @safe pure nothrow @nogc { 689 return tokens.empty; 690 } 691 692 private void dropMutants() @safe { 693 import std.algorithm : filter; 694 import std.array : array; 695 696 if (tokens.empty) 697 return; 698 699 // removing mutants that the tokens have "passed by" 700 const t = tokens.front; 701 auto r = muts[].filter!(a => a.offset.end <= t.offset.begin).array; 702 muts.removeKey(r); 703 } 704 } 705 706 struct Span { 707 import std.range : isOutputRange; 708 709 Token tok; 710 FileMutant[] muts; 711 712 string toString() @safe pure const { 713 import std.array : appender; 714 715 auto buf = appender!string; 716 toString(buf); 717 return buf.data; 718 } 719 720 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 721 import std.format : formattedWrite; 722 import std.range : put; 723 724 formattedWrite(w, "%s|%(%s %)", tok, muts); 725 } 726 } 727 728 @("shall return a range grouping mutants by the tokens they overlap") 729 @safe unittest { 730 import std.algorithm; 731 import std.array : array; 732 import std.conv; 733 import std.range; 734 import clang.c.Index : CXTokenKind; 735 736 auto offsets = zip(iota(0, 150, 10), iota(10, 160, 10)).map!(a => Offset(a[0], a[1])).array; 737 738 auto toks = offsets.map!(a => Token(CXTokenKind.comment, a, SourceLoc.init, 739 SourceLoc.init, a.begin.to!string)).retro.array; 740 auto span = Spanner(toks); 741 742 span.put(FileMutant(MutationId(2), Offset(11, 15), "token enclosing mutant")); 743 span.put(FileMutant(MutationId(3), Offset(31, 42), "mutant beginning inside a token")); 744 span.put(FileMutant(MutationId(4), Offset(50, 80), "mutant overlapping multiple tokens")); 745 746 span.put(FileMutant(MutationId(5), Offset(90, 100), "1 multiple mutants for a token")); 747 span.put(FileMutant(MutationId(6), Offset(90, 110), "2 multiple mutants for a token")); 748 span.put(FileMutant(MutationId(1), Offset(120, 130), "perfect overlap")); 749 750 auto res = span.toRange.array; 751 //logger.tracef("%(%s\n%)", res); 752 res[1].muts[0].id.shouldEqual(2); 753 res[2].muts.length.shouldEqual(0); 754 res[3].muts[0].id.shouldEqual(3); 755 res[4].muts[0].id.shouldEqual(3); 756 res[5].muts[0].id.shouldEqual(4); 757 res[6].muts[0].id.shouldEqual(4); 758 res[7].muts[0].id.shouldEqual(4); 759 res[8].muts.length.shouldEqual(0); 760 res[9].muts.length.shouldEqual(2); 761 res[9].muts[0].id.shouldEqual(5); 762 res[9].muts[1].id.shouldEqual(6); 763 res[10].muts[0].id.shouldEqual(6); 764 res[11].muts.length.shouldEqual(0); 765 res[12].muts[0].id.shouldEqual(1); 766 res[13].muts.length.shouldEqual(0); 767 } 768 769 void toIndex(FileIndex[] files, Element root, string htmlFileDir) @trusted { 770 import std.algorithm : sort, filter; 771 import std.conv : to; 772 import std.path : buildPath; 773 774 auto tbl_container = root.addChild("div").addClass("tbl_container"); 775 auto tbl = tmplDefaultTable(tbl_container, [ 776 "Path", "Score", "Alive", "NoMut", "Total" 777 ]); 778 779 // Users are not interested that files that contains zero mutants are shown 780 // in the list. It is especially annoying when they are marked with dark 781 // green. 782 bool has_suppressed; 783 foreach (f; files.sort!((a, b) => a.path < b.path) 784 .filter!(a => a.totalMutants != 0)) { 785 auto r = tbl.appendRow(); 786 r.addChild("td").addChild("a", f.display).href = buildPath(htmlFileDir, f.path); 787 788 const score = () { 789 if (f.totalMutants == 0) 790 return 1.0; 791 return cast(double) f.killedMutants / cast(double) f.totalMutants; 792 }(); 793 const style = () { 794 if (f.killedMutants == f.totalMutants) 795 return "background-color: green"; 796 if (score < 0.3) 797 return "background-color: red"; 798 if (score < 0.5) 799 return "background-color: salmon"; 800 if (score < 0.8) 801 return "background-color: lightyellow"; 802 if (score < 1.0) 803 return "background-color: lightgreen"; 804 return null; 805 }(); 806 807 r.addChild("td", format("%.3s", score)).style = style; 808 r.addChild("td", f.aliveMutants.to!string).style = style; 809 r.addChild("td", f.aliveNoMut.to!string).style = style; 810 r.addChild("td", f.totalMutants.to!string).style = style; 811 812 has_suppressed = has_suppressed || f.aliveNoMut != 0; 813 } 814 815 root.addChild("p", "NoMut is the number of alive mutants in the file that are ignored.") 816 .appendText(" This increases the score."); 817 root.setAttribute("onload", "init()"); 818 } 819 820 /** Metadata about the span to be used to e.g. color it. 821 * 822 * Each span has a mutant that becomes activated when the user click on the 823 * span. The user most likely is interested in seeing **a** mutant that has 824 * survived on that point becomes the color is red. 825 * 826 * This is why the algorithm uses the same prio as the one for choosing 827 * color. These two are strongly correlated with each other. 828 */ 829 struct MetaSpan { 830 // ordered in priority 831 enum StatusColor { 832 alive, 833 killed, 834 timeout, 835 killedByCompiler, 836 unknown, 837 none, 838 } 839 840 StatusColor status; 841 string onClick; 842 843 this(const(FileMutant)[] muts) { 844 immutable click_fmt2 = "ui_set_mut(%s)"; 845 status = StatusColor.none; 846 847 foreach (ref const m; muts) { 848 status = pickColor(m, status); 849 if (onClick.length == 0 && m.mut.status == Mutation.Status.alive) { 850 onClick = format(click_fmt2, m.id); 851 } 852 } 853 854 if (onClick.length == 0 && muts.length != 0) { 855 onClick = format(click_fmt2, muts[0].id); 856 } 857 } 858 } 859 860 /// Choose a color for a mutant span by prioritizing alive mutants above all. 861 MetaSpan.StatusColor pickColor(const FileMutant m, 862 MetaSpan.StatusColor status = MetaSpan.StatusColor.none) { 863 final switch (m.mut.status) { 864 case Mutation.Status.alive: 865 status = MetaSpan.StatusColor.alive; 866 break; 867 case Mutation.Status.killed: 868 if (status > MetaSpan.StatusColor.killed) 869 status = MetaSpan.StatusColor.killed; 870 break; 871 case Mutation.Status.killedByCompiler: 872 if (status > MetaSpan.StatusColor.killedByCompiler) 873 status = MetaSpan.StatusColor.killedByCompiler; 874 break; 875 case Mutation.Status.timeout: 876 if (status > MetaSpan.StatusColor.timeout) 877 status = MetaSpan.StatusColor.timeout; 878 break; 879 case Mutation.Status.unknown: 880 if (status > MetaSpan.StatusColor.unknown) 881 status = MetaSpan.StatusColor.unknown; 882 break; 883 } 884 return status; 885 } 886 887 string toVisible(MetaSpan.StatusColor s) { 888 if (s == MetaSpan.StatusColor.none) 889 return null; 890 return format("status_%s", s); 891 } 892 893 string toHover(MetaSpan.StatusColor s) { 894 if (s == MetaSpan.StatusColor.none) 895 return null; 896 return format("hover_%s", s); 897 }