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