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