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.algorithm : max, each, map, min, canFind, sort, filter, joiner; 14 import std.array : Appender, appender, array, empty; 15 import std.datetime : dur; 16 import std.exception : collectException; 17 import std.format : format; 18 import std.functional : toDelegate; 19 import std.path : buildPath, baseName, relativePath; 20 import std.range : only; 21 import std.stdio : File; 22 import std.typecons : tuple, Tuple; 23 import std.utf : toUTF8, byChar; 24 25 import arsd.dom : Document, Element, require, Table, RawSource, Link; 26 import my.actor; 27 import my.actor.utility.limiter; 28 import my.gc.refc; 29 import my.optional; 30 import my.set; 31 32 import dextool.plugin.mutate.backend.database : Database, FileRow, FileMutantRow, MutationId; 33 import dextool.plugin.mutate.backend.diff_parser : Diff; 34 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 35 import dextool.plugin.mutate.backend.report.type : FileReport, FilesReporter; 36 import dextool.plugin.mutate.backend.type : Mutation, Offset, SourceLoc, Token; 37 import dextool.plugin.mutate.backend.utility : Profile; 38 import dextool.plugin.mutate.config : ConfigReport; 39 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportSection; 40 import dextool.type : AbsolutePath, Path; 41 42 import dextool.plugin.mutate.backend.report.html.constants : HtmlStyle = Html, DashboardCss; 43 import dextool.plugin.mutate.backend.report.html.tmpl; 44 import dextool.plugin.mutate.backend.resource; 45 46 @safe: 47 48 void report(ref System sys, AbsolutePath dbPath, const MutationKind[] userKinds, 49 ConfigReport conf, FilesysIO fio, ref Diff diff) @trusted { 50 import dextool.plugin.mutate.backend.database : FileMutantRow; 51 import dextool.plugin.mutate.backend.mutation_type : toInternal; 52 53 auto flowCtrl = sys.spawn(&spawnFlowControlTotalCPUs); 54 auto reportCollector = sys.spawn(&spawnFileReportCollector, flowCtrl); 55 auto overview = sys.spawn(&spawnOverviewActor, flowCtrl, reportCollector, 56 dbPath, userKinds.dup, conf, fio, diff); 57 58 auto self = scopedActor; 59 self.request(overview, infTimeout).send(WaitForDoneMsg.init).then((bool a) {}); 60 } 61 62 struct FileIndex { 63 import dextool.plugin.mutate.backend.report.analyzers : MutationScore; 64 65 Path path; 66 string display; 67 MutationScore stat; 68 } 69 70 @safe: 71 private: 72 73 string toJson(string s) { 74 import std.json : JSONValue; 75 76 return JSONValue(s).toString; 77 } 78 79 struct FileCtx { 80 import std.stdio : File; 81 import blob_model : Blob; 82 import dextool.plugin.mutate.backend.database : FileId, TestCaseInfo2; 83 84 Path processFile; 85 File out_; 86 87 Spanner span; 88 89 Document doc; 90 91 // The text of the current file that is being processed. 92 Blob raw; 93 94 /// Database ID for this file. 95 FileId fileId; 96 97 /// Find the test cases that killed a mutant. They are sorted by most killed -> least killed. 98 TestCaseInfo[][MutationId] tcKilledMutant; 99 100 /// All test cases in the file. 101 TestCaseInfo[] testCases; 102 103 static FileCtx make(string title, FileId id, Blob raw, TestCaseInfo2[] tc_info) @trusted { 104 import dextool.plugin.mutate.backend.report.html.tmpl; 105 106 auto r = FileCtx.init; 107 r.doc = tmplBasicPage.filesCss; 108 r.doc.title = title; 109 r.doc.mainBody.setAttribute("onload", "javascript:init();"); 110 111 auto s = r.doc.root.childElements("head")[0].addChild("style"); 112 s.addChild(new RawSource(r.doc, tmplIndexStyle)); 113 114 s = r.doc.root.childElements("head")[0].addChild("script"); 115 s.addChild(new RawSource(r.doc, jsSource)); 116 117 r.doc.mainBody.appendHtml(tmplIndexBody); 118 119 r.fileId = id; 120 121 r.raw = raw; 122 123 typeof(tcKilledMutant) tmp; 124 foreach (a; tc_info) { 125 foreach (mut; a.killed) { 126 tmp.update(mut, { return [TestCaseInfo(a.name, a.killed.length)]; }, 127 (ref TestCaseInfo[] v) => v ~= TestCaseInfo(a.name, a.killed.length)); 128 } 129 } 130 r.testCases = tc_info.map!(a => TestCaseInfo(a.name, a.killed.length)).array; 131 132 foreach (kv; tmp.byKeyValue) { 133 r.tcKilledMutant[kv.key] = kv.value.sort.array; 134 } 135 136 return r; 137 } 138 139 TestCaseInfo[] getTestCaseInfo(MutationId mutationId) @safe pure nothrow { 140 if (auto v = mutationId in tcKilledMutant) 141 return *v; 142 return null; 143 } 144 145 static struct TestCaseInfo { 146 import dextool.plugin.mutate.backend.type : TestCase; 147 148 TestCase name; 149 long killed; 150 151 int opCmp(ref const typeof(this) s) @safe pure nothrow const @nogc scope { 152 if (killed < s.killed) 153 return -1; 154 else if (killed > s.killed) 155 return 1; 156 else if (name < s.name) 157 return -1; 158 else if (name > s.name) 159 return 1; 160 return 0; 161 } 162 163 bool opEquals(ref const typeof(this) s) @safe pure nothrow const @nogc scope { 164 return name == s.name; 165 } 166 167 size_t toHash() @safe nothrow const { 168 return name.toHash; 169 } 170 } 171 } 172 173 auto tokenize(AbsolutePath base_dir, Path f) @trusted { 174 import std.typecons : Yes; 175 import libclang_ast.context; 176 static import dextool.plugin.mutate.backend.utility; 177 178 const fpath = buildPath(base_dir, f).Path.AbsolutePath; 179 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 180 return dextool.plugin.mutate.backend.utility.tokenize!(Yes.splitMultiLineTokens)(ctx, fpath); 181 } 182 183 struct FileMutant { 184 nothrow: 185 static struct Text { 186 /// the original text that covers the offset. 187 string original; 188 /// The mutation text that covers the offset. 189 string mutation; 190 } 191 192 MutationId id; 193 Offset offset; 194 Text txt; 195 Mutation mut; 196 197 this(MutationId id, Offset offset, string original, string mutation, Mutation mut) { 198 import std.utf : validate; 199 import dextool.plugin.mutate.backend.type : invalidUtf8; 200 201 this.id = id; 202 this.offset = offset; 203 this.mut = mut; 204 205 try { 206 validate(original); 207 this.txt.original = original; 208 } catch (Exception e) { 209 this.txt.original = invalidUtf8; 210 } 211 212 try { 213 validate(mutation); 214 // users prefer being able to see what has been removed. 215 if (mutation.length == 0) 216 this.txt.mutation = "/* " ~ this.txt.original ~ " */"; 217 else 218 this.txt.mutation = mutation; 219 } catch (Exception e) { 220 this.txt.mutation = invalidUtf8; 221 } 222 } 223 224 this(MutationId id, Offset offset, string original) { 225 this(id, offset, original, null, Mutation.init); 226 } 227 228 string original() @safe pure nothrow const @nogc { 229 return txt.original; 230 } 231 232 string mutation() @safe pure nothrow const @nogc { 233 return txt.mutation; 234 } 235 236 int opCmp(ref const typeof(this) s) const @safe { 237 if (offset.begin > s.offset.begin) 238 return 1; 239 if (offset.begin < s.offset.begin) 240 return -1; 241 if (offset.end > s.offset.end) 242 return 1; 243 if (offset.end < s.offset.end) 244 return -1; 245 return 0; 246 } 247 } 248 249 @("shall be possible to construct a FileMutant in @safe") 250 @safe unittest { 251 auto fmut = FileMutant(MutationId(1), Offset(1, 2), "smurf"); 252 } 253 254 /* 255 I get a mutant that have a start/end offset. 256 I have all tokens. 257 I can't write the html before I have all mutants for the offset. 258 Hmm potentially this mean that I can't write any html until I have analyzed all mutants for the file. 259 This must be so.... 260 261 How to do it? 262 263 From reading https://stackoverflow.com/questions/11389627/span-overlapping-strings-in-a-paragraph 264 it seems that generating a <span..> for each token with multiple classes in them. A class for each mutant. 265 then they can be toggled on/off. 266 267 a <href> tag to the beginning to jump to the mutant. 268 */ 269 270 /** Provide an interface to travers the tokens and get the overlapping mutants. 271 */ 272 struct Spanner { 273 import std.container : RedBlackTree, redBlackTree; 274 import std.range : isOutputRange; 275 276 alias BTree(T) = RedBlackTree!(T, "a < b", true); 277 278 BTree!Token tokens; 279 BTree!FileMutant muts; 280 281 this(Token[] tokens) @trusted { 282 this.tokens = new typeof(this.tokens); 283 this.muts = new typeof(this.muts)(); 284 285 this.tokens.insert(tokens); 286 } 287 288 void put(const FileMutant fm) @trusted { 289 muts.insert(fm); 290 } 291 292 SpannerRange toRange() @safe { 293 return SpannerRange(tokens, muts); 294 } 295 296 string toString() @safe pure const { 297 auto buf = appender!string; 298 this.toString(buf); 299 return buf.data; 300 } 301 302 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 303 import std.format : formattedWrite; 304 import std.range : zip, StoppingPolicy; 305 import std.string; 306 import std.algorithm : max; 307 import std.traits : Unqual; 308 309 ulong sz; 310 311 foreach (ref const t; zip(StoppingPolicy.longest, tokens[], muts[])) { 312 auto c0 = format("%s", cast(Unqual!(typeof(t[0]))) t[0]); 313 string c1; 314 if (t[1] != typeof(t[1]).init) 315 c1 = format("%s", cast(Unqual!(typeof(t[1]))) t[1]); 316 sz = max(sz, c0.length, c1.length); 317 formattedWrite(w, "%s | %s\n", c0.rightJustify(sz), c1); 318 } 319 } 320 } 321 322 @("shall be possible to construct a Spanner in @safe") 323 @safe unittest { 324 import std.algorithm; 325 import std.conv; 326 import std.range; 327 import clang.c.Index : CXTokenKind; 328 329 auto toks = zip(iota(10), iota(10, 20)).map!(a => Token(CXTokenKind.comment, 330 Offset(a[0], a[1]), SourceLoc.init, SourceLoc.init, a[0].to!string)).retro.array; 331 auto span = Spanner(toks); 332 333 span.put(FileMutant(MutationId(1), Offset(1, 10), "smurf")); 334 span.put(FileMutant(MutationId(1), Offset(9, 15), "donkey")); 335 336 // TODO add checks 337 } 338 339 /** 340 * 341 * # Overlap Cases 342 * 1. Perfekt overlap 343 * |--T--| 344 * |--M--| 345 * 346 * 2. Token enclosing mutant 347 * |---T--| 348 * |-M-| 349 * 350 * 3. Mutant beginning inside a token 351 * |---T--| 352 * |-M----| 353 * 354 * 4. Mutant overlapping multiple tokens. 355 * |--T--|--T--| 356 * |--M--------| 357 */ 358 struct SpannerRange { 359 alias BTree = Spanner.BTree; 360 361 BTree!Token tokens; 362 BTree!FileMutant muts; 363 364 this(BTree!Token tokens, BTree!FileMutant muts) @safe { 365 this.tokens = tokens; 366 this.muts = muts; 367 dropMutants; 368 } 369 370 Span front() @safe pure nothrow { 371 assert(!empty, "Can't get front of an empty range"); 372 auto t = tokens.front; 373 if (muts.empty) 374 return Span(t); 375 376 auto app = appender!(FileMutant[])(); 377 foreach (m; muts) { 378 if (m.offset.begin < t.offset.end) 379 app.put(m); 380 else 381 break; 382 } 383 384 return Span(t, app.data); 385 } 386 387 void popFront() @safe { 388 assert(!empty, "Can't pop front of an empty range"); 389 tokens.removeFront; 390 dropMutants; 391 } 392 393 bool empty() @safe pure nothrow @nogc { 394 return tokens.empty; 395 } 396 397 private void dropMutants() @safe { 398 if (tokens.empty) 399 return; 400 401 // removing mutants that the tokens have "passed by" 402 const t = tokens.front; 403 auto r = muts[].filter!(a => a.offset.end <= t.offset.begin).array; 404 muts.removeKey(r); 405 } 406 } 407 408 struct Span { 409 import std.range : isOutputRange; 410 411 Token tok; 412 FileMutant[] muts; 413 414 string toString() @safe pure const { 415 auto buf = appender!string; 416 toString(buf); 417 return buf.data; 418 } 419 420 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 421 import std.format : formattedWrite; 422 import std.range : put; 423 424 formattedWrite(w, "%s|%(%s %)", tok, muts); 425 } 426 } 427 428 @("shall return a range grouping mutants by the tokens they overlap") 429 @safe unittest { 430 import std.algorithm; 431 import std.conv; 432 import std.range; 433 import clang.c.Index : CXTokenKind; 434 435 import unit_threaded : shouldEqual; 436 437 auto offsets = zip(iota(0, 150, 10), iota(10, 160, 10)).map!(a => Offset(a[0], a[1])).array; 438 439 auto toks = offsets.map!(a => Token(CXTokenKind.comment, a, SourceLoc.init, 440 SourceLoc.init, a.begin.to!string)).retro.array; 441 auto span = Spanner(toks); 442 443 span.put(FileMutant(MutationId(2), Offset(11, 15), "token enclosing mutant")); 444 span.put(FileMutant(MutationId(3), Offset(31, 42), "mutant beginning inside a token")); 445 span.put(FileMutant(MutationId(4), Offset(50, 80), "mutant overlapping multiple tokens")); 446 447 span.put(FileMutant(MutationId(5), Offset(90, 100), "1 multiple mutants for a token")); 448 span.put(FileMutant(MutationId(6), Offset(90, 110), "2 multiple mutants for a token")); 449 span.put(FileMutant(MutationId(1), Offset(120, 130), "perfect overlap")); 450 451 auto res = span.toRange.array; 452 //logger.tracef("%(%s\n%)", res); 453 res[1].muts[0].id.get.shouldEqual(2); 454 res[2].muts.length.shouldEqual(0); 455 res[3].muts[0].id.get.shouldEqual(3); 456 res[4].muts[0].id.get.shouldEqual(3); 457 res[5].muts[0].id.get.shouldEqual(4); 458 res[6].muts[0].id.get.shouldEqual(4); 459 res[7].muts[0].id.get.shouldEqual(4); 460 res[8].muts.length.shouldEqual(0); 461 res[9].muts.length.shouldEqual(2); 462 res[9].muts[0].id.get.shouldEqual(5); 463 res[9].muts[1].id.get.shouldEqual(6); 464 res[10].muts[0].id.get.shouldEqual(6); 465 res[11].muts.length.shouldEqual(0); 466 res[12].muts[0].id.get.shouldEqual(1); 467 res[13].muts.length.shouldEqual(0); 468 } 469 470 void toIndex(FileIndex[] files, Element root, string htmlFileDir) @trusted { 471 import std.algorithm : sort, filter; 472 import std.conv : to; 473 474 DashboardCss.h2(root.addChild(new Link("#files", null)).setAttribute("id", "files"), "Files"); 475 476 auto fltr = root.addChild("div").addClass("input-group"); 477 fltr.addChild("input").setAttribute("type", "search").setAttribute("id", "fileFilterInput").setAttribute("onkeyup", 478 "filter_table_on_search('fileFilterInput', 'fileTable')").addClass( 479 "form-control").setAttribute("placeholder", "Search..."); 480 481 auto tbl = tmplSortableTable(root, [ 482 "Path", "Score", "Alive", "NoMut", "Total", "Time (min)" 483 ]); 484 tbl.setAttribute("id", "fileTable"); 485 486 // Users are not interested that files that contains zero mutants are shown 487 // in the list. It is especially annoying when they are marked with dark 488 // green. 489 bool hasSuppressed; 490 auto noMutants = appender!(FileIndex[])(); 491 foreach (f; files.sort!((a, b) => a.path < b.path)) { 492 if (f.stat.total == 0) { 493 noMutants.put(f); 494 } else { 495 auto r = tbl.appendRow(); 496 r.addChild("td").addChild("a", f.display).href = buildPath(htmlFileDir, f.path); 497 498 const score = f.stat.score; 499 const style = () { 500 if (f.stat.total == 0) 501 return "background-color: lightgrey"; 502 if (f.stat.killed == f.stat.total) 503 return "background-color: green"; 504 if (score < 0.3) 505 return "background-color: red"; 506 if (score < 0.5) 507 return "background-color: salmon"; 508 if (score < 0.8) 509 return "background-color: lightyellow"; 510 if (score < 1.0) 511 return "background-color: lightgreen"; 512 return null; 513 }(); 514 515 r.addChild("td", format!"%.3s"(score)).style = style; 516 r.addChild("td", f.stat.alive.to!string); 517 r.addChild("td", f.stat.aliveNoMut.to!string); 518 r.addChild("td", f.stat.total.to!string); 519 r.addChild("td", f.stat 520 .totalTime 521 .sum 522 .total!"minutes" 523 .to!string); 524 525 hasSuppressed = hasSuppressed || f.stat.aliveNoMut != 0; 526 } 527 } 528 529 if (!noMutants.data.empty) { 530 root.addChild("p", "Analyzed files with no mutants in them."); 531 auto noMutTbl = tmplSortableTable(root, ["Path"]); 532 foreach (f; noMutants.data) { 533 auto r = noMutTbl.appendRow(); 534 r.addChild("td").addChild("a", f.display).href = buildPath(htmlFileDir, f.path); 535 } 536 } 537 538 if (hasSuppressed) { 539 root.addChild("p", "NoMut is the number of alive mutants in the file that are ignored.") 540 .appendText(" This increases the score."); 541 } 542 } 543 544 /** Metadata about the span to be used to e.g. color it. 545 * 546 * Each span has a mutant that becomes activated when the user click on the 547 * span. The user most likely is interested in seeing **a** mutant that has 548 * survived on that point becomes the color is red. 549 * 550 * This is why the algorithm uses the same prio as the one for choosing 551 * color. These two are strongly correlated with each other. 552 */ 553 struct MetaSpan { 554 // ordered in priority 555 enum StatusColor { 556 alive, 557 killed, 558 timeout, 559 memOverload, 560 killedByCompiler, 561 skipped, 562 unknown, 563 none, 564 noCoverage, 565 } 566 567 StatusColor status; 568 string onClick; 569 570 this(const(FileMutant)[] muts) { 571 immutable click_fmt2 = "ui_set_mut(%s)"; 572 status = StatusColor.none; 573 574 foreach (ref const m; muts) { 575 status = pickColor(m, status); 576 if (onClick.length == 0 && m.mut.status == Mutation.Status.alive) { 577 onClick = format(click_fmt2, m.id.get); 578 } 579 } 580 581 if (onClick.length == 0 && muts.length != 0) { 582 onClick = format(click_fmt2, muts[0].id.get); 583 } 584 } 585 } 586 587 /// Choose a color for a mutant span by prioritizing alive mutants above all. 588 MetaSpan.StatusColor pickColor(const FileMutant m, 589 MetaSpan.StatusColor status = MetaSpan.StatusColor.none) { 590 final switch (m.mut.status) { 591 case Mutation.Status.noCoverage: 592 status = MetaSpan.StatusColor.noCoverage; 593 break; 594 case Mutation.Status.alive: 595 status = MetaSpan.StatusColor.alive; 596 break; 597 case Mutation.Status.killed: 598 if (status > MetaSpan.StatusColor.killed) 599 status = MetaSpan.StatusColor.killed; 600 break; 601 case Mutation.Status.killedByCompiler: 602 if (status > MetaSpan.StatusColor.killedByCompiler) 603 status = MetaSpan.StatusColor.killedByCompiler; 604 break; 605 case Mutation.Status.timeout: 606 if (status > MetaSpan.StatusColor.timeout) 607 status = MetaSpan.StatusColor.timeout; 608 break; 609 case Mutation.Status.memOverload: 610 if (status > MetaSpan.StatusColor.memOverload) 611 status = MetaSpan.StatusColor.memOverload; 612 break; 613 case Mutation.Status.skipped: 614 if (status > MetaSpan.StatusColor.skipped) 615 status = MetaSpan.StatusColor.skipped; 616 break; 617 case Mutation.Status.unknown: 618 if (status > MetaSpan.StatusColor.unknown) 619 status = MetaSpan.StatusColor.unknown; 620 break; 621 case Mutation.Status.equivalent: 622 if (status > MetaSpan.StatusColor.unknown) 623 status = MetaSpan.StatusColor.unknown; 624 break; 625 } 626 return status; 627 } 628 629 string toVisible(MetaSpan.StatusColor s) { 630 if (s == MetaSpan.StatusColor.none) 631 return null; 632 return format("status_%s", s); 633 } 634 635 void generateFile(ref Database db, ref FileCtx ctx) @trusted { 636 import std.conv : to; 637 import std.range : repeat, enumerate; 638 import std.traits : EnumMembers; 639 import dextool.plugin.mutate.type : MutationKind; 640 import dextool.plugin.mutate.backend.database.type : MutantMetaData; 641 import dextool.plugin.mutate.backend.report.utility : window; 642 import dextool.plugin.mutate.backend.mutation_type : toUser; 643 644 static struct MData { 645 MutationId id; 646 FileMutant.Text txt; 647 Mutation mut; 648 MutantMetaData metaData; 649 } 650 651 auto root = ctx.doc.mainBody; 652 auto lines = root.addChild("table").setAttribute("id", "locs").setAttribute("cellpadding", "0"); 653 auto line = lines.addChild("tr").addChild("td").setAttribute("id", "loc-1"); 654 line.addClass("loc"); 655 656 line.addChild("span", "1:").addClass("line_nr"); 657 auto mut_data = appender!(string[])(); 658 mut_data.put("var g_muts_data = {};"); 659 mut_data.put("g_muts_data[-1] = {'kind' : null, 'status' : null, 'testCases' : null, 'orgText' : null, 'mutText' : null, 'meta' : null};"); 660 661 // used to make sure that metadata about a mutant is only written onces 662 // to the global arrays. 663 Set!MutationId metadataOnlyOnce; 664 auto muts = appender!(MData[])(); 665 666 // this is the last location. It is used to calculate the num of 667 // newlines, detect when a line changes etc. 668 auto lastLoc = SourceLoc(1, 1); 669 670 foreach (const s; ctx.span.toRange) { 671 if (s.tok.loc.line > lastLoc.line) { 672 lastLoc.column = 1; 673 } 674 auto meta = MetaSpan(s.muts); 675 676 foreach (const i; 0 .. max(0, s.tok.loc.line - lastLoc.line)) { 677 line = lines.addChild("tr").addChild("td"); 678 line.setAttribute("id", format("%s-%s", "loc", lastLoc.line + i + 1)) 679 .addClass("loc").addChild("span", format("%s:", 680 lastLoc.line + i + 1)).addClass("line_nr"); 681 682 // force a newline in the generated html to improve readability 683 lines.appendText("\n"); 684 } 685 686 const spaces = max(0, s.tok.loc.column - lastLoc.column); 687 line.addChild(new RawSource(ctx.doc, format("%-(%s%)", " ".repeat(spaces)))); 688 689 auto d0 = line.addChild("div").setAttribute("style", "display: inline;"); 690 with (d0.addChild("span", s.tok.spelling)) { 691 addClass("original"); 692 addClass(s.tok.toName); 693 if (auto v = meta.status.toVisible) 694 addClass(v); 695 if (s.muts.length != 0) 696 addClass(format("%(mutid%s %)", s.muts.map!(a => a.id))); 697 if (meta.onClick.length != 0) 698 setAttribute("onclick", meta.onClick); 699 } 700 701 foreach (m; s.muts.filter!(m => m.id !in metadataOnlyOnce)) { 702 metadataOnlyOnce.add(m.id); 703 704 const metadata = db.mutantApi.getMutantationMetaData(m.id); 705 706 muts.put(MData(m.id, m.txt, m.mut, metadata)); 707 { 708 auto mutantHtmlTag = d0.addChild("span").addClass("mutant") 709 .setAttribute("id", m.id.toString); 710 if (m.mutation.canFind('\n')) { 711 mutantHtmlTag.addChild("pre", m.mutation).addClass("mutant2"); 712 } else { 713 mutantHtmlTag.appendText(m.mutation); 714 } 715 } 716 d0.addChild("a").setAttribute("href", "#" ~ m.id.toString); 717 718 auto testCases = ctx.getTestCaseInfo(m.id); 719 if (testCases.empty) { 720 mut_data.put(format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : null, 'orgText' : %s, 'mutText' : %s, 'meta' : '%s'};", 721 m.id, m.mut.kind.to!int, toUser(m.mut.kind).to!int, 722 m.mut.status.to!ubyte, toJson(window(m.txt.original)), 723 toJson(window(m.txt.mutation)), metadata.kindToString)); 724 } else { 725 mut_data.put(format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : [%('%s',%)'], 'orgText' : %s, 'mutText' : %s, 'meta' : '%s'};", 726 m.id, m.mut.kind.to!int, toUser(m.mut.kind).to!int, 727 m.mut.status.to!ubyte, testCases.map!(a => a.name), 728 toJson(window(m.txt.original)), 729 toJson(window(m.txt.mutation)), metadata.kindToString)); 730 } 731 } 732 lastLoc = s.tok.locEnd; 733 } 734 735 // make sure there is a newline before the script start to improve 736 // readability of the html document source. 737 root.appendText("\n"); 738 739 with (root.addChild("script")) { 740 // force a newline in the generated html to improve readability 741 appendText("\n"); 742 addChild(new RawSource(ctx.doc, format("const MAX_NUM_TESTCASES = %s;", 743 db.testCaseApi.getDetectedTestCases.length))); 744 appendText("\n"); 745 addChild(new RawSource(ctx.doc, format("const g_mutids = [%(%s,%)];", 746 muts.data.map!(a => a.id)))); 747 appendText("\n"); 748 addChild(new RawSource(ctx.doc, format("const g_mut_st_map = [%('%s',%)'];", 749 [EnumMembers!(Mutation.Status)]))); 750 appendText("\n"); 751 addChild(new RawSource(ctx.doc, format("const g_mut_kind_map = [%('%s',%)'];", 752 [EnumMembers!(Mutation.Kind)]))); 753 appendText("\n"); 754 addChild(new RawSource(ctx.doc, format("const g_mut_kindGroup_map = [%('%s',%)'];", 755 [EnumMembers!(MutationKind)]))); 756 appendText("\n"); 757 758 // Creates a list of number of kills per testcase. 759 appendChild(new RawSource(ctx.doc, "var g_testcases_kills = {}")); 760 appendText("\n"); 761 foreach (tc; ctx.testCases) { 762 appendChild(new RawSource(ctx.doc, 763 format("g_testcases_kills['%s'] = [%s];", tc.name, tc.killed))); 764 appendText("\n"); 765 } 766 appendChild(new RawSource(ctx.doc, mut_data.data.joiner("\n").toUTF8)); 767 appendText("\n"); 768 } 769 770 try { 771 ctx.out_.write(ctx.doc.toString); 772 } catch (Exception e) { 773 logger.error(e.msg).collectException; 774 logger.error("Unable to generate a HTML report for ", ctx.processFile).collectException; 775 } 776 } 777 778 Document makeDashboard() @trusted { 779 import dextool.plugin.mutate.backend.resource : dashboard, jsIndex; 780 781 auto data = dashboard(); 782 783 auto doc = new Document(data.dashboardHtml.get); 784 auto style = doc.root.childElements("head")[0].addChild("style"); 785 style.addChild(new RawSource(doc, data.bootstrapCss.get)); 786 style.addChild(new RawSource(doc, data.dashboardCss.get)); 787 style.addChild(new RawSource(doc, tmplDefaultCss)); 788 789 auto script = doc.root.childElements("head")[0].addChild("script"); 790 script.addChild(new RawSource(doc, data.jquery.get)); 791 script.addChild(new RawSource(doc, data.bootstrapJs.get)); 792 script.addChild(new RawSource(doc, data.moment.get)); 793 script.addChild(new RawSource(doc, data.chart.get)); 794 script.addChild(new RawSource(doc, jsIndex)); 795 796 // jsIndex provide init() 797 doc.mainBody.setAttribute("onload", "init()"); 798 799 return doc; 800 } 801 802 struct NavbarItem { 803 string name; 804 string link; 805 } 806 807 void addNavbarItems(NavbarItem[] items, Element root) @trusted { 808 foreach (item; items) { 809 root.addChild("li").addChild(new Link(item.link, item.name)); 810 } 811 } 812 813 struct InitMsg { 814 } 815 816 struct DoneMsg { 817 } 818 819 struct GenerateReportMsg { 820 } 821 822 struct FailMsg { 823 } 824 825 alias FileReportActor = typedActor!(void function(InitMsg, AbsolutePath dbPath, AbsolutePath logFilesDir), 826 void function(AbsolutePath logFilesDir), void function(GenerateReportMsg), 827 void function(DoneMsg), void function(FailMsg)); 828 829 auto spawnFileReport(FileReportActor.Impl self, FlowControlActor.Address flowCtrl, 830 FileReportCollectorActor.Address collector, 831 AbsolutePath dbPath, FilesysIO fio, Mutation.Kind[] kinds, 832 ConfigReport conf, AbsolutePath logFilesDir, FileRow fr) @trusted { 833 static struct State { 834 Mutation.Kind[] kinds; 835 ConfigReport conf; 836 FlowControlActor.Address flowCtrl; 837 FileReportCollectorActor.Address collector; 838 FileRow fileRow; 839 840 Path reportFile; 841 842 Database db; 843 844 FileCtx ctx; 845 } 846 847 auto st = tuple!("self", "state", "fio")(self, refCounted(State(kinds, 848 conf, flowCtrl, collector, fr)), fio.dup); 849 alias Ctx = typeof(st); 850 851 static void init_(ref Ctx ctx, InitMsg, AbsolutePath dbPath, AbsolutePath logFilesDir) @trusted { 852 ctx.state.get.db = Database.make(dbPath); 853 send(ctx.self, logFilesDir); 854 } 855 856 static void start(ref Ctx ctx, AbsolutePath logFilesDir) @safe nothrow { 857 import dextool.plugin.mutate.backend.report.html.utility : pathToHtml; 858 859 try { 860 const original = ctx.state.get.fileRow.file.idup.pathToHtml; 861 const report = (original ~ HtmlStyle.ext).Path; 862 ctx.state.get.reportFile = report; 863 864 const out_path = buildPath(logFilesDir, report).Path.AbsolutePath; 865 866 auto raw = ctx.fio.makeInput(AbsolutePath(buildPath(ctx.fio.getOutputDir, 867 ctx.state.get.fileRow.file))); 868 869 auto tc_info = ctx.state.get.db.testCaseApi.getAllTestCaseInfo2( 870 ctx.state.get.fileRow.id, ctx.state.get.kinds); 871 872 ctx.state.get.ctx = FileCtx.make(original, ctx.state.get.fileRow.id, raw, tc_info); 873 ctx.state.get.ctx.processFile = ctx.state.get.fileRow.file; 874 ctx.state.get.ctx.out_ = File(out_path, "w"); 875 ctx.state.get.ctx.span = Spanner(tokenize(ctx.fio.getOutputDir, 876 ctx.state.get.fileRow.file)); 877 878 send(ctx.self, GenerateReportMsg.init); 879 } catch (Exception e) { 880 logger.warning(e.msg).collectException; 881 send(ctx.self, FailMsg.init).collectException; 882 } 883 } 884 885 static void run(ref Ctx ctx, GenerateReportMsg) @safe nothrow { 886 auto profile = Profile("html file report " ~ ctx.state.get.fileRow.file); 887 void fn(const ref FileMutantRow fr) { 888 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 889 890 // TODO unnecessary to create the mutation text here. 891 // Move it to endFileEvent. This is inefficient. 892 893 // the mutation text has been found to contain '\0' characters when the 894 // mutant span multiple lines. These null characters render badly in 895 // the html report. 896 static string cleanup(const(char)[] raw) @safe nothrow { 897 return raw.byChar.filter!(a => a != '\0').array.idup; 898 } 899 900 auto txt = makeMutationText(ctx.state.get.ctx.raw, 901 fr.mutationPoint.offset, fr.mutation.kind, fr.lang); 902 ctx.state.get.ctx.span.put(FileMutant(fr.id, 903 fr.mutationPoint.offset, cleanup(txt.original), 904 cleanup(txt.mutation), fr.mutation)); 905 } 906 907 try { 908 ctx.state.get.db.iterateFileMutants(ctx.state.get.kinds, 909 ctx.state.get.fileRow.file, &fn); 910 generateFile(ctx.state.get.db, ctx.state.get.ctx); 911 912 send(ctx.self, DoneMsg.init); 913 } catch (Exception e) { 914 logger.warning(e.msg).collectException; 915 send(ctx.self, FailMsg.init).collectException; 916 } 917 } 918 919 static void done(ref Ctx ctx, DoneMsg) @safe nothrow { 920 import dextool.plugin.mutate.backend.report.analyzers : reportScore; 921 922 try { 923 auto stat = reportScore(ctx.state.get.db, ctx.state.get.kinds, 924 ctx.state.get.fileRow.file); 925 send(ctx.state.get.collector, FileIndex(ctx.state.get.reportFile, 926 ctx.state.get.fileRow.file, stat)); 927 928 ctx.self.shutdown; 929 } catch (Exception e) { 930 logger.warning(e.msg).collectException; 931 send(ctx.self, FailMsg.init).collectException; 932 } 933 } 934 935 static void failed(ref Ctx ctx, FailMsg) @safe { 936 import dextool.plugin.mutate.backend.report.analyzers : MutationScore; 937 938 logger.warning("Failed to generate a HTML report for ", ctx.state.get.fileRow.file); 939 send(ctx.state.get.collector, FileIndex(ctx.state.get.reportFile, 940 ctx.state.get.fileRow.file, MutationScore.init)); 941 ctx.self.shutdown; 942 } 943 944 self.exceptionHandler = toDelegate(&logExceptionHandler); 945 946 self.request(flowCtrl, infTimeout).send(TakeTokenMsg.init) 947 .capture(self.address, dbPath, logFilesDir).then((ref Tuple!(FileReportActor.Address, 948 AbsolutePath, AbsolutePath) ctx, my.actor.utility.limiter.Token _) => send(ctx[0], 949 InitMsg.init, ctx[1], ctx[2])); 950 951 return impl(self, &init_, capture(st), &start, capture(st), &done, 952 capture(st), &run, capture(st), &failed, capture(st)); 953 } 954 955 struct GetIndexesMsg { 956 } 957 958 struct StartReporterMsg { 959 } 960 961 struct DoneStartingReportersMsg { 962 } 963 964 alias FileReportCollectorActor = typedActor!(void function(StartReporterMsg), 965 void function(DoneStartingReportersMsg), /// Collects an index. 966 void function(FileIndex), /// Returns all collected indexes. 967 FileIndex[]function(GetIndexesMsg)); 968 969 /// Collect file indexes from finished reports 970 auto spawnFileReportCollector(FileReportCollectorActor.Impl self, FlowControlActor.Address flow) { 971 static struct State { 972 FlowControlActor.Address flow; 973 974 uint reporters; 975 bool doneStarting; 976 FileIndex[] files; 977 Promise!(FileIndex[]) promise; 978 979 bool done() { 980 return doneStarting && (reporters == files.length); 981 } 982 } 983 984 auto st = tuple!("self", "state")(self, refCounted(State(flow))); 985 alias Ctx = typeof(st); 986 987 static void started(ref Ctx ctx, StartReporterMsg) { 988 ctx.state.get.reporters++; 989 } 990 991 static void doneStarting(ref Ctx ctx, DoneStartingReportersMsg) { 992 ctx.state.get.doneStarting = true; 993 } 994 995 static void index(ref Ctx ctx, FileIndex fi) { 996 ctx.state.get.files ~= fi; 997 998 send(ctx.state.get.flow, ReturnTokenMsg.init); 999 logger.infof("Generated %s (%s)", fi.display, fi.stat.score); 1000 1001 if (ctx.state.get.done && !ctx.state.get.promise.empty) { 1002 ctx.state.get.promise.deliver(ctx.state.get.files); 1003 ctx.self.shutdown; 1004 } 1005 } 1006 1007 static RequestResult!(FileIndex[]) getIndexes(ref Ctx ctx, GetIndexesMsg) { 1008 if (ctx.state.get.done) { 1009 if (!ctx.state.get.promise.empty) 1010 ctx.state.get.promise.deliver(ctx.state.get.files); 1011 ctx.self.shutdown; 1012 return typeof(return)(ctx.state.get.files); 1013 } 1014 1015 assert(ctx.state.get.promise.empty, "can only be one active request at a time"); 1016 ctx.state.get.promise = makePromise!(FileIndex[]); 1017 return typeof(return)(ctx.state.get.promise); 1018 } 1019 1020 self.exceptionHandler = () @trusted { 1021 return toDelegate(&logExceptionHandler); 1022 }(); 1023 return impl(self, &started, capture(st), &doneStarting, capture(st), 1024 &index, capture(st), &getIndexes, capture(st)); 1025 } 1026 1027 struct GetPagesMsg { 1028 } 1029 1030 alias SubPage = Tuple!(string, "fileName", string, "linkTxt"); 1031 alias SubContent = Tuple!(string, "name", string, "tag", string, "content"); 1032 1033 alias AnalyzeReportCollectorActor = typedActor!(void function(StartReporterMsg), void function(DoneStartingReportersMsg), /// Collects an index. 1034 void function(SubPage), void function(SubContent), void function(CheckDoneMsg), 1035 Tuple!(SubPage[], SubContent[]) function(GetPagesMsg)); 1036 1037 auto spawnAnalyzeReportCollector(AnalyzeReportCollectorActor.Impl self, 1038 FlowControlActor.Address flow) { 1039 alias Result = Tuple!(SubPage[], SubContent[]); 1040 static struct State { 1041 FlowControlActor.Address flow; 1042 1043 uint awaitingReports; 1044 bool doneStarting; 1045 1046 SubPage[] subPages; 1047 SubContent[] subContent; 1048 Promise!(Tuple!(SubPage[], SubContent[])) promise; 1049 1050 bool done() { 1051 return doneStarting && (awaitingReports == (subPages.length + subContent.length)); 1052 } 1053 } 1054 1055 auto st = tuple!("self", "state")(self, refCounted(State(flow))); 1056 alias Ctx = typeof(st); 1057 1058 static void started(ref Ctx ctx, StartReporterMsg) { 1059 ctx.state.get.awaitingReports++; 1060 } 1061 1062 static void doneStarting(ref Ctx ctx, DoneStartingReportersMsg) { 1063 ctx.state.get.doneStarting = true; 1064 } 1065 1066 static void subPage(ref Ctx ctx, SubPage p) { 1067 ctx.state.get.subPages ~= p; 1068 send(ctx.self, CheckDoneMsg.init); 1069 send(ctx.state.get.flow, ReturnTokenMsg.init); 1070 logger.infof("Generated %s", p.linkTxt); 1071 } 1072 1073 static void subContent(ref Ctx ctx, SubContent p) { 1074 ctx.state.get.subContent ~= p; 1075 send(ctx.self, CheckDoneMsg.init); 1076 send(ctx.state.get.flow, ReturnTokenMsg.init); 1077 logger.infof("Generated %s", p.name); 1078 } 1079 1080 static void checkDone(ref Ctx ctx, CheckDoneMsg) { 1081 // defensive programming in case a promise request arrive after the last page is generated. 1082 delayedSend(ctx.self, delay(1.dur!"seconds"), CheckDoneMsg.init); 1083 if (!ctx.state.get.done) 1084 return; 1085 1086 if (!ctx.state.get.promise.empty) { 1087 ctx.state.get.promise.deliver(tuple(ctx.state.get.subPages, ctx.state.get.subContent)); 1088 ctx.self.shutdown; 1089 } 1090 } 1091 1092 static RequestResult!Result getPages(ref Ctx ctx, GetPagesMsg) { 1093 if (ctx.state.get.done) { 1094 if (!ctx.state.get.promise.empty) 1095 ctx.state.get.promise.deliver(tuple(ctx.state.get.subPages, 1096 ctx.state.get.subContent)); 1097 ctx.self.shutdown; 1098 return typeof(return)(tuple(ctx.state.get.subPages, ctx.state.get.subContent)); 1099 } 1100 1101 assert(ctx.state.get.promise.empty, "can only be one active request at a time"); 1102 ctx.state.get.promise = makePromise!Result; 1103 return typeof(return)(ctx.state.get.promise); 1104 } 1105 1106 self.exceptionHandler = () @trusted { 1107 return toDelegate(&logExceptionHandler); 1108 }(); 1109 return impl(self, &started, capture(st), &doneStarting, capture(st), 1110 &subPage, capture(st), &checkDone, capture(st), &getPages, 1111 capture(st), &subContent, capture(st)); 1112 } 1113 1114 struct StartAnalyzersMsg { 1115 } 1116 1117 struct WaitForDoneMsg { 1118 } 1119 1120 struct IndexWaitMsg { 1121 } 1122 1123 struct CheckDoneMsg { 1124 } 1125 1126 struct GenerateIndexMsg { 1127 } 1128 1129 alias OverviewActor = typedActor!(void function(InitMsg, AbsolutePath), void function(StartAnalyzersMsg, AbsolutePath), 1130 void function(StartReporterMsg, AbsolutePath), void function(IndexWaitMsg), 1131 void function(GenerateIndexMsg), void function(CheckDoneMsg), // Returns a response when the reporting is done. 1132 bool function(WaitForDoneMsg)); 1133 1134 /** Generate `index.html` and act as the top coordinating actor that spawn, 1135 * control and summarises the result from all the sub-report actors. 1136 */ 1137 auto spawnOverviewActor(OverviewActor.Impl self, FlowControlActor.Address flowCtrl, 1138 FileReportCollectorActor.Address fileCollector, 1139 AbsolutePath dbPath, MutationKind[] userKinds, ConfigReport conf, 1140 FilesysIO fio, ref Diff diff) @trusted { 1141 import std.stdio : writefln, writeln; 1142 import undead.xml : encode; 1143 import dextool.plugin.mutate.backend.report.analyzers : TestCaseMetadata; 1144 1145 static struct State { 1146 FlowControlActor.Address flow; 1147 FileReportCollectorActor.Address fileCollector; 1148 ConfigReport conf; 1149 1150 // Report alive mutants in this section 1151 Diff diff; 1152 1153 /// What the user configured. 1154 MutationKind[] humanReadableKinds; 1155 1156 Set!ReportSection sections; 1157 1158 Mutation.Kind[] kinds; 1159 1160 /// The base directory of logdirs 1161 AbsolutePath logDir; 1162 /// Reports for each file 1163 AbsolutePath logFilesDir; 1164 /// Reports for each test case 1165 AbsolutePath logTestCasesDir; 1166 1167 // User provided metadata. 1168 TestCaseMetadata metaData; 1169 1170 Database db; 1171 1172 FileIndex[] files; 1173 SubPage[] subPages; 1174 SubContent[] subContent; 1175 1176 /// signals that the whole report is done. 1177 bool reportsDone; 1178 bool filesDone; 1179 bool done; 1180 Promise!bool waitForDone; 1181 } 1182 1183 auto st = tuple!("self", "state", "fio")(self, refCounted(State(flowCtrl, 1184 fileCollector, conf, diff, userKinds, conf.reportSection.toSet)), fio.dup); 1185 alias Ctx = typeof(st); 1186 1187 static void init_(ref Ctx ctx, InitMsg, AbsolutePath dbPath) { 1188 import std.file : mkdirRecurse; 1189 import dextool.plugin.mutate.backend.mutation_type : toInternal; 1190 import dextool.plugin.mutate.backend.report.analyzers : parseTestCaseMetadata; 1191 1192 ctx.state.get.db = Database.make(dbPath); 1193 1194 ctx.state.get.kinds = toInternal(ctx.state.get.humanReadableKinds); 1195 1196 ctx.state.get.logDir = buildPath(ctx.state.get.conf.logDir, HtmlStyle.dir) 1197 .Path.AbsolutePath; 1198 ctx.state.get.logFilesDir = buildPath(ctx.state.get.logDir, 1199 HtmlStyle.fileDir).Path.AbsolutePath; 1200 ctx.state.get.logTestCasesDir = buildPath(ctx.state.get.logDir, 1201 HtmlStyle.testCaseDir).Path.AbsolutePath; 1202 1203 if (ctx.state.get.conf.testMetadata.hasValue) 1204 ctx.state.get.metaData = parseTestCaseMetadata((cast(Optional!( 1205 ConfigReport.TestMetaData)) ctx.state.get.conf.testMetadata).orElse( 1206 ConfigReport.TestMetaData(AbsolutePath.init)).get); 1207 1208 foreach (a; only(ctx.state.get.logDir, ctx.state.get.logFilesDir, 1209 ctx.state.get.logTestCasesDir)) 1210 mkdirRecurse(a); 1211 1212 send(ctx.self, StartReporterMsg.init, dbPath); 1213 send(ctx.self, StartAnalyzersMsg.init, dbPath); 1214 } 1215 1216 static void startAnalyzers(ref Ctx ctx, StartAnalyzersMsg, AbsolutePath dbPath) { 1217 import dextool.plugin.mutate.backend.report.html.page_diff; 1218 import dextool.plugin.mutate.backend.report.html.page_minimal_set; 1219 import dextool.plugin.mutate.backend.report.html.page_mutant; 1220 import dextool.plugin.mutate.backend.report.html.page_nomut; 1221 import dextool.plugin.mutate.backend.report.html.page_stats; 1222 import dextool.plugin.mutate.backend.report.html.page_test_case; 1223 import dextool.plugin.mutate.backend.report.html.page_test_group_similarity; 1224 import dextool.plugin.mutate.backend.report.html.page_test_groups; 1225 import dextool.plugin.mutate.backend.report.html.trend; 1226 1227 string makeFname(string name) { 1228 return buildPath(ctx.state.get.logDir, name ~ HtmlStyle.ext); 1229 } 1230 1231 auto collector = ctx.self.homeSystem.spawn(&spawnAnalyzeReportCollector, 1232 ctx.state.get.flow); 1233 1234 runAnalyzer!makeStats(ctx.self, ctx.state.get.flow, collector, 1235 SubContent("Overview", "#overview", null), dbPath, ctx.state.get.kinds, 1236 AbsolutePath(ctx.state.get.logDir ~ Path("worklist" ~ HtmlStyle.ext))); 1237 1238 runAnalyzer!makeMutantPage(ctx.self, ctx.state.get.flow, collector, 1239 SubContent("Mutants", "#mutants", null), dbPath, ctx.state.get.conf, 1240 ctx.state.get.kinds, 1241 AbsolutePath(ctx.state.get.logDir ~ Path("mutants" ~ HtmlStyle.ext))); 1242 1243 runAnalyzer!makeTestCases(ctx.self, ctx.state.get.flow, collector, 1244 SubContent("Test Cases", "#test_cases", null), dbPath, ctx.state.get.conf, 1245 ctx.state.get.kinds, ctx.state.get.metaData, ctx.state.get.logTestCasesDir); 1246 1247 if (ReportSection.trend in ctx.state.get.sections) { 1248 runAnalyzer!makeTrend(ctx.self, ctx.state.get.flow, collector, 1249 SubContent("Trend", "#trend", null), dbPath, ctx.state.get.kinds); 1250 } 1251 1252 if (!ctx.state.get.diff.empty) { 1253 runAnalyzer!makeDiffView(ctx.self, ctx.state.get.flow, collector, 1254 SubPage(makeFname("diff_view"), "Diff View"), dbPath, ctx.state.get.conf, 1255 ctx.state.get.humanReadableKinds, ctx.state.get.kinds, 1256 ctx.state.get.diff, ctx.fio.getOutputDir); 1257 } 1258 if (ReportSection.tc_groups in ctx.state.get.sections) { 1259 runAnalyzer!makeTestGroups(ctx.self, ctx.state.get.flow, collector, 1260 SubPage(makeFname("test_groups"), "Test Groups"), dbPath, 1261 ctx.state.get.conf, ctx.state.get.humanReadableKinds, ctx.state.get.kinds); 1262 } 1263 1264 if (ReportSection.tc_min_set in ctx.state.get.sections) { 1265 runAnalyzer!makeMinimalSetAnalyse(ctx.self, ctx.state.get.flow, collector, 1266 SubPage(makeFname("minimal_set"), "Minimal Test Set"), dbPath, 1267 ctx.state.get.conf, ctx.state.get.humanReadableKinds, ctx.state.get.kinds); 1268 } 1269 1270 if (ReportSection.tc_groups_similarity in ctx.state.get.sections) { 1271 runAnalyzer!makeTestGroupSimilarityAnalyse(ctx.self, ctx.state.get.flow, collector, 1272 SubPage(makeFname("test_group_similarity"), "Test Group Similarity"), dbPath, 1273 ctx.state.get.conf, ctx.state.get.humanReadableKinds, ctx.state.get.kinds); 1274 } 1275 1276 runAnalyzer!makeNomut(ctx.self, ctx.state.get.flow, collector, 1277 SubPage(makeFname("nomut"), "NoMut Details"), dbPath, ctx.state.get.conf, 1278 ctx.state.get.humanReadableKinds, ctx.state.get.kinds); 1279 1280 send(collector, DoneStartingReportersMsg.init); 1281 1282 ctx.self.request(collector, infTimeout).send(GetPagesMsg.init) 1283 .capture(ctx).then((ref Ctx ctx, SubPage[] sp, SubContent[] sc) { 1284 ctx.state.get.subPages = sp; 1285 ctx.state.get.subContent = sc; 1286 ctx.state.get.reportsDone = true; 1287 send(ctx.self, IndexWaitMsg.init); 1288 }); 1289 } 1290 1291 static void startFileReportes(ref Ctx ctx, StartReporterMsg, AbsolutePath dbPath) { 1292 foreach (f; ctx.state.get.db.getDetailedFiles) { 1293 auto fa = ctx.self.homeSystem.spawn(&spawnFileReport, 1294 ctx.state.get.flow, ctx.state.get.fileCollector, dbPath, 1295 ctx.fio.dup, ctx.state.get.kinds, ctx.state.get.conf, 1296 ctx.state.get.logFilesDir, f); 1297 send(ctx.state.get.fileCollector, StartReporterMsg.init); 1298 } 1299 send(ctx.state.get.fileCollector, DoneStartingReportersMsg.init); 1300 1301 ctx.self.request(ctx.state.get.fileCollector, infTimeout) 1302 .send(GetIndexesMsg.init).capture(ctx).then((ref Ctx ctx, FileIndex[] a) { 1303 ctx.state.get.files = a; 1304 ctx.state.get.filesDone = true; 1305 send(ctx.self, IndexWaitMsg.init); 1306 }); 1307 } 1308 1309 static void indexWait(ref Ctx ctx, IndexWaitMsg) { 1310 if (ctx.state.get.reportsDone && ctx.state.get.filesDone) 1311 send(ctx.self, GenerateIndexMsg.init); 1312 } 1313 1314 static void checkDone(ref Ctx ctx, CheckDoneMsg) { 1315 if (!ctx.state.get.done) { 1316 delayedSend(ctx.self, delay(1.dur!"seconds"), CheckDoneMsg.init); 1317 return; 1318 } 1319 1320 if (!ctx.state.get.waitForDone.empty) 1321 ctx.state.get.waitForDone.deliver(true); 1322 } 1323 1324 static Promise!bool waitForDone(ref Ctx ctx, WaitForDoneMsg) { 1325 send(ctx.self, CheckDoneMsg.init); 1326 ctx.state.get.waitForDone = makePromise!bool; 1327 return ctx.state.get.waitForDone; 1328 } 1329 1330 static void genIndex(ref Ctx ctx, GenerateIndexMsg) { 1331 scope (exit) 1332 () { ctx.state.get.done = true; send(ctx.self, CheckDoneMsg.init); }(); 1333 1334 import std.datetime : Clock; 1335 import dextool.plugin.mutate.backend.report.html.page_tree_map; 1336 1337 auto profile = Profile("post process report"); 1338 1339 auto index = makeDashboard; 1340 index.title = format("Mutation Testing Report %(%s %) %s", 1341 ctx.state.get.humanReadableKinds, Clock.currTime); 1342 1343 auto content = index.mainBody.getElementById("content"); 1344 1345 NavbarItem[] navbarItems; 1346 void addSubPage(Fn)(Fn fn, string name, string linkTxt) { 1347 const fname = buildPath(ctx.state.get.logDir, name ~ HtmlStyle.ext); 1348 logger.infof("Generating %s (%s)", linkTxt, name); 1349 File(fname, "w").write(fn()); 1350 navbarItems ~= NavbarItem(linkTxt, fname.baseName); 1351 } 1352 1353 // content must be added in a specific order such as statistics first 1354 SubContent[string] subContent; 1355 foreach (sc; ctx.state.get.subContent) 1356 subContent[sc.tag] = sc; 1357 void addContent(string tag) { 1358 auto item = subContent[tag]; 1359 navbarItems ~= NavbarItem(item.name, tag); 1360 content.addChild(new RawSource(index, item.content)); 1361 subContent.remove(tag); 1362 } 1363 1364 addContent("#overview"); 1365 // add files here to force it to always be after the overview. 1366 navbarItems ~= NavbarItem("Files", "#files"); 1367 1368 foreach (tag; subContent.byKey.array.sort) 1369 addContent(tag); 1370 1371 foreach (sp; ctx.state.get.subPages.sort!((a, b) => a.fileName < b.fileName)) { 1372 const link = relativePath(sp.fileName, ctx.state.get.logDir); 1373 navbarItems ~= NavbarItem(sp.linkTxt, link); 1374 } 1375 1376 // keep 1377 if (ReportSection.treemap in ctx.state.get.sections) { 1378 addSubPage(() => makeTreeMapPage(ctx.state.get.files), "tree_map", "Treemap"); 1379 } 1380 1381 ctx.state.get.files.toIndex(content, HtmlStyle.fileDir); 1382 1383 addNavbarItems(navbarItems, index.mainBody.getElementById("navbar-sidebar")); 1384 1385 File(buildPath(ctx.state.get.logDir, "index" ~ HtmlStyle.ext), "w").write( 1386 index.toPrettyString); 1387 } 1388 1389 self.exceptionHandler = toDelegate(&logExceptionHandler); 1390 send(self, InitMsg.init, dbPath); 1391 return impl(self, &init_, capture(st), &startFileReportes, capture(st), 1392 &waitForDone, capture(st), &checkDone, capture(st), &genIndex, 1393 capture(st), &startAnalyzers, capture(st), &indexWait, capture(st)); 1394 } 1395 1396 void runAnalyzer(alias fn, Args...)(OverviewActor.Impl self, FlowControlActor.Address flow, 1397 AnalyzeReportCollectorActor.Address collector, SubPage sp, 1398 AbsolutePath dbPath, auto ref Args args) @trusted { 1399 // keep params separated because it is easier to forward the captured arguments to `fn`. 1400 auto params = tuple(args); 1401 auto ctx = tuple!("self", "collector", "sp", "db")(self, collector, sp, dbPath); 1402 1403 // wait for flow to return a token. 1404 // then start the analyzer and send the result to the collector. 1405 send(collector, StartReporterMsg.init); 1406 1407 self.request(flow, infTimeout).send(TakeTokenMsg.init).capture(params, ctx) 1408 .then((ref Tuple!(typeof(params), typeof(ctx)) ctx, my.actor.utility.limiter.Token _) { 1409 // actor spawned in the system that will run the analyze. Uses a 1410 // dynamic actor because then we do not need to make an interface. 1411 // It should be OK because it is only used here, not as a generic 1412 // actor. The "type checking" is done when `fn` is called which 1413 // ensure that the captured parameters match. 1414 ctx[1].self.homeSystem.spawn((Actor* self, typeof(params) params, typeof(ctx[1]) ctx) { 1415 // tells the actor to actually do the work 1416 send(self, self, ctx.db, ctx.collector, ctx.sp); 1417 return impl(self, (ref typeof(params) ctx, Actor* self, AbsolutePath dbPath, 1418 AnalyzeReportCollectorActor.Address collector, SubPage sp) { 1419 auto db = Database.make(dbPath); 1420 auto content = fn(db, ctx.expand); 1421 File(sp.fileName, "w").write(content); 1422 send(collector, sp); 1423 self.shutdown; 1424 }, capture(params)); 1425 }, ctx[0], ctx[1]); 1426 }); 1427 } 1428 1429 void runAnalyzer(alias fn, Args...)(OverviewActor.Impl self, FlowControlActor.Address flow, 1430 AnalyzeReportCollectorActor.Address collector, SubContent sc, 1431 AbsolutePath dbPath, auto ref Args args) @trusted { 1432 import dextool.plugin.mutate.backend.report.html.tmpl : tmplBasicPage; 1433 1434 // keep params separated because it is easier to forward the captured arguments to `fn`. 1435 auto params = tuple(args); 1436 auto ctx = tuple!("self", "collector", "sc", "db")(self, collector, sc, dbPath); 1437 1438 // wait for flow to return a token. 1439 // then start the analyzer and send the result to the collector. 1440 send(collector, StartReporterMsg.init); 1441 1442 self.request(flow, infTimeout).send(TakeTokenMsg.init).capture(params, ctx) 1443 .then((ref Tuple!(typeof(params), typeof(ctx)) ctx, my.actor.utility.limiter.Token _) { 1444 // actor spawned in the system that will run the analyze. Uses a 1445 // dynamic actor because then we do not need to make an interface. 1446 // It should be OK because it is only used here, not as a generic 1447 // actor. The "type checking" is done when `fn` is called which 1448 // ensure that the captured parameters match. 1449 ctx[1].self.homeSystem.spawn((Actor* self, typeof(params) params, typeof(ctx[1]) ctx) { 1450 // tells the actor to actually do the work 1451 send(self, self, ctx.db, ctx.collector, ctx.sc); 1452 return impl(self, (ref typeof(params) ctx, Actor* self, AbsolutePath dbPath, 1453 AnalyzeReportCollectorActor.Address collector, SubContent sc) { 1454 auto db = Database.make(dbPath); 1455 auto doc = tmplBasicPage; 1456 auto root = doc.mainBody.addChild("div"); 1457 fn(db, sc.tag, root, ctx.expand); 1458 sc.content = root.toPrettyString; 1459 send(collector, sc); 1460 self.shutdown; 1461 }, capture(params)); 1462 }, ctx[0], ctx[1]); 1463 }); 1464 }