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