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