1 /**
2 Copyright: Copyright (c) 2021, 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.page_mutant;
11 
12 import logger = std.experimental.logger;
13 import std.array : empty;
14 import std.conv : to;
15 import std.datetime : Clock;
16 import std.format : format;
17 import std.traits : EnumMembers;
18 
19 import arsd.dom : Document, Element, require, Table, RawSource, Link;
20 import my.path : AbsolutePath, Path;
21 
22 import dextool.cachetools;
23 import dextool.plugin.mutate.backend.database : Database;
24 import dextool.plugin.mutate.backend.report.analyzers : reportSelectedAliveMutants;
25 import dextool.plugin.mutate.backend.report.html.constants : HtmlStyle = Html, DashboardCss;
26 import dextool.plugin.mutate.backend.report.html.constants;
27 import dextool.plugin.mutate.backend.report.html.tmpl : tmplBasicPage,
28     tmplDefaultTable, dashboardCss, tmplDefaultMatrixTable, tmplSortableTable;
29 import dextool.plugin.mutate.backend.report.html.utility : pathToHtmlLink, toShortDate;
30 import dextool.plugin.mutate.backend.resource;
31 import dextool.plugin.mutate.backend.type : Mutation, toString;
32 import dextool.plugin.mutate.config : ConfigReport;
33 import dextool.plugin.mutate.type : MutationKind;
34 
35 @safe:
36 
37 void makeMutantPage(ref Database db, string tag, Element root, ref const ConfigReport conf,
38         const(Mutation.Kind)[] kinds, const AbsolutePath mutantPageFname) @trusted {
39     DashboardCss.h2(root.addChild(new Link(tag, null)).setAttribute("id", tag[1 .. $]), "Mutants");
40 
41     root.addChild("a", "All mutants").href = mutantPageFname.baseName;
42 
43     makeAllMutantsPage(db, kinds, mutantPageFname);
44     makeHighInterestMutants(db, kinds, conf.highInterestMutantsNr, root);
45 }
46 
47 private:
48 
49 string mixinMutantStatus() {
50     string s;
51     s ~= "enum MutantStatus {";
52     foreach (a; [EnumMembers!(Mutation.Status)])
53         s ~= a.to!string ~ ",";
54     s ~= "nomut";
55     s ~= "}";
56     return s;
57 }
58 
59 mixin(mixinMutantStatus);
60 
61 MutantStatus toStatus(Mutation.Status s) {
62     return cast(MutantStatus) s;
63 }
64 
65 immutable string[MutantStatus] statusDescription;
66 immutable string[MutantStatus] statusColor;
67 
68 shared static this() @trusted {
69     statusDescription = cast(immutable)[
70         MutantStatus.unknown: "Mutants that haven't been tested yet.",
71         MutantStatus.alive: "No test case failed when the mutant is tested.",
72         MutantStatus.killed: "At least one test case fail when the mutant is tested.",
73         MutantStatus.killedByCompiler
74         : "The compiler found and killed the mutant.",
75         MutantStatus.timeout
76         : "The test suite never terminate, infinite loop, when the mutant is tested.",
77         MutantStatus.noCoverage: "The mutant is never executed by the test suite.",
78         MutantStatus.equivalent
79         : "No change in the test case binaries happens when the mutant is injected and compiled.",
80         MutantStatus.skipped: "The mutant is skipped because another mutant that covers it survived (is alive).",
81         MutantStatus.nomut
82         : "The mutant is manually marked as not interesting. There is no intention of writing a test to kill it."
83     ];
84 
85     statusColor = cast(immutable)[
86         //Mutation.Status.unknown:,
87         // light red
88         MutantStatus.alive: "background-color: #ff9980",
89         // light green
90         MutantStatus.killed: "background-color: #b3ff99",
91         MutantStatus.killedByCompiler: "background-color: #b3ff99",
92         MutantStatus.timeout: "background-color: #b3ff99",
93         MutantStatus.noCoverage: "background-color: #ff9980",
94         //Mutation.Status.equivalent:,
95         //Mutation.Status.skipped:,
96     ];
97 }
98 
99 void makeAllMutantsPage(ref Database db, const(Mutation.Kind)[] kinds, const AbsolutePath pageFname) @system {
100     auto doc = tmplBasicPage.dashboardCss;
101     scope (success)
102         () {
103         import std.stdio : File;
104 
105         auto fout = File(pageFname, "w");
106         fout.write(doc.toPrettyString);
107     }();
108 
109     doc.title(format("Mutants %s", Clock.currTime));
110     doc.mainBody.setAttribute("onload", "init()");
111 
112     {
113         auto data = dashboard();
114         auto style = doc.root.childElements("head")[0].addChild("style");
115         style.addChild(new RawSource(doc, data.bootstrapCss.get));
116         style.addChild(new RawSource(doc, data.dashboardCss.get));
117         style.addChild(new RawSource(doc, tmplDefaultCss));
118 
119         auto script = doc.root.childElements("head")[0].addChild("script");
120         script.addChild(new RawSource(doc, jsIndex));
121     }
122 
123     auto root = doc.mainBody;
124     root.addChild("p",
125             "Priority: how important it is to kill the mutant. It is based on modified source code size.");
126     root.addChild("p",
127             "ExitCode: the exit code of the test suite when the mutant where killed. 1: normal");
128     root.addChild("p",
129             "Tests: number of tests that killed the mutant (failed when it was executed).");
130     root.addChild("p", "Tested: date when the mutant was last tested/executed.");
131 
132     const tabGroupName = "mutant_status";
133     Element[MutantStatus] tabLink;
134 
135     { // tab links
136         auto tab = root.addChild("div").addClass("tab");
137         foreach (const status; [EnumMembers!MutantStatus]) {
138             auto b = tab.addChild("button").addClass("tablinks")
139                 .addClass("tablinks_" ~ tabGroupName);
140             b.setAttribute("onclick", format!`openTab(event, '%s', '%s')`(status, tabGroupName));
141             b.appendText(status.to!string);
142             tabLink[status] = b;
143         }
144     }
145 
146     Table[MutantStatus] tabContent;
147     foreach (const status; [EnumMembers!MutantStatus]) {
148         auto div = root.addChild("div").addClass("tabcontent")
149             .addClass("tabcontent_" ~ tabGroupName).setAttribute("id", status.to!string);
150         div.addChild("p", statusDescription[status]);
151         tabContent[status] = tmplSortableTable(div, [
152                 "Link", "Priority", "ExitCode", "Tests", "Tested"
153                 ]);
154     }
155 
156     long[MutantStatus] statusCnt;
157     addMutants(db, kinds, tabContent, statusCnt);
158 
159     foreach (a; statusCnt.byKeyValue) {
160         tabLink[a.key].appendText(format!" %s"(a.value));
161         if (auto c = a.key in statusColor)
162             tabLink[a.key].style = *c;
163     }
164 }
165 
166 void addMutants(ref Database db, const(Mutation.Kind)[] kinds,
167         ref Table[MutantStatus] content, ref long[MutantStatus] statusCnt) @system {
168     import std.path : buildPath;
169     import dextool.plugin.mutate.backend.database : IterateMutantRow2, MutationId;
170 
171     static string toLinkPath(Path path, MutationId id) {
172         return format!"%s#%s"(buildPath(HtmlStyle.fileDir, pathToHtmlLink(path)), id);
173     }
174 
175     void mutant(ref const IterateMutantRow2 mut) {
176         const status = () {
177             if (mut.attrs.isNoMut)
178                 return MutantStatus.nomut;
179             return toStatus(mut.mutant.status);
180         }();
181 
182         statusCnt[status] += 1;
183         auto r = content[status].appendRow;
184 
185         r.addChild("td").addChild("a", format("%s:%s", mut.file,
186                 mut.sloc.line)).href = toLinkPath(mut.file, mut.id);
187         r.addChild("td", mut.prio.get.to!string);
188         r.addChild("td", toString(mut.exitStatus));
189         r.addChild("td", mut.killedByTestCases.to!string);
190         r.addChild("td", mut.mutant.status == Mutation.Status.unknown ? "" : mut.tested.toShortDate);
191     }
192 
193     db.iterateMutants(kinds, &mutant);
194 }
195 
196 void makeHighInterestMutants(ref Database db, const(Mutation.Kind)[] kinds,
197         typeof(ConfigReport.highInterestMutantsNr) showInterestingMutants, Element root) @trusted {
198     import std.path : buildPath;
199     import dextool.plugin.mutate.backend.report.html.utility : pathToHtmlLink;
200 
201     const sample = reportSelectedAliveMutants(db, kinds, showInterestingMutants.get);
202     if (sample.highestPrio.empty)
203         return;
204 
205     DashboardCss.h3(root, "High Interest Mutants");
206 
207     if (sample.highestPrio.length != 0) {
208         root.addChild("p", format("This list the %s mutants that affect the most source code and has survived.",
209                 sample.highestPrio.length));
210         auto tbl_container = root.addChild("div").addClass("tbl_container");
211         auto tbl = tmplDefaultTable(tbl_container, [
212                 "Link", "Tested", "Priority"
213                 ]);
214 
215         foreach (const mutst; sample.highestPrio) {
216             const mut = sample.mutants[mutst.statusId];
217             auto r = tbl.appendRow();
218             r.addChild("td").addChild("a", format("%s:%s", mut.file,
219                     mut.sloc.line)).href = format("%s#%s", buildPath(HtmlStyle.fileDir,
220                     pathToHtmlLink(mut.file)), mut.id.get);
221             r.addChild("td", mutst.updated.toString);
222             r.addChild("td", mutst.prio.get.to!string);
223         }
224     }
225 }