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