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%)", "&nbsp;".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 }