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