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 11 */ 12 module dextool.plugin.mutate.backend.report.markdown; 13 14 import logger = std.experimental.logger; 15 import std.array : empty; 16 import std.exception : collectException; 17 import std.typecons : Yes, No; 18 19 import dextool.plugin.mutate.backend.database : Database, IterateMutantRow; 20 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, makeMutationText; 21 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 22 import dextool.plugin.mutate.backend.report.analyzers : reportMutationSubtypeStats, 23 reportStatistics; 24 import dextool.plugin.mutate.backend.report.type : SimpleWriter, ReportEvent; 25 import dextool.plugin.mutate.backend.report.utility : window, windowSize, Table, toSections; 26 import dextool.plugin.mutate.backend.type : Mutation, Offset; 27 import dextool.plugin.mutate.config : ConfigReport; 28 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportLevel, ReportSection; 29 import dextool.set; 30 import dextool.type; 31 32 @safe: 33 34 struct Markdown(Writer, TraceWriter) { 35 import std.ascii : newline; 36 import std.format : formattedWrite, format; 37 import std.range : put; 38 39 private int curr_head; 40 private Writer w; 41 private TraceWriter w_trace; 42 43 private this(int heading, Writer w, TraceWriter w_trace) { 44 this.curr_head = heading; 45 this.w = w; 46 this.w_trace = w_trace; 47 } 48 49 this(Writer w, TraceWriter w_trace) { 50 this.w = w; 51 this.w_trace = w_trace; 52 } 53 54 auto heading(ARGS...)(auto ref ARGS args) { 55 import std.algorithm : copy; 56 import std.range : repeat, take; 57 58 () @trusted { 59 repeat('#').take(curr_head + 1).copy(w); 60 put(w, " "); 61 formattedWrite(w, args); 62 }(); 63 64 // two newlines because some markdown parsers do not correctly identify 65 // a heading if it isn't separated 66 put(w, newline); 67 put(w, newline); 68 return (typeof(this)(curr_head + 1, w, w_trace)); 69 } 70 71 auto popHeading() { 72 if (curr_head != 0) 73 put(w, newline); 74 return typeof(this)(curr_head - 1, w, w_trace); 75 } 76 77 auto beginSyntaxBlock(ARGS...)(auto ref ARGS args) { 78 put(w, "```"); 79 static if (ARGS.length != 0) 80 formattedWrite(w, args); 81 put(w, newline); 82 return this; 83 } 84 85 auto endSyntaxBlock() { 86 put(w, "```"); 87 put(w, newline); 88 return this; 89 } 90 91 void put(const(char)[] s) { 92 write(s); 93 } 94 95 auto write(ARGS...)(auto ref ARGS args) { 96 () @trusted { formattedWrite(w, "%s", args); }(); 97 return this; 98 } 99 100 auto writef(ARGS...)(auto ref ARGS args) { 101 () @trusted { formattedWrite(w, args); }(); 102 return this; 103 } 104 105 auto writeln(ARGS...)(auto ref ARGS args) { 106 this.write(args); 107 put(w, newline); 108 return this; 109 } 110 111 auto writefln(ARGS...)(auto ref ARGS args) { 112 this.writef(args); 113 put(w, newline); 114 return this; 115 } 116 117 auto trace(ARGS...)(auto ref ARGS args) { 118 this.writeln(w_trace, args); 119 return this; 120 } 121 122 auto tracef(ARGS...)(auto ref ARGS args) { 123 formattedWrite(w_trace, args); 124 put(w_trace, newline); 125 return this; 126 } 127 } 128 129 /** Report mutations in a format easily readable by a human. 130 */ 131 @safe final class ReportMarkdown : ReportEvent { 132 import std.conv : to; 133 import std.format : format, FormatSpec; 134 import std.stdio : write; 135 import dextool.plugin.mutate.backend.utility; 136 137 static immutable col_w = 10; 138 static immutable mutation_w = 10 + 8 + 8; 139 140 const Mutation.Kind[] kinds; 141 bool reportIndividualMutants; 142 Set!ReportSection sections; 143 FilesysIO fio; 144 145 Markdown!(SimpleWriter, SimpleWriter) markdown; 146 Markdown!(SimpleWriter, SimpleWriter) markdown_loc; 147 Markdown!(SimpleWriter, SimpleWriter) markdown_sum; 148 149 Table!5 mut_tbl; 150 alias Row = Table!(5).Row; 151 152 long[MakeMutationTextResult] mutationStat; 153 154 this(const Mutation.Kind[] kinds, const ConfigReport conf, FilesysIO fio) { 155 this.kinds = kinds; 156 this.fio = fio; 157 158 ReportSection[] tmp_sec = conf.reportSection.length == 0 159 ? conf.reportLevel.toSections : conf.reportSection.dup; 160 161 sections = tmp_sec.toSet; 162 reportIndividualMutants = sections.contains(ReportSection.all_mut) 163 || sections.contains(ReportSection.alive) || sections.contains(ReportSection.killed); 164 } 165 166 override void mutationKindEvent(const MutationKind[] kind_) { 167 SimpleWriter tracer; 168 if (ReportSection.all_mut) 169 tracer = (const(char[]) s) => write(s); 170 else 171 tracer = delegate(const(char)[] s) @safe {}; 172 173 markdown = Markdown!(SimpleWriter, SimpleWriter)((const(char[]) s) => write(s), tracer); 174 markdown = markdown.heading("Mutation Type %(%s, %)", kind_); 175 } 176 177 override void locationStartEvent(ref Database db) { 178 if (reportIndividualMutants) { 179 markdown_loc = markdown.heading("Mutants"); 180 mut_tbl.heading = ["From", "To", "File Line:Column", "ID", "Status"]; 181 } 182 } 183 184 override void locationEvent(ref Database db, const ref IterateMutantRow r) @trusted { 185 void report() { 186 MakeMutationTextResult mut_txt; 187 try { 188 auto abs_path = AbsolutePath(FileName(r.file), DirName(fio.getOutputDir)); 189 mut_txt = makeMutationText(fio.makeInput(abs_path), 190 r.mutationPoint.offset, r.mutation.kind, r.lang); 191 192 if (r.mutation.status == Mutation.Status.alive) { 193 if (auto v = mut_txt in mutationStat) 194 ++(*v); 195 else 196 mutationStat[mut_txt] = 1; 197 } 198 } catch (Exception e) { 199 logger.warning(e.msg); 200 } 201 202 // dfmt off 203 Row r_ = [ 204 format("`%s`", window(mut_txt.original, windowSize)), 205 format("`%s`", window(mut_txt.mutation, windowSize)), 206 format("%s %s:%s", r.file, r.sloc.line, r.sloc.column), 207 r.id.to!string, 208 r.mutation.status.to!string, 209 ]; 210 mut_tbl.put(r_); 211 // dfmt on 212 } 213 214 if (!reportIndividualMutants) 215 return; 216 217 try { 218 if (sections.contains(ReportSection.alive)) { 219 if (r.mutation.status == Mutation.Status.alive) { 220 report(); 221 } 222 } 223 224 if (sections.contains(ReportSection.killed)) { 225 if (r.mutation.status == Mutation.Status.killed) { 226 report(); 227 } 228 } 229 230 if (sections.contains(ReportSection.all_mut)) 231 report(); 232 } catch (Exception e) { 233 logger.trace(e.msg).collectException; 234 } 235 } 236 237 override void locationEndEvent() { 238 import std.format : FormatSpec; 239 240 if (!reportIndividualMutants) 241 return; 242 243 auto fmt = FormatSpec!char("%s"); 244 () @trusted { mut_tbl.toString((const(char[]) s) => write(s), fmt); }(); 245 246 markdown_loc.popHeading; 247 } 248 249 override void locationStatEvent() { 250 if (mutationStat.length != 0 && sections.contains(ReportSection.mut_stat)) { 251 auto item = markdown.heading("Alive Mutation Statistics"); 252 253 Table!4 substat_tbl; 254 Table!4.Row SRow; 255 256 substat_tbl.heading = ["Percentage", "Count", "From", "To"]; 257 reportMutationSubtypeStats(mutationStat, substat_tbl); 258 259 auto fmt = FormatSpec!char("%s"); 260 () @trusted { 261 substat_tbl.toString((const(char[]) s) => write(s), fmt); 262 }(); 263 item.popHeading; 264 } 265 } 266 267 override void statEvent(ref Database db) { 268 import dextool.plugin.mutate.backend.report.analyzers : reportDeadTestCases, 269 reportTestCaseFullOverlap, toTable; 270 271 const fmt = FormatSpec!char("%s"); 272 273 if (sections.contains(ReportSection.tc_killed_no_mutants)) { 274 auto item = markdown.heading("Test Cases with Zero Kills"); 275 auto r = reportDeadTestCases(db); 276 277 if (r.ratio > 0) 278 item.writefln("%s/%s = %s of all test cases", r.numDeadTC, r.total, r.ratio); 279 280 Table!2 tbl; 281 tbl.heading = ["TestCase", "Location"]; 282 r.toTable(tbl); 283 () @trusted { tbl.toString((const(char[]) s) => write(s), fmt); }(); 284 285 item.popHeading; 286 } 287 288 if (sections.contains(ReportSection.tc_full_overlap)) { 289 Table!2 tbl; 290 tbl.heading = ["TestCase", "Count"]; 291 auto stat = reportTestCaseFullOverlap(db, kinds); 292 stat.toTable!(No.colWithMutants)(tbl); 293 294 if (!tbl.empty) { 295 auto item = markdown.heading("Redundant Test Cases (killing the same mutants)"); 296 stat.sumToString(item); 297 item.writeln(stat); 298 () @trusted { tbl.toString((const(char[]) s) => write(s), fmt); }(); 299 item.popHeading; 300 } 301 } 302 303 if (sections.contains(ReportSection.summary)) { 304 markdown_sum = markdown.heading("Summary"); 305 306 markdown_sum.beginSyntaxBlock; 307 markdown_sum.writefln(reportStatistics(db, kinds).toString); 308 markdown_sum.endSyntaxBlock; 309 310 markdown_sum.popHeading; 311 } 312 } 313 }