1 /** 2 Copyright: Copyright (c) 2018, 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 #SPC-report_for_human_plain 11 */ 12 module dextool.plugin.mutate.backend.report.plain; 13 14 import logger = std.experimental.logger; 15 import std.array : empty; 16 import std.conv : to; 17 import std.exception : collectException; 18 import std.path : buildPath; 19 import std.stdio : stdout, File, writeln, writefln; 20 import std.typecons : Yes, No; 21 22 import dextool.type; 23 24 import dextool.plugin.mutate.backend.database : Database, IterateMutantRow, MutationId; 25 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, makeMutationText; 26 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 27 import dextool.plugin.mutate.backend.report.analyzers : reportMutationSubtypeStats, reportMarkedMutants, reportStatistics, 28 MutationsMap, reportTestCaseKillMap, MutationReprMap, MutationRepr, MutationScoreHistory; 29 import dextool.plugin.mutate.backend.report.type : ReportEvent; 30 import dextool.plugin.mutate.backend.report.utility : window, windowSize, Table; 31 import dextool.plugin.mutate.backend.type : Mutation; 32 import dextool.plugin.mutate.config : ConfigReport; 33 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportSection; 34 import dextool.plugin.mutate.backend.utility : Profile; 35 36 @safe: 37 38 void report(ref Database db, const MutationKind[] userKinds, const ConfigReport conf, FilesysIO fio) { 39 import dextool.plugin.mutate.backend.utility : Profile; 40 import dextool.plugin.mutate.backend.mutation_type : toInternal; 41 42 const kinds = toInternal(userKinds); 43 44 auto a = new ReportPlain(kinds, conf, fio); 45 46 a.mutationKindEvent(userKinds); 47 48 { 49 auto profile = Profile("iterate mutants for report"); 50 void iter(const ref IterateMutantRow row) { 51 a.locationEvent(db, row); 52 } 53 54 db.iterateMutants(kinds, &iter); 55 } 56 57 auto profile = Profile("post process report"); 58 a.locationStatEvent; 59 a.statEvent(db); 60 } 61 62 /** Report mutations in a format easily readable by a human. 63 */ 64 @safe final class ReportPlain { 65 import std.array : Appender; 66 import dextool.plugin.mutate.backend.utility; 67 import my.set; 68 69 const Mutation.Kind[] kinds; 70 const ConfigReport conf; 71 Set!ReportSection sections; 72 FilesysIO fio; 73 74 long[MakeMutationTextResult] mutationStat; 75 76 MutationsMap testCaseMutationKilled; 77 MutationReprMap mutationReprMap; 78 Appender!(MutationId[]) testCaseSuggestions; 79 80 this(const Mutation.Kind[] kinds, const ConfigReport conf, FilesysIO fio) { 81 this.kinds = kinds; 82 this.fio = fio; 83 this.conf = conf; 84 this.sections = conf.reportSection.toSet; 85 } 86 87 void mutationKindEvent(const MutationKind[] kind_) { 88 writefln("Mutation operators: %(%s, %)", kind_); 89 } 90 91 void locationEvent(ref Database db, const ref IterateMutantRow r) @trusted { 92 void report() { 93 MakeMutationTextResult mut_txt; 94 AbsolutePath abs_path; 95 try { 96 abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path)); 97 mut_txt = makeMutationText(fio.makeInput(abs_path), 98 r.mutationPoint.offset, r.mutation.kind, r.lang); 99 } catch (Exception e) { 100 logger.warning(e.msg); 101 } 102 103 logger.infof("%s %s from '%s' to '%s' %s in %s:%s:%s", r.id.get, 104 r.mutation.status, mut_txt.original, mut_txt.mutation, 105 r.attrs, abs_path, r.sloc.line, r.sloc.column); 106 } 107 108 void updateMutationStat() { 109 if (r.mutation.status != Mutation.Status.alive) 110 return; 111 112 try { 113 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path)); 114 auto mut_txt = makeMutationText(fio.makeInput(abs_path), 115 r.mutationPoint.offset, r.mutation.kind, r.lang); 116 117 if (auto v = mut_txt in mutationStat) 118 ++(*v); 119 else 120 mutationStat[mut_txt] = 1; 121 } catch (Exception e) { 122 logger.warning(e.msg); 123 } 124 } 125 126 void updateTestCaseStat(TestCase[] testCases) { 127 if (r.mutation.status != Mutation.Status.killed || testCases.empty) 128 return; 129 130 try { 131 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path)); 132 auto mut_txt = makeMutationText(fio.makeInput(abs_path), 133 r.mutationPoint.offset, r.mutation.kind, r.lang); 134 } catch (Exception e) { 135 logger.warning(e.msg); 136 } 137 } 138 139 void updateTestCaseMap(TestCase[] testCases) { 140 if (r.mutation.status != Mutation.Status.killed || testCases.empty) 141 return; 142 143 try { 144 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path)); 145 auto mut_txt = makeMutationText(fio.makeInput(abs_path), 146 r.mutationPoint.offset, r.mutation.kind, r.lang); 147 mutationReprMap[r.id] = MutationRepr(r.sloc, r.file, mut_txt); 148 149 foreach (const a; testCases) { 150 testCaseMutationKilled[a][r.id] = true; 151 } 152 } catch (Exception e) { 153 logger.warning(e.msg); 154 } 155 } 156 157 void updateTestCaseSuggestion() { 158 if (r.mutation.status == Mutation.Status.alive) 159 testCaseSuggestions.put(r.id); 160 } 161 162 void reportTestCase(TestCase[] testCases) { 163 if (r.mutation.status != Mutation.Status.killed || testCases.empty) 164 return; 165 logger.infof("%s killed by [%(%s, %)]", r.id.get, testCases); 166 } 167 168 try { 169 if (ReportSection.alive in sections && r.mutation.status == Mutation.Status.alive) 170 report; 171 172 if (ReportSection.killed in sections && r.mutation.status == Mutation.Status.killed) 173 report; 174 175 if (ReportSection.all_mut in sections) 176 report; 177 178 if (ReportSection.mut_stat in sections) 179 updateMutationStat; 180 181 auto testCases = () { 182 if (ReportSection.tc_killed in sections 183 || ReportSection.tc_stat in sections || ReportSection.tc_map in sections) { 184 return db.getTestCases(r.id); 185 } 186 return null; 187 }(); 188 189 if (ReportSection.tc_killed in sections) 190 reportTestCase(testCases); 191 192 if (ReportSection.tc_stat in sections) 193 updateTestCaseStat(testCases); 194 195 if (ReportSection.tc_map in sections) 196 updateTestCaseMap(testCases); 197 198 if (ReportSection.tc_suggestion in sections) 199 updateTestCaseSuggestion(); 200 } catch (Exception e) { 201 logger.trace(e.msg).collectException; 202 } 203 } 204 205 void locationStatEvent() { 206 if (ReportSection.tc_map in sections && testCaseMutationKilled.length != 0) { 207 logger.info("Test Case Kill Map"); 208 209 static void txtWriter(string s) { 210 writeln(s); 211 } 212 213 static void writer(ref Table!4 tbl) { 214 writeln(tbl); 215 } 216 217 reportTestCaseKillMap(testCaseMutationKilled, mutationReprMap, &txtWriter, &writer); 218 } 219 220 if (ReportSection.mut_stat in sections && mutationStat.length != 0) { 221 logger.info("Alive Mutation Statistics"); 222 223 Table!4 substat_tbl; 224 225 substat_tbl.heading = ["Percentage", "Count", "From", "To"]; 226 reportMutationSubtypeStats(mutationStat, substat_tbl); 227 228 writeln(substat_tbl); 229 } 230 } 231 232 void statEvent(ref Database db) { 233 import dextool.plugin.mutate.backend.report.analyzers : reportTestCaseFullOverlap, reportTestCaseStats, 234 reportMutationTestCaseSuggestion, reportDeadTestCases, toTable, 235 reportMutationScoreHistory; 236 237 auto stdout_ = () @trusted { return stdout; }(); 238 239 if (ReportSection.tc_stat in sections) { 240 logger.info("Test Case Kill Statistics"); 241 Table!3 tc_tbl; 242 243 tc_tbl.heading = ["Percentage", "Count", "TestCase"]; 244 auto r = reportTestCaseStats(db, kinds); 245 r.toTable(conf.tcKillSortNum, conf.tcKillSortOrder, tc_tbl); 246 247 writeln(tc_tbl); 248 } 249 250 if (ReportSection.tc_killed_no_mutants in sections) { 251 logger.info("Test Case(s) that has killed no mutants"); 252 auto r = reportDeadTestCases(db); 253 if (r.ratio > 0) 254 writefln("%s/%s = %s of all test cases", r.numDeadTC, r.total, r.ratio); 255 256 Table!2 tbl; 257 tbl.heading = ["TestCase", "Location"]; 258 r.toTable(tbl); 259 writeln(tbl); 260 } 261 262 if (ReportSection.tc_suggestion in sections && testCaseSuggestions.data.length != 0) { 263 static void writer(ref Table!1 tbl) { 264 writeln(tbl); 265 } 266 267 reportMutationTestCaseSuggestion(db, testCaseSuggestions.data, &writer); 268 } 269 270 if (ReportSection.tc_full_overlap in sections 271 || ReportSection.tc_full_overlap_with_mutation_id in sections) { 272 auto stat = reportTestCaseFullOverlap(db, kinds); 273 274 if (ReportSection.tc_full_overlap in sections) { 275 logger.info("Redundant Test Cases (killing the same mutants)"); 276 Table!2 tbl; 277 stat.toTable!(No.colWithMutants)(tbl); 278 tbl.heading = ["TestCase", "Count"]; 279 writeln(stat.sumToString); 280 writeln(tbl); 281 } 282 283 if (ReportSection.tc_full_overlap_with_mutation_id in sections) { 284 logger.info("Redundant Test Cases (killing the same mutants)"); 285 Table!3 tbl; 286 stat.toTable!(Yes.colWithMutants)(tbl); 287 tbl.heading = ["TestCase", "Count", "Mutation ID"]; 288 writeln(stat.sumToString); 289 writeln(tbl); 290 } 291 } 292 293 if (ReportSection.marked_mutants in sections) { 294 logger.info("Marked mutants"); 295 auto r = reportMarkedMutants(db, kinds); 296 writeln(r.tbl); 297 } 298 299 if (ReportSection.trend in sections) { 300 logger.info("Mutation Score History"); 301 auto r = reportMutationScoreHistory(db); 302 writeln(.toTable(r)); 303 } 304 305 if (ReportSection.summary in sections) { 306 logger.info("Summary"); 307 auto summary = reportStatistics(db, kinds); 308 writeln(summary.toString); 309 310 syncStatus(db, kinds); 311 } 312 313 writeln; 314 } 315 } 316 317 private: 318 319 Table!2 toTable(MutationScoreHistory data) { 320 Table!2 tbl; 321 tbl.heading = ["Date", "Score"]; 322 foreach (a; data.data) { 323 typeof(tbl).Row row = [a.timeStamp.to!string, a.score.get.to!string]; 324 tbl.put(row); 325 } 326 327 return tbl; 328 } 329 330 void syncStatus(ref Database db, const(Mutation.Kind)[] kinds) { 331 import std.algorithm : sort; 332 import std.typecons : tuple; 333 import dextool.plugin.mutate.backend.report.analyzers : reportSyncStatus; 334 335 auto status = reportSyncStatus(db, kinds, 1); 336 if (status.mutants.empty) 337 return; 338 339 if (status.mutants[0].updated > status.code && status.mutants[0].updated > status.test) { 340 return; 341 } 342 343 logger.info("Sync Status"); 344 345 Table!2 tbl; 346 tbl.heading = ["Type", "Updated"]; 347 foreach (r; [ 348 tuple("Test", status.test), tuple("Code", status.code), 349 tuple("Coverage", status.coverage), 350 tuple("Oldest Mutant", status.mutants[0].updated) 351 ].sort!((a, b) => a[1] < b[1])) { 352 typeof(tbl).Row row = [r[0], r[1].to!string]; 353 tbl.put(row); 354 } 355 writeln(tbl); 356 }