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