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