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.exception : collectException;
16 import std.format : format;
17 import std.stdio : File;
18 import std.utf : toUTF8, byChar;
19 
20 import arsd.dom : Document, Element, require, Table, RawSource, Link;
21 import my.set;
22 
23 import dextool.plugin.mutate.backend.database : Database, FileRow, FileMutantRow, MutationId;
24 import dextool.plugin.mutate.backend.diff_parser : Diff;
25 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
26 import dextool.plugin.mutate.backend.report.type : FileReport, FilesReporter;
27 import dextool.plugin.mutate.backend.type : Mutation, Offset, SourceLoc, Token;
28 import dextool.plugin.mutate.config : ConfigReport;
29 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportSection;
30 import dextool.type : AbsolutePath, Path;
31 
32 import dextool.plugin.mutate.backend.report.html.constants : HtmlStyle = Html, DashboardCss;
33 import dextool.plugin.mutate.backend.report.html.tmpl;
34 import dextool.plugin.mutate.backend.resource;
35 
36 version (unittest) {
37     import unit_threaded : shouldEqual;
38 }
39 
40 @safe:
41 
42 void report(ref Database db, const MutationKind[] userKinds, const ConfigReport conf,
43         FilesysIO fio, ref Diff diff) {
44     import dextool.plugin.mutate.backend.database : FileMutantRow;
45     import dextool.plugin.mutate.backend.mutation_type : toInternal;
46     import dextool.plugin.mutate.backend.utility : Profile;
47 
48     const kinds = toInternal(userKinds);
49 
50     auto fps = new ReportHtml(kinds, conf, fio, diff);
51 
52     fps.mutationKindEvent(userKinds);
53 
54     foreach (f; db.getDetailedFiles) {
55         auto profile = Profile("generate report for " ~ f.file);
56 
57         fps.getFileReportEvent(db, f);
58 
59         void fn(const ref FileMutantRow row) {
60             fps.fileMutantEvent(row);
61         }
62 
63         db.iterateFileMutants(kinds, f.file, &fn);
64         generateFile(db, fps.ctx);
65     }
66 
67     auto profile = Profile("post process report");
68     fps.postProcessEvent(db);
69 }
70 
71 struct FileIndex {
72     import dextool.plugin.mutate.backend.report.analyzers : MutationScore;
73 
74     Path path;
75     string display;
76     MutationScore stat;
77 }
78 
79 @safe final class ReportHtml {
80     import std.stdio : File, writefln, writeln;
81     import undead.xml : encode;
82 
83     const Mutation.Kind[] kinds;
84     const ConfigReport conf;
85 
86     /// The base directory of logdirs
87     const AbsolutePath logDir;
88     /// Reports for each file
89     const AbsolutePath logFilesDir;
90 
91     /// What the user configured.
92     MutationKind[] humanReadableKinds;
93     Set!ReportSection sections;
94 
95     FilesysIO fio;
96 
97     // all files that have been produced.
98     Appender!(FileIndex[]) files;
99 
100     // the context for the file that is currently being processed.
101     FileCtx ctx;
102 
103     // Report alive mutants in this section
104     Diff diff;
105 
106     this(const(Mutation.Kind)[] kinds, const ConfigReport conf, FilesysIO fio, ref Diff diff) {
107         import std.path : buildPath;
108 
109         this.kinds = kinds;
110         this.fio = fio;
111         this.conf = conf;
112         this.logDir = buildPath(conf.logDir, HtmlStyle.dir).Path.AbsolutePath;
113         this.logFilesDir = buildPath(this.logDir, HtmlStyle.fileDir).Path.AbsolutePath;
114         this.diff = diff;
115 
116         sections = conf.reportSection.toSet;
117     }
118 
119     void mutationKindEvent(const MutationKind[] k) {
120         import std.file : mkdirRecurse;
121 
122         humanReadableKinds = k.dup;
123         mkdirRecurse(this.logDir);
124         mkdirRecurse(this.logFilesDir);
125     }
126 
127     void getFileReportEvent(ref Database db, const ref FileRow fr) {
128         import std.path : buildPath;
129         import std.stdio : File;
130         import dextool.plugin.mutate.backend.report.html.page_files;
131         import dextool.plugin.mutate.backend.report.analyzers : reportScore;
132 
133         const original = fr.file.dup.pathToHtml;
134         const report = (original ~ HtmlStyle.ext).Path;
135 
136         auto stat = reportScore(db, kinds, fr.file);
137 
138         files.put(FileIndex(report, fr.file, stat));
139 
140         const out_path = buildPath(logFilesDir, report).Path.AbsolutePath;
141 
142         auto raw = fio.makeInput(AbsolutePath(buildPath(fio.getOutputDir, fr.file)));
143 
144         auto tc_info = db.getAllTestCaseInfo2(fr.id, kinds);
145 
146         ctx = FileCtx.make(original, fr.id, raw, tc_info);
147         ctx.processFile = fr.file;
148         ctx.out_ = File(out_path, "w");
149         ctx.span = Spanner(tokenize(fio.getOutputDir, fr.file));
150     }
151 
152     void fileMutantEvent(const ref FileMutantRow fr) {
153         import dextool.plugin.mutate.backend.generate_mutant : makeMutationText;
154 
155         // TODO unnecessary to create the mutation text here.
156         // Move it to endFileEvent. This is inefficient.
157 
158         // the mutation text has been found to contain '\0' characters when the
159         // mutant span multiple lines. These null characters render badly in
160         // the html report.
161         static string cleanup(const(char)[] raw) @safe nothrow {
162             return raw.byChar.filter!(a => a != '\0').array.idup;
163         }
164 
165         auto txt = makeMutationText(ctx.raw, fr.mutationPoint.offset, fr.mutation.kind, fr.lang);
166         ctx.span.put(FileMutant(fr.id, fr.mutationPoint.offset,
167                 cleanup(txt.original), cleanup(txt.mutation), fr.mutation));
168     }
169 
170     void postProcessEvent(ref Database db) @trusted {
171         import std.datetime : Clock;
172         import std.path : buildPath, baseName;
173         import dextool.plugin.mutate.backend.report.html.page_dead_test_case;
174         import dextool.plugin.mutate.backend.report.html.page_diff;
175         import dextool.plugin.mutate.backend.report.html.page_long_term_view;
176         import dextool.plugin.mutate.backend.report.html.page_minimal_set;
177         import dextool.plugin.mutate.backend.report.html.page_nomut;
178         import dextool.plugin.mutate.backend.report.html.page_stats;
179         import dextool.plugin.mutate.backend.report.html.page_test_case_full_overlap;
180         import dextool.plugin.mutate.backend.report.html.page_test_case_similarity;
181         import dextool.plugin.mutate.backend.report.html.page_test_case_stat;
182         import dextool.plugin.mutate.backend.report.html.page_test_case_unique;
183         import dextool.plugin.mutate.backend.report.html.page_test_group_similarity;
184         import dextool.plugin.mutate.backend.report.html.page_test_groups;
185         import dextool.plugin.mutate.backend.report.html.page_tree_map;
186         import dextool.plugin.mutate.backend.report.html.trend;
187 
188         auto index = makeDashboard;
189         index.title = format("Mutation Testing Report %(%s %) %s",
190                 humanReadableKinds, Clock.currTime);
191 
192         auto content = index.mainBody.getElementById("content");
193 
194         NavbarItem[] navbarItems;
195         void addSubPage(Fn)(Fn fn, string name, string linkTxt) {
196             const fname = buildPath(logDir, name ~ HtmlStyle.ext);
197             logger.infof("Generating %s (%s)", linkTxt, name);
198             File(fname, "w").write(fn());
199             navbarItems ~= NavbarItem(linkTxt, fname.baseName);
200         }
201 
202         void addContent(Fn)(Fn fn, string name, string tag) {
203             logger.infof("Generating %s", name);
204             fn(tag);
205             navbarItems ~= NavbarItem(name, tag);
206         }
207 
208         addContent((string tag) => makeStats(db, kinds, tag, content), "Overview", "#overview");
209         navbarItems ~= NavbarItem("Files", "#files"); // add files here to force it to always be after the overview
210 
211         addContent((string tag) => makeHighInterestMutants(db, kinds,
212                 conf.highInterestMutantsNr, tag, content), "High Interest", "#high_interest");
213 
214         if (!diff.empty) {
215             addSubPage(() => makeDiffView(db, conf, humanReadableKinds, kinds,
216                     diff, fio.getOutputDir), "diff_view", "Diff View");
217         }
218         if (ReportSection.treemap in sections) {
219             addSubPage(() => makeTreeMapPage(files.data), "tree_map", "Treemap");
220         }
221         if (ReportSection.tc_stat in sections) {
222             addSubPage(() => makeTestCaseStats(db, conf, humanReadableKinds,
223                     kinds), "test_case_stat", "Test Case Statistics");
224         }
225         if (ReportSection.tc_groups in sections) {
226             addSubPage(() => makeTestGroups(db, conf, humanReadableKinds,
227                     kinds), "test_groups", "Test Groups");
228         }
229         addSubPage(() => makeNomut(db, conf, humanReadableKinds, kinds), "nomut", "NoMut Details");
230         if (ReportSection.tc_min_set in sections) {
231             addSubPage(() => makeMinimalSetAnalyse(db, conf, humanReadableKinds,
232                     kinds), "minimal_set", "Minimal Test Set");
233         }
234         if (ReportSection.tc_similarity in sections) {
235             addSubPage(() => makeTestCaseSimilarityAnalyse(db, conf, humanReadableKinds,
236                     kinds), "test_case_similarity", "Test Case Similarity");
237         }
238         if (ReportSection.tc_groups_similarity in sections) {
239             addSubPage(() => makeTestGroupSimilarityAnalyse(db, conf, humanReadableKinds,
240                     kinds), "test_group_similarity", "Test Group Similarity");
241         }
242         if (ReportSection.tc_unique in sections) {
243             addSubPage(() => makeTestCaseUnique(db, conf, humanReadableKinds,
244                     kinds), "test_case_unique", "Test Case Uniqueness");
245         }
246         if (ReportSection.tc_killed_no_mutants in sections) {
247             addContent((string tag) => makeDeadTestCase(db, kinds, tag, content),
248                     "Killed No Mutants Test Cases", "#killed_no_mutants_test_cases");
249         }
250         if (ReportSection.tc_full_overlap in sections
251                 || ReportSection.tc_full_overlap_with_mutation_id in sections) {
252             addSubPage(() => makeFullOverlapTestCase(db, conf, humanReadableKinds,
253                     kinds), "full_overlap_test_cases", "Full Overlap Test Cases");
254         }
255         if (ReportSection.trend in sections) {
256             addContent((string tag) => makeTrend(db, kinds, tag, content), "Trend", "#trend");
257         }
258 
259         files.data.toIndex(content, HtmlStyle.fileDir);
260 
261         addNavbarItems(navbarItems, index.mainBody.getElementById("navbar-sidebar"));
262 
263         File(buildPath(logDir, "index" ~ HtmlStyle.ext), "w").write(index.toPrettyString);
264     }
265 }
266 
267 @safe:
268 private:
269 
270 string toJson(string s) {
271     import std.json : JSONValue;
272 
273     return JSONValue(s).toString;
274 }
275 
276 struct FileCtx {
277     import std.stdio : File;
278     import blob_model : Blob;
279     import dextool.plugin.mutate.backend.database : FileId, TestCaseInfo2;
280 
281     Path processFile;
282     File out_;
283 
284     Spanner span;
285 
286     Document doc;
287 
288     // The text of the current file that is being processed.
289     Blob raw;
290 
291     /// Database ID for this file.
292     FileId fileId;
293 
294     /// Find the test cases that killed a mutant. They are sorted by most killed -> least killed.
295     TestCaseInfo[][MutationId] tcKilledMutant;
296 
297     /// All test cases in the file.
298     TestCaseInfo[] testCases;
299 
300     static FileCtx make(string title, FileId id, Blob raw, TestCaseInfo2[] tc_info) @trusted {
301         import dextool.plugin.mutate.backend.report.html.tmpl;
302 
303         auto r = FileCtx.init;
304         r.doc = tmplBasicPage.filesCss;
305         r.doc.title = title;
306         r.doc.mainBody.setAttribute("onload", "javascript:init();");
307 
308         auto s = r.doc.root.childElements("head")[0].addChild("style");
309         s.addChild(new RawSource(r.doc, tmplIndexStyle));
310 
311         s = r.doc.root.childElements("head")[0].addChild("script");
312         s.addChild(new RawSource(r.doc, jsSource));
313 
314         r.doc.mainBody.appendHtml(tmplIndexBody);
315 
316         r.fileId = id;
317 
318         r.raw = raw;
319 
320         typeof(tcKilledMutant) tmp;
321         foreach (a; tc_info) {
322             foreach (mut; a.killed) {
323                 tmp.update(mut, { return [TestCaseInfo(a.name, a.killed.length)]; },
324                         (ref TestCaseInfo[] v) => v ~= TestCaseInfo(a.name, a.killed.length));
325             }
326         }
327         r.testCases = tc_info.map!(a => TestCaseInfo(a.name, a.killed.length)).array;
328 
329         foreach (kv; tmp.byKeyValue) {
330             r.tcKilledMutant[kv.key] = kv.value.sort.array;
331         }
332 
333         return r;
334     }
335 
336     TestCaseInfo[] getTestCaseInfo(MutationId mutationId) @safe pure nothrow {
337         if (auto v = mutationId in tcKilledMutant)
338             return *v;
339         return null;
340     }
341 
342     static struct TestCaseInfo {
343         import dextool.plugin.mutate.backend.type : TestCase;
344 
345         TestCase name;
346         long killed;
347 
348         int opCmp(ref const typeof(this) s) @safe pure nothrow const @nogc scope {
349             if (killed < s.killed)
350                 return -1;
351             else if (killed > s.killed)
352                 return 1;
353             else if (name < s.name)
354                 return -1;
355             else if (name > s.name)
356                 return 1;
357             return 0;
358         }
359 
360         bool opEquals(ref const typeof(this) s) @safe pure nothrow const @nogc scope {
361             return name == s.name;
362         }
363 
364         size_t toHash() @safe nothrow const {
365             return name.toHash;
366         }
367     }
368 }
369 
370 auto tokenize(AbsolutePath base_dir, Path f) @trusted {
371     import std.path : buildPath;
372     import std.typecons : Yes;
373     import libclang_ast.context;
374     static import dextool.plugin.mutate.backend.utility;
375 
376     const fpath = buildPath(base_dir, f).Path.AbsolutePath;
377     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
378     return dextool.plugin.mutate.backend.utility.tokenize!(Yes.splitMultiLineTokens)(ctx, fpath);
379 }
380 
381 struct FileMutant {
382 nothrow:
383     static struct Text {
384         /// the original text that covers the offset.
385         string original;
386         /// The mutation text that covers the offset.
387         string mutation;
388     }
389 
390     MutationId id;
391     Offset offset;
392     Text txt;
393     Mutation mut;
394 
395     this(MutationId id, Offset offset, string original, string mutation, Mutation mut) {
396         import std.utf : validate;
397         import dextool.plugin.mutate.backend.type : invalidUtf8;
398 
399         this.id = id;
400         this.offset = offset;
401         this.mut = mut;
402 
403         try {
404             validate(original);
405             this.txt.original = original;
406         } catch (Exception e) {
407             this.txt.original = invalidUtf8;
408         }
409 
410         try {
411             validate(mutation);
412             // users prefer being able to see what has been removed.
413             if (mutation.length == 0)
414                 this.txt.mutation = "/* " ~ this.txt.original ~ " */";
415             else
416                 this.txt.mutation = mutation;
417         } catch (Exception e) {
418             this.txt.mutation = invalidUtf8;
419         }
420     }
421 
422     this(MutationId id, Offset offset, string original) {
423         this(id, offset, original, null, Mutation.init);
424     }
425 
426     string original() @safe pure nothrow const @nogc {
427         return txt.original;
428     }
429 
430     string mutation() @safe pure nothrow const @nogc {
431         return txt.mutation;
432     }
433 
434     int opCmp(ref const typeof(this) s) const @safe {
435         if (offset.begin > s.offset.begin)
436             return 1;
437         if (offset.begin < s.offset.begin)
438             return -1;
439         if (offset.end > s.offset.end)
440             return 1;
441         if (offset.end < s.offset.end)
442             return -1;
443         return 0;
444     }
445 }
446 
447 @("shall be possible to construct a FileMutant in @safe")
448 @safe unittest {
449     auto fmut = FileMutant(MutationId(1), Offset(1, 2), "smurf");
450 }
451 
452 /*
453 I get a mutant that have a start/end offset.
454 I have all tokens.
455 I can't write the html before I have all mutants for the offset.
456 Hmm potentially this mean that I can't write any html until I have analyzed all mutants for the file.
457 This must be so....
458 
459 How to do it?
460 
461 From reading https://stackoverflow.com/questions/11389627/span-overlapping-strings-in-a-paragraph
462 it seems that generating a <span..> for each token with multiple classes in them. A class for each mutant.
463 then they can be toggled on/off.
464 
465 a <href> tag to the beginning to jump to the mutant.
466 */
467 
468 /** Provide an interface to travers the tokens and get the overlapping mutants.
469  */
470 struct Spanner {
471     import std.container : RedBlackTree, redBlackTree;
472     import std.range : isOutputRange;
473 
474     alias BTree(T) = RedBlackTree!(T, "a < b", true);
475 
476     BTree!Token tokens;
477     BTree!FileMutant muts;
478 
479     this(Token[] tokens) @trusted {
480         this.tokens = new typeof(this.tokens);
481         this.muts = new typeof(this.muts)();
482 
483         this.tokens.insert(tokens);
484     }
485 
486     void put(const FileMutant fm) @trusted {
487         muts.insert(fm);
488     }
489 
490     SpannerRange toRange() @safe {
491         return SpannerRange(tokens, muts);
492     }
493 
494     string toString() @safe pure const {
495         auto buf = appender!string;
496         this.toString(buf);
497         return buf.data;
498     }
499 
500     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
501         import std.format : formattedWrite;
502         import std.range : zip, StoppingPolicy;
503         import std.string;
504         import std.algorithm : max;
505         import std.traits : Unqual;
506 
507         ulong sz;
508 
509         foreach (ref const t; zip(StoppingPolicy.longest, tokens[], muts[])) {
510             auto c0 = format("%s", cast(Unqual!(typeof(t[0]))) t[0]);
511             string c1;
512             if (t[1] != typeof(t[1]).init)
513                 c1 = format("%s", cast(Unqual!(typeof(t[1]))) t[1]);
514             sz = max(sz, c0.length, c1.length);
515             formattedWrite(w, "%s | %s\n", c0.rightJustify(sz), c1);
516         }
517     }
518 }
519 
520 @("shall be possible to construct a Spanner in @safe")
521 @safe unittest {
522     import std.algorithm;
523     import std.conv;
524     import std.range;
525     import clang.c.Index : CXTokenKind;
526 
527     auto toks = zip(iota(10), iota(10, 20)).map!(a => Token(CXTokenKind.comment,
528             Offset(a[0], a[1]), SourceLoc.init, SourceLoc.init, a[0].to!string)).retro.array;
529     auto span = Spanner(toks);
530 
531     span.put(FileMutant(MutationId(1), Offset(1, 10), "smurf"));
532     span.put(FileMutant(MutationId(1), Offset(9, 15), "donkey"));
533 
534     // TODO add checks
535 }
536 
537 /**
538  *
539  * # Overlap Cases
540  * 1. Perfekt overlap
541  * |--T--|
542  * |--M--|
543  *
544  * 2. Token enclosing mutant
545  * |---T--|
546  *   |-M-|
547  *
548  * 3. Mutant beginning inside a token
549  * |---T--|
550  *   |-M----|
551  *
552  * 4. Mutant overlapping multiple tokens.
553  * |--T--|--T--|
554  * |--M--------|
555  */
556 struct SpannerRange {
557     alias BTree = Spanner.BTree;
558 
559     BTree!Token tokens;
560     BTree!FileMutant muts;
561 
562     this(BTree!Token tokens, BTree!FileMutant muts) @safe {
563         this.tokens = tokens;
564         this.muts = muts;
565         dropMutants;
566     }
567 
568     Span front() @safe pure nothrow {
569         assert(!empty, "Can't get front of an empty range");
570         auto t = tokens.front;
571         if (muts.empty)
572             return Span(t);
573 
574         auto app = appender!(FileMutant[])();
575         foreach (m; muts) {
576             if (m.offset.begin < t.offset.end)
577                 app.put(m);
578             else
579                 break;
580         }
581 
582         return Span(t, app.data);
583     }
584 
585     void popFront() @safe {
586         assert(!empty, "Can't pop front of an empty range");
587         tokens.removeFront;
588         dropMutants;
589     }
590 
591     bool empty() @safe pure nothrow @nogc {
592         return tokens.empty;
593     }
594 
595     private void dropMutants() @safe {
596         if (tokens.empty)
597             return;
598 
599         // removing mutants that the tokens have "passed by"
600         const t = tokens.front;
601         auto r = muts[].filter!(a => a.offset.end <= t.offset.begin).array;
602         muts.removeKey(r);
603     }
604 }
605 
606 struct Span {
607     import std.range : isOutputRange;
608 
609     Token tok;
610     FileMutant[] muts;
611 
612     string toString() @safe pure const {
613         auto buf = appender!string;
614         toString(buf);
615         return buf.data;
616     }
617 
618     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
619         import std.format : formattedWrite;
620         import std.range : put;
621 
622         formattedWrite(w, "%s|%(%s %)", tok, muts);
623     }
624 }
625 
626 @("shall return a range grouping mutants by the tokens they overlap")
627 @safe unittest {
628     import std.algorithm;
629     import std.conv;
630     import std.range;
631     import clang.c.Index : CXTokenKind;
632 
633     auto offsets = zip(iota(0, 150, 10), iota(10, 160, 10)).map!(a => Offset(a[0], a[1])).array;
634 
635     auto toks = offsets.map!(a => Token(CXTokenKind.comment, a, SourceLoc.init,
636             SourceLoc.init, a.begin.to!string)).retro.array;
637     auto span = Spanner(toks);
638 
639     span.put(FileMutant(MutationId(2), Offset(11, 15), "token enclosing mutant"));
640     span.put(FileMutant(MutationId(3), Offset(31, 42), "mutant beginning inside a token"));
641     span.put(FileMutant(MutationId(4), Offset(50, 80), "mutant overlapping multiple tokens"));
642 
643     span.put(FileMutant(MutationId(5), Offset(90, 100), "1 multiple mutants for a token"));
644     span.put(FileMutant(MutationId(6), Offset(90, 110), "2 multiple mutants for a token"));
645     span.put(FileMutant(MutationId(1), Offset(120, 130), "perfect overlap"));
646 
647     auto res = span.toRange.array;
648     //logger.tracef("%(%s\n%)", res);
649     res[1].muts[0].id.get.shouldEqual(2);
650     res[2].muts.length.shouldEqual(0);
651     res[3].muts[0].id.get.shouldEqual(3);
652     res[4].muts[0].id.get.shouldEqual(3);
653     res[5].muts[0].id.get.shouldEqual(4);
654     res[6].muts[0].id.get.shouldEqual(4);
655     res[7].muts[0].id.get.shouldEqual(4);
656     res[8].muts.length.shouldEqual(0);
657     res[9].muts.length.shouldEqual(2);
658     res[9].muts[0].id.get.shouldEqual(5);
659     res[9].muts[1].id.get.shouldEqual(6);
660     res[10].muts[0].id.get.shouldEqual(6);
661     res[11].muts.length.shouldEqual(0);
662     res[12].muts[0].id.get.shouldEqual(1);
663     res[13].muts.length.shouldEqual(0);
664 }
665 
666 void toIndex(FileIndex[] files, Element root, string htmlFileDir) @trusted {
667     import std.algorithm : sort, filter;
668     import std.conv : to;
669     import std.path : buildPath;
670 
671     DashboardCss.h2(root.addChild(new Link("#files", null)).setAttribute("id", "files"), "Files");
672 
673     auto tbl = tmplSortableTable(root, [
674             "Path", "Score", "Alive", "NoMut", "Total", "Time (min)"
675             ]);
676 
677     // Users are not interested that files that contains zero mutants are shown
678     // in the list. It is especially annoying when they are marked with dark
679     // green.
680     bool hasSuppressed;
681     foreach (f; files.sort!((a, b) => a.path < b.path)) {
682         auto r = tbl.appendRow();
683         r.addChild("td").addChild("a", f.display).href = buildPath(htmlFileDir, f.path);
684 
685         const score = f.stat.score;
686         const style = () {
687             if (f.stat.killed == f.stat.total)
688                 return "background-color: green";
689             if (score < 0.3)
690                 return "background-color: red";
691             if (score < 0.5)
692                 return "background-color: salmon";
693             if (score < 0.8)
694                 return "background-color: lightyellow";
695             if (score < 1.0)
696                 return "background-color: lightgreen";
697             return null;
698         }();
699 
700         r.addChild("td", format!"%.3s"(score)).style = style;
701         r.addChild("td", f.stat.alive.to!string);
702         r.addChild("td", f.stat.aliveNoMut.to!string);
703         r.addChild("td", f.stat.total.to!string);
704         r.addChild("td", f.stat
705                 .totalTime
706                 .sum
707                 .total!"minutes"
708                 .to!string);
709 
710         hasSuppressed = hasSuppressed || f.stat.aliveNoMut != 0;
711     }
712 
713     if (hasSuppressed) {
714         root.addChild("p", "NoMut is the number of alive mutants in the file that are ignored.")
715             .appendText(" This increases the score.");
716     }
717 }
718 
719 /** Metadata about the span to be used to e.g. color it.
720  *
721  * Each span has a mutant that becomes activated when the user click on the
722  * span. The user most likely is interested in seeing **a** mutant that has
723  * survived on that point becomes the color is red.
724  *
725  * This is why the algorithm uses the same prio as the one for choosing
726  * color. These two are strongly correlated with each other.
727  */
728 struct MetaSpan {
729     // ordered in priority
730     enum StatusColor {
731         alive,
732         killed,
733         timeout,
734         killedByCompiler,
735         unknown,
736         none,
737         noCoverage
738     }
739 
740     StatusColor status;
741     string onClick;
742 
743     this(const(FileMutant)[] muts) {
744         immutable click_fmt2 = "ui_set_mut(%s)";
745         status = StatusColor.none;
746 
747         foreach (ref const m; muts) {
748             status = pickColor(m, status);
749             if (onClick.length == 0 && m.mut.status == Mutation.Status.alive) {
750                 onClick = format(click_fmt2, m.id.get);
751             }
752         }
753 
754         if (onClick.length == 0 && muts.length != 0) {
755             onClick = format(click_fmt2, muts[0].id.get);
756         }
757     }
758 }
759 
760 /// Choose a color for a mutant span by prioritizing alive mutants above all.
761 MetaSpan.StatusColor pickColor(const FileMutant m,
762         MetaSpan.StatusColor status = MetaSpan.StatusColor.none) {
763     final switch (m.mut.status) {
764     case Mutation.Status.noCoverage:
765         status = MetaSpan.StatusColor.noCoverage;
766         break;
767     case Mutation.Status.alive:
768         status = MetaSpan.StatusColor.alive;
769         break;
770     case Mutation.Status.killed:
771         if (status > MetaSpan.StatusColor.killed)
772             status = MetaSpan.StatusColor.killed;
773         break;
774     case Mutation.Status.killedByCompiler:
775         if (status > MetaSpan.StatusColor.killedByCompiler)
776             status = MetaSpan.StatusColor.killedByCompiler;
777         break;
778     case Mutation.Status.timeout:
779         if (status > MetaSpan.StatusColor.timeout)
780             status = MetaSpan.StatusColor.timeout;
781         break;
782     case Mutation.Status.unknown:
783         if (status > MetaSpan.StatusColor.unknown)
784             status = MetaSpan.StatusColor.unknown;
785         break;
786     }
787     return status;
788 }
789 
790 string toVisible(MetaSpan.StatusColor s) {
791     if (s == MetaSpan.StatusColor.none)
792         return null;
793     return format("status_%s", s);
794 }
795 
796 void generateFile(ref Database db, ref FileCtx ctx) @trusted {
797     import std.conv : to;
798     import std.range : repeat, enumerate;
799     import std.traits : EnumMembers;
800     import dextool.plugin.mutate.type : MutationKind;
801     import dextool.plugin.mutate.backend.database.type : MutantMetaData;
802     import dextool.plugin.mutate.backend.report.utility : window;
803     import dextool.plugin.mutate.backend.mutation_type : toUser;
804 
805     static struct MData {
806         MutationId id;
807         FileMutant.Text txt;
808         Mutation mut;
809         MutantMetaData metaData;
810     }
811 
812     auto root = ctx.doc.mainBody;
813     auto lines = root.addChild("table").setAttribute("id", "locs").setAttribute("cellpadding", "0");
814     auto line = lines.addChild("tr").addChild("td").setAttribute("id", "loc-1");
815     line.addClass("loc");
816 
817     line.addChild("span", "1:").addClass("line_nr");
818     auto mut_data = appender!(string[])();
819     mut_data.put("var g_muts_data = {};");
820     mut_data.put("g_muts_data[-1] = {'kind' : null, 'status' : null, 'testCases' : null, 'orgText' : null, 'mutText' : null, 'meta' : null};");
821 
822     // used to make sure that metadata about a mutant is only written onces
823     // to the global arrays.
824     Set!MutationId ids;
825     auto muts = appender!(MData[])();
826 
827     // this is the last location. It is used to calculate the num of
828     // newlines, detect when a line changes etc.
829     auto lastLoc = SourceLoc(1, 1);
830 
831     foreach (const s; ctx.span.toRange) {
832         if (s.tok.loc.line > lastLoc.line) {
833             lastLoc.column = 1;
834         }
835         auto meta = MetaSpan(s.muts);
836 
837         foreach (const i; 0 .. max(0, s.tok.loc.line - lastLoc.line)) {
838             line = lines.addChild("tr").addChild("td");
839             line.setAttribute("id", format("%s-%s", "loc", lastLoc.line + i + 1))
840                 .addClass("loc").addChild("span", format("%s:",
841                         lastLoc.line + i + 1)).addClass("line_nr");
842 
843             // force a newline in the generated html to improve readability
844             lines.appendText("\n");
845         }
846 
847         const spaces = max(0, s.tok.loc.column - lastLoc.column);
848         line.addChild(new RawSource(ctx.doc, format("%-(%s%)", "&nbsp;".repeat(spaces))));
849 
850         auto d0 = line.addChild("div").setAttribute("style", "display: inline;");
851         with (d0.addChild("span", s.tok.spelling)) {
852             addClass("original");
853             addClass(s.tok.toName);
854             if (auto v = meta.status.toVisible)
855                 addClass(v);
856             if (s.muts.length != 0)
857                 addClass(format("%(mutid%s %)", s.muts.map!(a => a.id)));
858             if (meta.onClick.length != 0)
859                 setAttribute("onclick", meta.onClick);
860         }
861 
862         foreach (m; s.muts.filter!(m => !ids.contains(m.id))) {
863             ids.add(m.id);
864 
865             const metadata = db.getMutantationMetaData(m.id);
866 
867             muts.put(MData(m.id, m.txt, m.mut, metadata));
868             {
869                 auto mutantHtmlTag = d0.addChild("span").addClass("mutant")
870                     .setAttribute("id", m.id.toString);
871                 if (m.mutation.canFind('\n')) {
872                     mutantHtmlTag.addChild("pre", m.mutation).addClass("mutant2");
873                 } else {
874                     mutantHtmlTag.appendText(m.mutation);
875                 }
876             }
877             d0.addChild("a").setAttribute("href", "#" ~ m.id.toString);
878 
879             auto testCases = ctx.getTestCaseInfo(m.id);
880             if (testCases.empty) {
881                 mut_data.put(format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : null, 'orgText' : %s, 'mutText' : %s, 'meta' : '%s'};",
882                         m.id, m.mut.kind.to!int, toUser(m.mut.kind).to!int,
883                         m.mut.status.to!ubyte, toJson(window(m.txt.original)),
884                         toJson(window(m.txt.mutation)), metadata.kindToString));
885             } else {
886                 mut_data.put(format("g_muts_data[%s] = {'kind' : %s, 'kindGroup' : %s, 'status' : %s, 'testCases' : [%('%s',%)'], 'orgText' : %s, 'mutText' : %s, 'meta' : '%s'};",
887                         m.id, m.mut.kind.to!int, toUser(m.mut.kind).to!int,
888                         m.mut.status.to!ubyte, testCases.map!(a => a.name),
889                         toJson(window(m.txt.original)),
890                         toJson(window(m.txt.mutation)), metadata.kindToString));
891             }
892         }
893         lastLoc = s.tok.locEnd;
894     }
895 
896     // make sure there is a newline before the script start to improve
897     // readability of the html document source.
898     root.appendText("\n");
899 
900     with (root.addChild("script")) {
901         // force a newline in the generated html to improve readability
902         appendText("\n");
903         addChild(new RawSource(ctx.doc, format("const MAX_NUM_TESTCASES = %s;",
904                 db.getDetectedTestCases.length)));
905         appendText("\n");
906         addChild(new RawSource(ctx.doc, format("const g_mutids = [%(%s,%)];",
907                 muts.data.map!(a => a.id))));
908         appendText("\n");
909         addChild(new RawSource(ctx.doc, format("const g_mut_st_map = [%('%s',%)'];",
910                 [EnumMembers!(Mutation.Status)])));
911         appendText("\n");
912         addChild(new RawSource(ctx.doc, format("const g_mut_kind_map = [%('%s',%)'];",
913                 [EnumMembers!(Mutation.Kind)])));
914         appendText("\n");
915         addChild(new RawSource(ctx.doc, format("const g_mut_kindGroup_map = [%('%s',%)'];",
916                 [EnumMembers!(MutationKind)])));
917         appendText("\n");
918 
919         // Creates a list of number of kills per testcase.
920         appendChild(new RawSource(ctx.doc, "var g_testcases_kills = {}"));
921         appendText("\n");
922         foreach (tc; ctx.testCases) {
923             appendChild(new RawSource(ctx.doc,
924                     format("g_testcases_kills['%s'] = [%s];", tc.name, tc.killed)));
925             appendText("\n");
926         }
927         appendChild(new RawSource(ctx.doc, mut_data.data.joiner("\n").toUTF8));
928         appendText("\n");
929     }
930 
931     try {
932         ctx.out_.write(ctx.doc.toString);
933     } catch (Exception e) {
934         logger.error(e.msg).collectException;
935         logger.error("Unable to generate a HTML report for ", ctx.processFile).collectException;
936     }
937 }
938 
939 Document makeDashboard() @trusted {
940     import dextool.plugin.mutate.backend.resource : dashboard, jsIndex;
941 
942     auto data = dashboard();
943 
944     auto doc = new Document(data.dashboardHtml.get);
945     auto style = doc.root.childElements("head")[0].addChild("style");
946     style.addChild(new RawSource(doc, data.bootstrapCss.get));
947     style.addChild(new RawSource(doc, data.dashboardCss.get));
948     style.addChild(new RawSource(doc, tmplDefaultCss));
949 
950     auto script = doc.root.childElements("head")[0].addChild("script");
951     script.addChild(new RawSource(doc, data.jquery.get));
952     script.addChild(new RawSource(doc, data.bootstrapJs.get));
953     script.addChild(new RawSource(doc, data.moment.get));
954     script.addChild(new RawSource(doc, data.chart.get));
955     script.addChild(new RawSource(doc, jsIndex));
956 
957     // jsIndex provide init()
958     doc.mainBody.setAttribute("onload", "init()");
959 
960     return doc;
961 }
962 
963 struct NavbarItem {
964     string name;
965     string link;
966 }
967 
968 void addNavbarItems(NavbarItem[] items, Element root) @trusted {
969     foreach (item; items) {
970         root.addChild("li").addChild(new Link(item.link, item.name));
971     }
972 }