1 /** 2 Copyright: Copyright (c) 2020, 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_test_case; 11 12 import logger = std.experimental.logger; 13 import std.algorithm : sort; 14 import std.array : empty; 15 import std.conv : to; 16 import std.datetime : Clock, dur, SysTime; 17 import std.format : format; 18 import std.path : buildPath; 19 import std.range : enumerate; 20 import std.stdio : File; 21 import std.traits : EnumMembers; 22 23 import arsd.dom : Element, RawSource, Link, Document, Table; 24 import my.optional; 25 import my.path : AbsolutePath; 26 import my.set; 27 28 import dextool.plugin.mutate.backend.database : Database, spinSql, MutationId, 29 TestCaseId, MutationStatusId, MutantInfo2; 30 import dextool.plugin.mutate.backend.report.analyzers : reportTestCaseUniqueness, 31 TestCaseUniqueness, reportTestCaseSimilarityAnalyse, 32 TestCaseSimilarityAnalyse, TestCaseClassifier, makeTestCaseClassifier, TestCaseMetadata; 33 import dextool.plugin.mutate.backend.report.html.constants : HtmlStyle = Html, DashboardCss; 34 import dextool.plugin.mutate.backend.report.html.tmpl : tmplBasicPage, 35 dashboardCss, tmplSortableTable, tmplDefaultTable; 36 import dextool.plugin.mutate.backend.report.html.utility : pathToHtmlLink, toShortDate; 37 import dextool.plugin.mutate.backend.resource; 38 import dextool.plugin.mutate.backend.type : Mutation, toString, TestCase; 39 import dextool.plugin.mutate.config : ConfigReport; 40 import dextool.plugin.mutate.type : MutationKind, ReportSection; 41 42 @safe: 43 44 void makeTestCases(ref Database db, string tag, Element root, ref const ConfigReport conf, 45 const(Mutation.Kind)[] kinds, TestCaseMetadata metaData, AbsolutePath testCasesDir) @trusted { 46 DashboardCss.h2(root.addChild(new Link(tag, null)).setAttribute("id", tag[1 .. $]), 47 "Test Cases"); 48 auto sections = conf.reportSection.toSet; 49 50 ReportData data; 51 52 if (ReportSection.tc_similarity in sections) 53 data.similaritiesData = reportTestCaseSimilarityAnalyse(db, kinds, 5); 54 55 data.addSuggestion = ReportSection.tc_suggestion in sections; 56 // 10 is magic number. feels good. 57 data.classifier = makeTestCaseClassifier(db, 10); 58 59 const tabGroupName = "testcase_class"; 60 Element[Classification] tabLink; 61 62 { // tab links 63 auto tab = root.addChild("div").addClass("tab"); 64 foreach (class_; [EnumMembers!Classification]) { 65 auto b = tab.addChild("button").addClass("tablinks") 66 .addClass("tablinks_" ~ tabGroupName); 67 b.setAttribute("onclick", format!`openTab(event, '%s', '%s')`(class_, tabGroupName)); 68 b.appendText(class_.to!string); 69 tabLink[class_] = b; 70 } 71 } 72 73 Table[Classification] tabContent; 74 foreach (a; [EnumMembers!Classification]) { 75 auto div = root.addChild("div").addClass("tabcontent") 76 .addClass("tabcontent_" ~ tabGroupName).setAttribute("id", a.to!string); 77 if (a == Classification.Redundant) 78 div.addChild("p", format(classDescription[a], data.classifier.threshold)); 79 else 80 div.addChild("p", classDescription[a]); 81 tabContent[a] = tmplSortableTable(div, ["Name", "Tests", "Killed"]); 82 } 83 84 long[Classification] classCnt; 85 foreach (tcId; spinSql!(() => db.testCaseApi.getDetectedTestCaseIds)) { 86 const name = spinSql!(() => db.testCaseApi.getTestCaseName(tcId)); 87 88 auto reportFname = name.pathToHtmlLink; 89 auto fout = File(testCasesDir ~ reportFname, "w"); 90 TestCaseSummary summary; 91 spinSql!(() { 92 // do all the heavy database interaction in a transaction to 93 // speedup by reduce locking. 94 auto t = db.transaction; 95 makeTestCasePage(db, kinds, name, tcId, (data.similaritiesData is null) 96 ? null : data.similaritiesData.similarities.get(tcId, null), 97 data, metaData, summary, fout); 98 }); 99 100 auto classification = classify(TestCase(name), summary, data.classifier, metaData); 101 classCnt[classification] += 1; 102 103 auto r = tabContent[classification].appendRow; 104 { 105 auto td = r.addChild("td"); 106 td.addChild("a", name).href = buildPath(HtmlStyle.testCaseDir, reportFname); 107 } 108 r.addChild("td", summary.score.to!string); 109 r.addChild("td", summary.kills.to!string); 110 } 111 112 foreach (a; classCnt.byKeyValue) { 113 tabLink[a.key].appendText(format!" %s"(a.value)); 114 if (auto c = a.key in classColor) 115 tabLink[a.key].style = *c; 116 } 117 } 118 119 private: 120 121 enum Classification { 122 Unique, 123 Redundant, 124 Buggy, 125 Normal 126 } 127 128 immutable string[Classification] classDescription; 129 immutable string[Classification] classColor; 130 131 shared static this() @trusted { 132 classDescription = cast(immutable)[ 133 Classification.Unique: "kills mutants that no other test case do.", 134 Classification.Redundant: "all mutants the test case kill are also killed by %s other test cases. The test case is probably redudant and thus can be removed.", 135 Classification.Buggy 136 : "zero killed mutants. The test case is most probably incorrect. Immediatly inspect the test case.", 137 Classification.Normal: "" 138 ]; 139 140 classColor = cast(immutable)[ 141 // light green 142 Classification.Unique: "background-color: #b3ff99", 143 // light orange 144 Classification.Redundant: "background-color: #ffc266", 145 // light red 146 Classification.Buggy: "background-color: #ff9980" 147 ]; 148 } 149 150 Classification classify(TestCase tc, const TestCaseSummary summary, 151 const ref TestCaseClassifier tclass, const ref TestCaseMetadata metaData) { 152 if (summary.kills == 0) 153 return Classification.Buggy; 154 if (summary.score == 1) 155 return Classification.Unique; 156 157 if (auto v = tc in metaData.redundant) { 158 if (*v) 159 return Classification.Redundant; 160 } else if (summary.score >= tclass.threshold) 161 return Classification.Redundant; 162 163 return Classification.Normal; 164 } 165 166 struct ReportData { 167 TestCaseSimilarityAnalyse similaritiesData; 168 TestCaseClassifier classifier; 169 170 bool addSuggestion; 171 } 172 173 struct TestCaseSummary { 174 long kills; 175 176 // min(f) where f is the number of test cases that killed a mutant. 177 // thus if a test case have one unique mutant the score is 1, none then it 178 // is the lowest of all mutant test case kills. 179 long score; 180 } 181 182 void makeTestCasePage(ref Database db, const(Mutation.Kind)[] kinds, const string name, 183 const TestCaseId tcId, TestCaseSimilarityAnalyse.Similarity[] similarities, 184 const ReportData rdata, TestCaseMetadata metaData, ref TestCaseSummary summary, ref File out_) @system { 185 import std.path : baseName; 186 import dextool.plugin.mutate.backend.type : TestCase; 187 188 auto doc = tmplBasicPage.dashboardCss; 189 scope (success) 190 out_.write(doc.toPrettyString); 191 192 doc.title(format("%s %s", name, Clock.currTime)); 193 doc.mainBody.setAttribute("onload", "init()"); 194 doc.root.childElements("head")[0].addChild("script").addChild(new RawSource(doc, jsIndex)); 195 196 doc.mainBody.addChild("h1").appendText(name); 197 if (auto v = TestCase(name) in metaData.loc) { 198 auto p = doc.mainBody.addChild("p"); 199 p.addChild("a", v.file.baseName).href = v.file; 200 if (v.line.hasValue) 201 p.appendText(" at line " ~ v.line.orElse(0u).to!string); 202 } 203 if (auto v = TestCase(name) in metaData.text) 204 doc.mainBody.addChild(new RawSource(doc, *v)); 205 206 doc.mainBody.addChild("h2").appendText("Killed"); 207 addKilledMutants(db, kinds, tcId, rdata, summary, doc.mainBody); 208 209 if (!similarities.empty) { 210 doc.mainBody.addChild("h2").appendText("Similarity"); 211 addSimilarity(db, similarities, doc.mainBody); 212 } 213 } 214 215 void addKilledMutants(ref Database db, const(Mutation.Kind)[] kinds, 216 const TestCaseId tcId, const ReportData rdata, ref TestCaseSummary summary, Element root) @system { 217 import std.algorithm : min; 218 219 auto kills = db.testCaseApi.testCaseKilledSrcMutants(kinds, tcId); 220 summary.kills = kills.length; 221 summary.score = kills.length == 0 ? 0 : long.max; 222 223 auto uniqueElem = root.addChild("div"); 224 225 { 226 auto p = root.addChild("p"); 227 p.addChild("b", "TestCases"); 228 p.appendText(": number of test cases that kill the mutant."); 229 } 230 { 231 auto p = root.addChild("p"); 232 p.addChild("b", "Suggestion"); 233 p.appendText(": alive mutants on the same source code location. Because they are close to a mutant that this test case killed it may be suitable to extend this test case to also kill the suggested mutant."); 234 } 235 236 auto tbl = tmplSortableTable(root, ["Link", "TestCases"] ~ (rdata.addSuggestion 237 ? ["Suggestion"] : null) ~ ["Priority", "ExitCode", "Tested"]); 238 239 foreach (const id; kills.sort) { 240 auto r = tbl.appendRow(); 241 242 const info = db.mutantApi.getMutantInfo(id).orElse(MutantInfo2.init); 243 244 r.addChild("td").addChild("a", format("%s:%s", info.file, 245 info.sloc.line)).href = format("%s#%s", buildPath("..", 246 HtmlStyle.fileDir, pathToHtmlLink(info.file)), info.id.get); 247 248 summary.score = min(info.tcKilled, summary.score); 249 { 250 auto td = r.addChild("td", info.tcKilled.to!string); 251 if (info.tcKilled == 1) { 252 td.style = "background-color: lightgreen"; 253 } 254 } 255 256 if (rdata.addSuggestion) { 257 auto tds = r.addChild("td"); 258 foreach (s; db.mutantApi.getSurroundingAliveMutants(id).enumerate) { 259 // column sort in the html report do not work correctly if starting from 0. 260 auto td = tds.addChild("a", format("%s", s.index + 1)); 261 td.href = format("%s#%s", buildPath("..", HtmlStyle.fileDir, 262 pathToHtmlLink(info.file)), db.mutantApi.getMutationId(s.value).get); 263 td.appendText(" "); 264 } 265 } 266 267 r.addChild("td", info.prio.get.to!string); 268 r.addChild("td", toString(info.exitStatus)); 269 r.addChild("td", info.updated.toShortDate); 270 } 271 } 272 273 void addSimilarity(ref Database db, TestCaseSimilarityAnalyse.Similarity[] similarities, 274 Element root) @system { 275 import dextool.cachetools; 276 277 auto getPath = nullableCache!(MutationStatusId, string, (MutationStatusId id) { 278 auto path = spinSql!(() => db.mutantApi.getPath(id)).get; 279 auto mutId = spinSql!(() => db.mutantApi.getMutationId(id)).get; 280 return format!"%s#%s"(buildPath("..", HtmlStyle.fileDir, pathToHtmlLink(path)), mutId.get); 281 })(0, 30.dur!"seconds"); 282 283 root.addChild("p", "How similary this test case is to others."); 284 { 285 auto p = root.addChild("p"); 286 p.addChild("b", "Note"); 287 p.appendText(": The analysis is based on the mutants that the test cases kill; thus, it is dependent on the mutation operators that are used when generating the report."); 288 289 root.addChild("p", "The intersection column is the mutants that are killed by both the current test case and in the column Test Case.") 290 .appendText( 291 " The difference column are the mutants that are only killed by the current test case."); 292 } 293 294 auto tbl = tmplDefaultTable(root, [ 295 "Test Case", "Similarity", "Difference", "Intersection" 296 ]); 297 foreach (const sim; similarities) { 298 auto r = tbl.appendRow(); 299 300 const name = db.testCaseApi.getTestCaseName(sim.testCase); 301 r.addChild("td").addChild("a", name).href = buildPath(name.pathToHtmlLink); 302 303 r.addChild("td", format("%#.3s", sim.similarity)); 304 305 auto difference = r.addChild("td"); 306 foreach (const mut; sim.difference) { 307 auto link = difference.addChild("a", mut.to!string); 308 link.href = getPath(mut).get; 309 difference.appendText(" "); 310 } 311 312 auto s = r.addChild("td"); 313 foreach (const mut; sim.intersection) { 314 auto link = s.addChild("a", mut.to!string); 315 link.href = getPath(mut).get; 316 s.appendText(" "); 317 } 318 } 319 }