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