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