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