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 }