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