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.memOverload
78         : "The test suite where terminated because the system memory limit triggered.",
79         MutantStatus.noCoverage: "The mutant is never executed by the test suite.",
80         MutantStatus.equivalent
81         : "No change in the test case binaries happens when the mutant is injected and compiled.",
82         MutantStatus.skipped: "The mutant is skipped because another mutant that covers it survived (is alive).",
83         MutantStatus.nomut
84         : "The mutant is manually marked as not interesting. There is no intention of writing a test to kill it."
85     ];
86 
87     statusColor = cast(immutable)[
88         //Mutation.Status.unknown:,
89         // light red
90         MutantStatus.alive: "background-color: #ff9980",
91         // light green
92         MutantStatus.killed: "background-color: #b3ff99",
93         MutantStatus.killedByCompiler: "background-color: #b3ff99",
94         MutantStatus.timeout: "background-color: #b3ff99",
95         MutantStatus.memOverload: "background-color: #b3ff99",
96         MutantStatus.noCoverage: "background-color: #ff9980",
97         //Mutation.Status.equivalent:,
98         //Mutation.Status.skipped:,
99     ];
100 }
101 
102 void makeAllMutantsPage(ref Database db, const(Mutation.Kind)[] kinds, const AbsolutePath pageFname) @system {
103     auto doc = tmplBasicPage.dashboardCss;
104     scope (success)
105         () {
106         import std.stdio : File;
107 
108         auto fout = File(pageFname, "w");
109         fout.write(doc.toPrettyString);
110     }();
111 
112     doc.title(format("Mutants %s", Clock.currTime));
113     doc.mainBody.setAttribute("onload", "init()");
114 
115     {
116         auto data = dashboard();
117         auto style = doc.root.childElements("head")[0].addChild("style");
118         style.addChild(new RawSource(doc, data.bootstrapCss.get));
119         style.addChild(new RawSource(doc, data.dashboardCss.get));
120         style.addChild(new RawSource(doc, tmplDefaultCss));
121 
122         auto script = doc.root.childElements("head")[0].addChild("script");
123         script.addChild(new RawSource(doc, jsIndex));
124     }
125 
126     auto root = doc.mainBody;
127     root.addChild("p",
128             "Priority: how important it is to kill the mutant. It is based on modified source code size.");
129     root.addChild("p",
130             "ExitCode: the exit code of the test suite when the mutant where killed. 1: normal");
131     root.addChild("p",
132             "Tests: number of tests that killed the mutant (failed when it was executed).");
133     root.addChild("p", "Tested: date when the mutant was last tested/executed.");
134 
135     const tabGroupName = "mutant_status";
136     Element[MutantStatus] tabLink;
137 
138     { // tab links
139         auto tab = root.addChild("div").addClass("tab");
140         foreach (const status; [EnumMembers!MutantStatus]) {
141             auto b = tab.addChild("button").addClass("tablinks")
142                 .addClass("tablinks_" ~ tabGroupName);
143             b.setAttribute("onclick", format!`openTab(event, '%s', '%s')`(status, tabGroupName));
144             b.appendText(status.to!string);
145             tabLink[status] = b;
146         }
147     }
148 
149     Table[MutantStatus] tabContent;
150     foreach (const status; [EnumMembers!MutantStatus]) {
151         auto div = root.addChild("div").addClass("tabcontent")
152             .addClass("tabcontent_" ~ tabGroupName).setAttribute("id", status.to!string);
153         div.addChild("p", statusDescription[status]);
154         tabContent[status] = tmplSortableTable(div, [
155                 "Link", "Priority", "ExitCode", "Tests", "Tested"
156                 ]);
157     }
158 
159     long[MutantStatus] statusCnt;
160     addMutants(db, kinds, tabContent, statusCnt);
161 
162     foreach (a; statusCnt.byKeyValue) {
163         tabLink[a.key].appendText(format!" %s"(a.value));
164         if (auto c = a.key in statusColor)
165             tabLink[a.key].style = *c;
166     }
167 }
168 
169 void addMutants(ref Database db, const(Mutation.Kind)[] kinds,
170         ref Table[MutantStatus] content, ref long[MutantStatus] statusCnt) @system {
171     import std.path : buildPath;
172     import dextool.plugin.mutate.backend.database : IterateMutantRow2, MutationId;
173 
174     static string toLinkPath(Path path, MutationId id) {
175         return format!"%s#%s"(buildPath(HtmlStyle.fileDir, pathToHtmlLink(path)), id);
176     }
177 
178     void mutant(ref const IterateMutantRow2 mut) {
179         const status = () {
180             if (mut.attrs.isNoMut)
181                 return MutantStatus.nomut;
182             return toStatus(mut.mutant.status);
183         }();
184 
185         statusCnt[status] += 1;
186         auto r = content[status].appendRow;
187 
188         r.addChild("td").addChild("a", format("%s:%s", mut.file,
189                 mut.sloc.line)).href = toLinkPath(mut.file, mut.id);
190         r.addChild("td", mut.prio.get.to!string);
191         r.addChild("td", toString(mut.exitStatus));
192         r.addChild("td", mut.killedByTestCases.to!string);
193         r.addChild("td", mut.mutant.status == Mutation.Status.unknown ? "" : mut.tested.toShortDate);
194     }
195 
196     db.iterateMutants(kinds, &mutant);
197 }
198 
199 void makeHighInterestMutants(ref Database db, const(Mutation.Kind)[] kinds,
200         typeof(ConfigReport.highInterestMutantsNr) showInterestingMutants, Element root) @trusted {
201     import std.path : buildPath;
202     import dextool.plugin.mutate.backend.report.html.utility : pathToHtmlLink;
203 
204     const sample = reportSelectedAliveMutants(db, kinds, showInterestingMutants.get);
205     if (sample.highestPrio.empty)
206         return;
207 
208     DashboardCss.h3(root, "High Interest Mutants");
209 
210     if (sample.highestPrio.length != 0) {
211         root.addChild("p", format("This list the %s mutants that affect the most source code and has survived.",
212                 sample.highestPrio.length));
213         auto tbl_container = root.addChild("div").addClass("tbl_container");
214         auto tbl = tmplDefaultTable(tbl_container, [
215                 "Link", "Tested", "Priority"
216                 ]);
217 
218         foreach (const mutst; sample.highestPrio) {
219             const mut = sample.mutants[mutst.statusId];
220             auto r = tbl.appendRow();
221             r.addChild("td").addChild("a", format("%s:%s", mut.file,
222                     mut.sloc.line)).href = format("%s#%s", buildPath(HtmlStyle.fileDir,
223                     pathToHtmlLink(mut.file)), mut.id.get);
224             r.addChild("td", mutst.updated.toString);
225             r.addChild("td", mutst.prio.get.to!string);
226         }
227     }
228 }