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 }