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_plain
11 */
12 module dextool.plugin.mutate.backend.report.plain;
13 
14 import logger = std.experimental.logger;
15 import std.array : empty;
16 import std.conv : to;
17 import std.exception : collectException;
18 import std.path : buildPath;
19 import std.stdio : stdout, File, writeln, writefln;
20 import std.typecons : Yes, No;
21 
22 import dextool.type;
23 
24 import dextool.plugin.mutate.backend.database : Database, IterateMutantRow, MutationId;
25 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, makeMutationText;
26 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
27 import dextool.plugin.mutate.backend.report.analyzers : reportMutationSubtypeStats, reportMarkedMutants, reportStatistics,
28     MutationsMap, reportTestCaseKillMap, MutationReprMap, MutationRepr, MutationScoreHistory;
29 import dextool.plugin.mutate.backend.report.type : ReportEvent;
30 import dextool.plugin.mutate.backend.report.utility : window, windowSize, Table;
31 import dextool.plugin.mutate.backend.type : Mutation;
32 import dextool.plugin.mutate.config : ConfigReport;
33 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportSection;
34 import dextool.plugin.mutate.backend.utility : Profile;
35 
36 @safe:
37 
38 void report(ref Database db, const MutationKind[] userKinds, const ConfigReport conf, FilesysIO fio) {
39     import dextool.plugin.mutate.backend.utility : Profile;
40     import dextool.plugin.mutate.backend.mutation_type : toInternal;
41 
42     const kinds = toInternal(userKinds);
43 
44     auto a = new ReportPlain(kinds, conf, fio);
45 
46     a.mutationKindEvent(userKinds);
47 
48     {
49         auto profile = Profile("iterate mutants for report");
50         void iter(const ref IterateMutantRow row) {
51             a.locationEvent(db, row);
52         }
53 
54         db.iterateMutants(kinds, &iter);
55     }
56 
57     auto profile = Profile("post process report");
58     a.locationStatEvent;
59     a.statEvent(db);
60 }
61 
62 /** Report mutations in a format easily readable by a human.
63  */
64 @safe final class ReportPlain {
65     import std.array : Appender;
66     import dextool.plugin.mutate.backend.utility;
67     import my.set;
68 
69     const Mutation.Kind[] kinds;
70     const ConfigReport conf;
71     Set!ReportSection sections;
72     FilesysIO fio;
73 
74     long[MakeMutationTextResult] mutationStat;
75 
76     MutationsMap testCaseMutationKilled;
77     MutationReprMap mutationReprMap;
78     Appender!(MutationId[]) testCaseSuggestions;
79 
80     this(const Mutation.Kind[] kinds, const ConfigReport conf, FilesysIO fio) {
81         this.kinds = kinds;
82         this.fio = fio;
83         this.conf = conf;
84         this.sections = conf.reportSection.toSet;
85     }
86 
87     void mutationKindEvent(const MutationKind[] kind_) {
88         writefln("Mutation operators: %(%s, %)", kind_);
89     }
90 
91     void locationEvent(ref Database db, const ref IterateMutantRow r) @trusted {
92         void report() {
93             MakeMutationTextResult mut_txt;
94             AbsolutePath abs_path;
95             try {
96                 abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
97                 mut_txt = makeMutationText(fio.makeInput(abs_path),
98                         r.mutationPoint.offset, r.mutation.kind, r.lang);
99             } catch (Exception e) {
100                 logger.warning(e.msg);
101             }
102 
103             logger.infof("%s %s from '%s' to '%s' %s in %s:%s:%s", r.id.get,
104                     r.mutation.status, mut_txt.original, mut_txt.mutation,
105                     r.attrs, abs_path, r.sloc.line, r.sloc.column);
106         }
107 
108         void updateMutationStat() {
109             if (r.mutation.status != Mutation.Status.alive)
110                 return;
111 
112             try {
113                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
114                 auto mut_txt = makeMutationText(fio.makeInput(abs_path),
115                         r.mutationPoint.offset, r.mutation.kind, r.lang);
116 
117                 if (auto v = mut_txt in mutationStat)
118                     ++(*v);
119                 else
120                     mutationStat[mut_txt] = 1;
121             } catch (Exception e) {
122                 logger.warning(e.msg);
123             }
124         }
125 
126         void updateTestCaseStat(TestCase[] testCases) {
127             if (r.mutation.status != Mutation.Status.killed || testCases.empty)
128                 return;
129 
130             try {
131                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
132                 auto mut_txt = makeMutationText(fio.makeInput(abs_path),
133                         r.mutationPoint.offset, r.mutation.kind, r.lang);
134             } catch (Exception e) {
135                 logger.warning(e.msg);
136             }
137         }
138 
139         void updateTestCaseMap(TestCase[] testCases) {
140             if (r.mutation.status != Mutation.Status.killed || testCases.empty)
141                 return;
142 
143             try {
144                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
145                 auto mut_txt = makeMutationText(fio.makeInput(abs_path),
146                         r.mutationPoint.offset, r.mutation.kind, r.lang);
147                 mutationReprMap[r.id] = MutationRepr(r.sloc, r.file, mut_txt);
148 
149                 foreach (const a; testCases) {
150                     testCaseMutationKilled[a][r.id] = true;
151                 }
152             } catch (Exception e) {
153                 logger.warning(e.msg);
154             }
155         }
156 
157         void updateTestCaseSuggestion() {
158             if (r.mutation.status == Mutation.Status.alive)
159                 testCaseSuggestions.put(r.id);
160         }
161 
162         void reportTestCase(TestCase[] testCases) {
163             if (r.mutation.status != Mutation.Status.killed || testCases.empty)
164                 return;
165             logger.infof("%s killed by [%(%s, %)]", r.id.get, testCases);
166         }
167 
168         try {
169             if (ReportSection.alive in sections && r.mutation.status == Mutation.Status.alive)
170                 report;
171 
172             if (ReportSection.killed in sections && r.mutation.status == Mutation.Status.killed)
173                 report;
174 
175             if (ReportSection.all_mut in sections)
176                 report;
177 
178             if (ReportSection.mut_stat in sections)
179                 updateMutationStat;
180 
181             auto testCases = () {
182                 if (ReportSection.tc_killed in sections
183                         || ReportSection.tc_stat in sections || ReportSection.tc_map in sections) {
184                     return db.getTestCases(r.id);
185                 }
186                 return null;
187             }();
188 
189             if (ReportSection.tc_killed in sections)
190                 reportTestCase(testCases);
191 
192             if (ReportSection.tc_stat in sections)
193                 updateTestCaseStat(testCases);
194 
195             if (ReportSection.tc_map in sections)
196                 updateTestCaseMap(testCases);
197 
198             if (ReportSection.tc_suggestion in sections)
199                 updateTestCaseSuggestion();
200         } catch (Exception e) {
201             logger.trace(e.msg).collectException;
202         }
203     }
204 
205     void locationStatEvent() {
206         if (ReportSection.tc_map in sections && testCaseMutationKilled.length != 0) {
207             logger.info("Test Case Kill Map");
208 
209             static void txtWriter(string s) {
210                 writeln(s);
211             }
212 
213             static void writer(ref Table!4 tbl) {
214                 writeln(tbl);
215             }
216 
217             reportTestCaseKillMap(testCaseMutationKilled, mutationReprMap, &txtWriter, &writer);
218         }
219 
220         if (ReportSection.mut_stat in sections && mutationStat.length != 0) {
221             logger.info("Alive Mutation Statistics");
222 
223             Table!4 substat_tbl;
224 
225             substat_tbl.heading = ["Percentage", "Count", "From", "To"];
226             reportMutationSubtypeStats(mutationStat, substat_tbl);
227 
228             writeln(substat_tbl);
229         }
230     }
231 
232     void statEvent(ref Database db) {
233         import dextool.plugin.mutate.backend.report.analyzers : reportTestCaseFullOverlap, reportTestCaseStats,
234             reportMutationTestCaseSuggestion, reportDeadTestCases, toTable,
235             reportMutationScoreHistory;
236 
237         auto stdout_ = () @trusted { return stdout; }();
238 
239         if (ReportSection.tc_stat in sections) {
240             logger.info("Test Case Kill Statistics");
241             Table!3 tc_tbl;
242 
243             tc_tbl.heading = ["Percentage", "Count", "TestCase"];
244             auto r = reportTestCaseStats(db, kinds);
245             r.toTable(conf.tcKillSortNum, conf.tcKillSortOrder, tc_tbl);
246 
247             writeln(tc_tbl);
248         }
249 
250         if (ReportSection.tc_killed_no_mutants in sections) {
251             logger.info("Test Case(s) that has killed no mutants");
252             auto r = reportDeadTestCases(db);
253             if (r.ratio > 0)
254                 writefln("%s/%s = %s of all test cases", r.numDeadTC, r.total, r.ratio);
255 
256             Table!2 tbl;
257             tbl.heading = ["TestCase", "Location"];
258             r.toTable(tbl);
259             writeln(tbl);
260         }
261 
262         if (ReportSection.tc_suggestion in sections && testCaseSuggestions.data.length != 0) {
263             static void writer(ref Table!1 tbl) {
264                 writeln(tbl);
265             }
266 
267             reportMutationTestCaseSuggestion(db, testCaseSuggestions.data, &writer);
268         }
269 
270         if (ReportSection.tc_full_overlap in sections
271                 || ReportSection.tc_full_overlap_with_mutation_id in sections) {
272             auto stat = reportTestCaseFullOverlap(db, kinds);
273 
274             if (ReportSection.tc_full_overlap in sections) {
275                 logger.info("Redundant Test Cases (killing the same mutants)");
276                 Table!2 tbl;
277                 stat.toTable!(No.colWithMutants)(tbl);
278                 tbl.heading = ["TestCase", "Count"];
279                 writeln(stat.sumToString);
280                 writeln(tbl);
281             }
282 
283             if (ReportSection.tc_full_overlap_with_mutation_id in sections) {
284                 logger.info("Redundant Test Cases (killing the same mutants)");
285                 Table!3 tbl;
286                 stat.toTable!(Yes.colWithMutants)(tbl);
287                 tbl.heading = ["TestCase", "Count", "Mutation ID"];
288                 writeln(stat.sumToString);
289                 writeln(tbl);
290             }
291         }
292 
293         if (ReportSection.marked_mutants in sections) {
294             logger.info("Marked mutants");
295             auto r = reportMarkedMutants(db, kinds);
296             writeln(r.tbl);
297         }
298 
299         if (ReportSection.trend in sections) {
300             logger.info("Mutation Score History");
301             auto r = reportMutationScoreHistory(db);
302             writeln(.toTable(r));
303         }
304 
305         if (ReportSection.summary in sections) {
306             logger.info("Summary");
307             auto summary = reportStatistics(db, kinds);
308             writeln(summary.toString);
309 
310             syncStatus(db, kinds);
311         }
312 
313         writeln;
314     }
315 }
316 
317 private:
318 
319 Table!2 toTable(MutationScoreHistory data) {
320     Table!2 tbl;
321     tbl.heading = ["Date", "Score"];
322     foreach (a; data.data) {
323         typeof(tbl).Row row = [a.timeStamp.to!string, a.score.get.to!string];
324         tbl.put(row);
325     }
326 
327     return tbl;
328 }
329 
330 void syncStatus(ref Database db, const(Mutation.Kind)[] kinds) {
331     import std.algorithm : sort;
332     import std.typecons : tuple;
333     import dextool.plugin.mutate.backend.report.analyzers : reportSyncStatus;
334 
335     auto status = reportSyncStatus(db, kinds, 1);
336     if (status.mutants.empty)
337         return;
338 
339     if (status.mutants[0].updated > status.code && status.mutants[0].updated > status.test) {
340         return;
341     }
342 
343     logger.info("Sync Status");
344 
345     Table!2 tbl;
346     tbl.heading = ["Type", "Updated"];
347     foreach (r; [
348             tuple("Test", status.test), tuple("Code", status.code),
349             tuple("Coverage", status.coverage),
350             tuple("Oldest Mutant", status.mutants[0].updated)
351         ].sort!((a, b) => a[1] < b[1])) {
352         typeof(tbl).Row row = [r[0], r[1].to!string];
353         tbl.put(row);
354     }
355     writeln(tbl);
356 }