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,
28     reportMarkedMutants, reportStatistics, 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 : ReportKind, ReportSection;
34 import dextool.plugin.mutate.backend.utility : Profile;
35 
36 @safe:
37 
38 void report(ref Database db, const ConfigReport conf, FilesysIO fio) {
39     import dextool.plugin.mutate.backend.utility : Profile;
40     import dextool.plugin.mutate.backend.mutation_type : toInternal;
41 
42     auto a = new ReportPlain(conf, fio);
43 
44     {
45         auto profile = Profile("iterate mutants for report");
46         void iter(const ref IterateMutantRow row) {
47             a.locationEvent(db, row);
48         }
49 
50         db.iterateMutants(&iter);
51     }
52 
53     auto profile = Profile("post process report");
54     a.locationStatEvent;
55     a.statEvent(db);
56 }
57 
58 /** Report mutations in a format easily readable by a human.
59  */
60 @safe final class ReportPlain {
61     import std.array : Appender;
62     import dextool.plugin.mutate.backend.utility;
63     import my.set;
64 
65     const ConfigReport conf;
66     Set!ReportSection sections;
67     FilesysIO fio;
68 
69     long[MakeMutationTextResult] mutationStat;
70 
71     this(const ConfigReport conf, FilesysIO fio) {
72         this.fio = fio;
73         this.conf = conf;
74         this.sections = conf.reportSection.toSet;
75     }
76 
77     void locationEvent(ref Database db, const ref IterateMutantRow r) @trusted {
78         void report() {
79             MakeMutationTextResult mut_txt;
80             AbsolutePath abs_path;
81             try {
82                 abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
83                 mut_txt = makeMutationText(fio.makeInput(abs_path),
84                         r.mutationPoint.offset, r.mutation.kind, r.lang);
85             } catch (Exception e) {
86                 logger.warning(e.msg);
87             }
88 
89             logger.infof("%s %s from '%s' to '%s' %s in %s:%s:%s", r.id.get,
90                     r.mutation.status, mut_txt.original, mut_txt.mutation,
91                     r.attrs, abs_path, r.sloc.line, r.sloc.column);
92         }
93 
94         void updateMutationStat() {
95             if (r.mutation.status != Mutation.Status.alive)
96                 return;
97 
98             try {
99                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
100                 auto mut_txt = makeMutationText(fio.makeInput(abs_path),
101                         r.mutationPoint.offset, r.mutation.kind, r.lang);
102 
103                 if (auto v = mut_txt in mutationStat)
104                     ++(*v);
105                 else
106                     mutationStat[mut_txt] = 1;
107             } catch (Exception e) {
108                 logger.warning(e.msg);
109             }
110         }
111 
112         void updateTestCaseStat(TestCase[] testCases) {
113             if (r.mutation.status != Mutation.Status.killed || testCases.empty)
114                 return;
115 
116             try {
117                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, r.file.Path));
118                 auto mut_txt = makeMutationText(fio.makeInput(abs_path),
119                         r.mutationPoint.offset, r.mutation.kind, r.lang);
120             } catch (Exception e) {
121                 logger.warning(e.msg);
122             }
123         }
124 
125         void reportTestCase(TestCase[] testCases) {
126             if (r.mutation.status != Mutation.Status.killed || testCases.empty)
127                 return;
128             logger.infof("%s killed by [%(%s, %)]", r.id.get, testCases);
129         }
130 
131         try {
132             if (ReportSection.alive in sections && r.mutation.status == Mutation.Status.alive)
133                 report;
134 
135             if (ReportSection.killed in sections && r.mutation.status == Mutation.Status.killed)
136                 report;
137 
138             if (ReportSection.all_mut in sections)
139                 report;
140 
141             if (ReportSection.mut_stat in sections)
142                 updateMutationStat;
143 
144             auto testCases = () {
145                 if (ReportSection.tc_killed in sections || ReportSection.tc_stat in sections) {
146                     return db.testCaseApi.getTestCases(r.id);
147                 }
148                 return null;
149             }();
150 
151             if (ReportSection.tc_killed in sections)
152                 reportTestCase(testCases);
153 
154             if (ReportSection.tc_stat in sections)
155                 updateTestCaseStat(testCases);
156         } catch (Exception e) {
157             logger.trace(e.msg).collectException;
158         }
159     }
160 
161     void locationStatEvent() {
162         if (ReportSection.mut_stat in sections && mutationStat.length != 0) {
163             logger.info("Alive Mutation Statistics");
164 
165             Table!4 substat_tbl;
166 
167             substat_tbl.heading = ["Percentage", "Count", "From", "To"];
168             reportMutationSubtypeStats(mutationStat, substat_tbl);
169 
170             writeln(substat_tbl);
171         }
172     }
173 
174     void statEvent(ref Database db) {
175         import dextool.plugin.mutate.backend.report.analyzers : reportTestCaseFullOverlap,
176             reportTestCaseStats, reportDeadTestCases, toTable, reportMutationScoreHistory;
177 
178         auto stdout_ = () @trusted { return stdout; }();
179 
180         if (ReportSection.tc_stat in sections) {
181             logger.info("Test Case Kill Statistics");
182             Table!3 tc_tbl;
183 
184             tc_tbl.heading = ["Percentage", "Count", "TestCase"];
185             auto r = reportTestCaseStats(db);
186             r.toTable(conf.tcKillSortNum, conf.tcKillSortOrder, tc_tbl);
187 
188             writeln(tc_tbl);
189         }
190 
191         if (ReportSection.tc_killed_no_mutants in sections) {
192             logger.info("Test Case(s) that has killed no mutants");
193             auto r = reportDeadTestCases(db);
194             if (r.ratio > 0)
195                 writefln("%s/%s = %s of all test cases", r.numDeadTC, r.total, r.ratio);
196 
197             Table!2 tbl;
198             tbl.heading = ["TestCase", "Location"];
199             r.toTable(tbl);
200             writeln(tbl);
201         }
202 
203         if (ReportSection.tc_full_overlap in sections
204                 || ReportSection.tc_full_overlap_with_mutation_id in sections) {
205             auto stat = reportTestCaseFullOverlap(db);
206 
207             if (ReportSection.tc_full_overlap in sections) {
208                 logger.info("Redundant Test Cases (killing the same mutants)");
209                 Table!2 tbl;
210                 stat.toTable!(No.colWithMutants)(tbl);
211                 tbl.heading = ["TestCase", "Count"];
212                 writeln(stat.sumToString);
213                 writeln(tbl);
214             }
215 
216             if (ReportSection.tc_full_overlap_with_mutation_id in sections) {
217                 logger.info("Redundant Test Cases (killing the same mutants)");
218                 Table!3 tbl;
219                 stat.toTable!(Yes.colWithMutants)(tbl);
220                 tbl.heading = ["TestCase", "Count", "Mutation ID"];
221                 writeln(stat.sumToString);
222                 writeln(tbl);
223             }
224         }
225 
226         if (ReportSection.marked_mutants in sections) {
227             logger.info("Marked mutants");
228             auto r = reportMarkedMutants(db);
229             writeln(r.tbl);
230         }
231 
232         if (ReportSection.trend in sections) {
233             logger.info("Mutation Score History");
234             auto r = reportMutationScoreHistory(db);
235             writeln(.toTable(r));
236         }
237 
238         if (ReportSection.summary in sections) {
239             logger.info("Summary");
240             auto summary = reportStatistics(db);
241             writeln(summary.toString);
242 
243             syncStatus(db);
244         }
245 
246         writeln;
247     }
248 }
249 
250 private:
251 
252 Table!2 toTable(MutationScoreHistory data) {
253     Table!2 tbl;
254     tbl.heading = ["Date", "Score"];
255     foreach (a; data.data) {
256         typeof(tbl).Row row = [a.timeStamp.to!string, a.score.get.to!string];
257         tbl.put(row);
258     }
259 
260     return tbl;
261 }
262 
263 void syncStatus(ref Database db) {
264     import std.algorithm : sort;
265     import std.typecons : tuple;
266     import dextool.plugin.mutate.backend.report.analyzers : reportSyncStatus;
267 
268     auto status = reportSyncStatus(db, 1);
269     if (status.mutants.empty)
270         return;
271 
272     if (status.mutants[0].updated > status.code && status.mutants[0].updated > status.test) {
273         return;
274     }
275 
276     logger.info("Sync Status");
277 
278     Table!2 tbl;
279     tbl.heading = ["Type", "Updated"];
280     foreach (r; [
281             tuple("Test", status.test), tuple("Code", status.code),
282             tuple("Coverage", status.coverage),
283             tuple("Oldest Mutant", status.mutants[0].updated)
284         ].sort!((a, b) => a[1] < b[1])) {
285         typeof(tbl).Row row = [r[0], r[1].to!string];
286         tbl.put(row);
287     }
288     writeln(tbl);
289 }