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 }