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