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 }