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 module dextool.plugin.mutate.backend.report.utility;
11 
12 import std.exception : collectException;
13 import logger = std.experimental.logger;
14 
15 import dextool.type;
16 
17 import dextool.plugin.mutate.backend.type : Mutation, Offset, TestCase;
18 import dextool.plugin.mutate.backend.database : Database;
19 import dextool.plugin.mutate.backend.interface_ : FilesysIO, SafeInput;
20 
21 @safe:
22 
23 // 5 because it covers all the operators and true/false
24 immutable windowSize = 5;
25 
26 immutable originalIsCorrupt = "deXtool: unable to open the file or it has changed since mutation where performed";
27 
28 immutable invalidFile = "Dextool: Invalid UTF-8 content";
29 
30 /// Create a range from `a` that has at most maxlen+3 letters in it.
31 auto window(T)(T a, size_t maxlen) {
32     import std.algorithm : filter, among, joiner;
33     import std.range : take, only, chain;
34 
35     // dfmt off
36     return chain(a.take(maxlen).filter!(a => !a.among('\n')),
37                  only(a.length > maxlen ? "..." : null).joiner);
38     // dfmt on
39 }
40 
41 struct MakeMutationTextResult {
42     string original = originalIsCorrupt;
43     string mutation;
44 
45     nothrow @safe size_t toHash() {
46         import std.digest.murmurhash;
47 
48         MurmurHash3!32 hash;
49         hash.put(cast(const(ubyte)[]) original);
50         hash.put(cast(const(ubyte)[]) mutation);
51         auto h = hash.finish;
52         return ((h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3]);
53     }
54 
55     bool opEquals(const this o) const nothrow @safe {
56         return original == o.original && mutation == o.mutation;
57     }
58 }
59 
60 auto makeMutationText(SafeInput file_, const Offset offs, Mutation.Kind kind) nothrow {
61     import dextool.plugin.mutate.backend.generate_mutant : makeMutation;
62 
63     MakeMutationTextResult rval;
64 
65     try {
66         if (offs.end < file_.read.length) {
67             rval.original = file_.read[offs.begin .. offs.end].toInternal;
68         }
69 
70         auto mut = makeMutation(kind);
71         rval.mutation = mut.mutate(rval.original);
72     }
73     catch (Exception e) {
74         logger.warning(e.msg).collectException;
75     }
76 
77     return rval;
78 }
79 
80 string toInternal(ubyte[] data) @safe nothrow {
81     import std.utf : validate;
82 
83     try {
84         auto result = () @trusted{ return cast(string) data; }();
85         validate(result);
86         return result;
87     }
88     catch (Exception e) {
89     }
90 
91     return invalidFile;
92 }
93 
94 void reportMutationSubtypeStats(ref const long[MakeMutationTextResult] mut_stat, ref Table!4 tbl) @safe nothrow {
95     import std.conv : to;
96     import std.format : format;
97     import std.algorithm : sum, map, sort, filter;
98 
99     long total = mut_stat.byValue.sum;
100 
101     import std.array : array;
102     import std.range : take;
103     import std.typecons : Tuple;
104 
105     // trusted because it is marked as @safe in dmd-2.078.1
106     // TODO remove this trusted when upgrading the minimal compiler
107     // can be simplified to:
108     // foreach (v, alive.byKeyValue.array.sort!((a, b) => a.value > b.value))....
109     auto kv = () @trusted{
110         return mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value)
111             .take(20).map!(a => Tuple!(MakeMutationTextResult, "key", long,
112                     "value")(a.key, a.value)).array;
113     }();
114 
115     foreach (v; kv) {
116         try {
117             auto percentage = (cast(double) v.value / cast(double) total) * 100.0;
118 
119             // dfmt off
120             typeof(tbl).Row r = [
121                 percentage.to!string,
122                 v.value.to!string,
123                 format("`%s`", window(v.key.original, windowSize)),
124                 format("`%s`", window(v.key.mutation, windowSize)),
125             ];
126             // dfmt on
127             tbl.put(r);
128         }
129         catch (Exception e) {
130             logger.warning(e.msg).collectException;
131         }
132     }
133 }
134 
135 void reportTestCaseStats(ref const long[TestCase] mut_stat, ref Table!3 tbl, long take_) @safe nothrow {
136     import std.algorithm : sum, sort;
137     import std.array : array;
138     import std.conv : to;
139     import std.range : take;
140 
141     long total = mut_stat.byValue.sum;
142 
143     foreach (v; mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value).take(take_)) {
144         try {
145             auto percentage = (cast(double) v.value / cast(double) total) * 100.0;
146             typeof(tbl).Row r = [percentage.to!string, v.value.to!string, v.key];
147             tbl.put(r);
148         }
149         catch (Exception e) {
150             logger.warning(e.msg).collectException;
151         }
152     }
153 }
154 
155 import dextool.plugin.mutate.backend.database : MutationId;
156 
157 /// Information needed to present the mutant to an user.
158 struct MutationRepr {
159     import dextool.type : Path;
160     import dextool.plugin.mutate.backend.type : SourceLoc;
161 
162     SourceLoc sloc;
163     Path file;
164     MakeMutationTextResult mutation;
165 }
166 
167 alias Mutations = bool[MutationId];
168 alias MutationsMap = Mutations[TestCase];
169 alias MutationReprMap = MutationRepr[MutationId];
170 
171 void reportTestCaseKillMap(WriterTextT, WriterT)(ref const MutationsMap mut_stat,
172         ref const MutationReprMap mutrepr, WriterTextT writer_txt, WriterT writer) @safe {
173     import std.conv : to;
174     import std.range : put;
175     import std.format : format;
176 
177     alias MutTable = Table!4;
178     alias Row = MutTable.Row;
179 
180     foreach (tc_muts; mut_stat.byKeyValue) {
181         put(writer_txt, tc_muts.key);
182 
183         MutTable tbl;
184         tbl.heading = ["ID", "File Line:Column", "From", "To"];
185 
186         foreach (mut; tc_muts.value.byKey) {
187             Row row;
188 
189             if (auto v = mut in mutrepr) {
190                 row[1] = format("%s %s:%s", v.file, v.sloc.line, v.sloc.column);
191                 row[2] = format("`%s`", window(v.mutation.original, windowSize));
192                 row[3] = format("`%s`", window(v.mutation.mutation, windowSize));
193             }
194 
195             row[0] = mut.to!string;
196             tbl.put(row);
197         }
198 
199         put(writer, tbl);
200     }
201 }
202 
203 void reportMutationTestCaseSuggestion(WriterT)(ref Database db,
204         const MutationId[] tc_sugg, WriterT writer) @safe {
205     import std.conv : to;
206     import std.range : put;
207     import std.format : format;
208 
209     alias MutTable = Table!1;
210     alias Row = MutTable.Row;
211 
212     foreach (mut_id; tc_sugg) {
213         MutTable tbl;
214         tbl.heading = [mut_id.to!string];
215 
216         try {
217             auto suggestions = db.getSurroundingTestCases(mut_id);
218             if (suggestions.length == 0)
219                 continue;
220 
221             foreach (tc; suggestions) {
222                 Row row;
223                 row[0] = format("`%s`", tc);
224                 tbl.put(row);
225             }
226             put(writer, tbl);
227         }
228         catch (Exception e) {
229             logger.warning(e.msg);
230         }
231     }
232 }
233 
234 void reportStatistics(ReportT)(ref Database db, const Mutation.Kind[] kinds, ref ReportT item) @safe nothrow {
235     import core.time : dur;
236     import std.algorithm : map, filter, sum;
237     import std.range : only;
238     import std.datetime : Clock;
239     import dextool.plugin.mutate.backend.utility;
240 
241     auto alive = db.aliveMutants(kinds);
242     auto killed = db.killedMutants(kinds);
243     auto timeout = db.timeoutMutants(kinds);
244     auto untested = db.unknownMutants(kinds);
245     auto killed_by_compiler = db.killedByCompilerMutants(kinds);
246 
247     try {
248         immutable align_ = 8;
249 
250         const auto total_time = only(alive, killed, timeout).filter!(a => !a.isNull)
251             .map!(a => a.time.total!"msecs").sum.dur!"msecs";
252         const auto total_cnt = only(alive, killed, timeout).filter!(a => !a.isNull)
253             .map!(a => a.count).sum;
254         const auto killed_cnt = only(killed, timeout).filter!(a => !a.isNull)
255             .map!(a => a.count).sum;
256         const auto untested_cnt = untested.isNull ? 0 : untested.count;
257         const auto predicted = total_cnt > 0 ? (untested_cnt * (total_time / total_cnt))
258             : 0.dur!"msecs";
259 
260         // execution time
261         if (untested_cnt > 0 && predicted > 0.dur!"msecs")
262             item.writefln("Predicted time until mutation testing is done: %s (%s)",
263                     predicted, Clock.currTime + predicted);
264         item.writefln("%-*s %s", align_ * 4, "Mutation execution time:", total_time);
265         if (!killed_by_compiler.isNull)
266             item.tracef("%-*s %s", align_ * 4, "Mutants killed by compiler:",
267                     killed_by_compiler.time);
268 
269         item.writeln("");
270 
271         // mutation score and details
272         if (!untested.isNull && untested.count > 0)
273             item.writefln("Untested: %s", untested.count);
274         if (!alive.isNull)
275             item.writefln("%-*s %s", align_, "Alive:", alive.count);
276         if (!killed.isNull)
277             item.writefln("%-*s %s", align_, "Killed:", killed.count);
278         if (!timeout.isNull)
279             item.writefln("%-*s %s", align_, "Timeout:", timeout.count);
280         item.writefln("%-*s %s", align_, "Total:", total_cnt);
281         if (total_cnt > 0)
282             item.writefln("%-*s %s", align_, "Score:",
283                     cast(double) killed_cnt / cast(double) total_cnt);
284         if (!killed_by_compiler.isNull)
285             item.tracef("%-*s %s", align_, "Killed by compiler:", killed_by_compiler.count);
286     }
287     catch (Exception e) {
288         logger.warning(e.msg).collectException;
289     }
290 }
291 
292 struct Table(int columnsNr) {
293     alias Row = string[columnsNr];
294 
295     Row heading_;
296     Row[] rows;
297     ulong[columnsNr] columnWidth;
298 
299     this(const Row heading) {
300         this.heading = heading;
301         updateColumns(heading);
302     }
303 
304     void heading(const Row r) {
305         heading_ = r;
306         updateColumns(r);
307     }
308 
309     void put(const Row r) {
310         rows ~= r;
311         updateColumns(r);
312     }
313 
314     import std.format : FormatSpec;
315 
316     void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const {
317         import std.ascii : newline;
318         import std.range : enumerate, repeat;
319         import std.format : formattedWrite;
320         import std.range.primitives : put;
321 
322         immutable sep = "|";
323         immutable lhs_sep = "| ";
324         immutable mid_sep = " | ";
325         immutable rhs_sep = " |";
326 
327         void printRow(const ref Row r) {
328             foreach (const r_; r[].enumerate) {
329                 if (r_.index == 0)
330                     put(w, lhs_sep);
331                 else
332                     put(w, mid_sep);
333                 formattedWrite(w, "%-*s", columnWidth[r_.index], r_.value);
334             }
335             put(w, rhs_sep);
336             put(w, newline);
337         }
338 
339         printRow(heading_);
340 
341         immutable dash = "-";
342         foreach (len; columnWidth) {
343             put(w, sep);
344             put(w, repeat(dash, len + 2));
345         }
346         put(w, sep);
347         put(w, newline);
348 
349         foreach (const ref r; rows) {
350             printRow(r);
351         }
352     }
353 
354     private void updateColumns(const ref Row r) {
355         import std.algorithm : filter, count, map;
356         import std.range : enumerate;
357         import std.utf : byCodeUnit;
358         import std.typecons : tuple;
359 
360         foreach (a; r[].enumerate.map!(a => tuple(a.index,
361                 a.value.byCodeUnit.count)).filter!(a => a[1] > columnWidth[a[0]])) {
362             columnWidth[a[0]] = a[1];
363         }
364     }
365 }