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 }