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