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.json;
11 
12 import logger = std.experimental.logger;
13 import std.array : empty, appender;
14 import std.exception : collectException;
15 import std.json : JSONValue;
16 import std.path : buildPath;
17 
18 import my.from_;
19 
20 import dextool.type;
21 
22 import dextool.plugin.mutate.backend.database : Database, FileRow, FileMutantRow, MutationId;
23 import dextool.plugin.mutate.backend.diff_parser : Diff;
24 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, makeMutationText;
25 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
26 import dextool.plugin.mutate.backend.report.type : FileReport, FilesReporter;
27 import dextool.plugin.mutate.backend.report.utility : window, windowSize;
28 import dextool.plugin.mutate.backend.type : Mutation;
29 import dextool.plugin.mutate.config : ConfigReport;
30 import dextool.plugin.mutate.type : MutationKind, ReportSection;
31 
32 @safe:
33 
34 void report(ref Database db, const MutationKind[] userKinds, const ConfigReport conf,
35         FilesysIO fio, ref Diff diff) {
36     import dextool.plugin.mutate.backend.database : FileMutantRow;
37     import dextool.plugin.mutate.backend.mutation_type : toInternal;
38     import dextool.plugin.mutate.backend.utility : Profile;
39 
40     const kinds = toInternal(userKinds);
41 
42     auto fps = new ReportJson(kinds, conf, fio, diff);
43 
44     fps.mutationKindEvent(userKinds);
45 
46     foreach (f; db.getDetailedFiles) {
47         auto profile = Profile("generate report for " ~ f.file);
48 
49         fps.getFileReportEvent(db, f);
50 
51         void fn(const ref FileMutantRow row) {
52             fps.fileMutantEvent(row);
53         }
54 
55         db.iterateFileMutants(kinds, f.file, &fn);
56 
57         fps.endFileEvent;
58     }
59 
60     auto profile = Profile("post process report");
61     fps.postProcessEvent(db);
62 }
63 
64 /**
65  * Expects locations to be grouped by file.
66  *
67  * TODO this is ugly. Use a JSON serializer instead.
68  */
69 final class ReportJson {
70     import std.array : array;
71     import std.algorithm : map, joiner, among;
72     import std.conv : to;
73     import std.format : format;
74     import std.json;
75     import my.set;
76 
77     const Mutation.Kind[] kinds;
78     const AbsolutePath logDir;
79     Set!ReportSection sections;
80     FilesysIO fio;
81 
82     // Report alive mutants in this section
83     Diff diff;
84 
85     JSONValue report;
86     JSONValue[] current_file_mutants;
87     FileRow current_file;
88 
89     this(const Mutation.Kind[] kinds, const ConfigReport conf, FilesysIO fio, ref Diff diff) {
90         this.kinds = kinds;
91         this.fio = fio;
92         this.logDir = conf.logDir;
93         this.diff = diff;
94 
95         sections = conf.reportSection.toSet;
96     }
97 
98     void mutationKindEvent(const MutationKind[] kinds) {
99         report = ["types": kinds.map!(a => a.to!string).array, "files": []];
100     }
101 
102     void getFileReportEvent(ref Database db, const ref FileRow fr) @trusted {
103         current_file = fr;
104     }
105 
106     void fileMutantEvent(const ref FileMutantRow r) @trusted {
107         auto appendMutant() {
108             JSONValue m = ["mutation_id" : r.id.to!long];
109             m.object["kind"] = r.mutation.kind.to!string;
110             m.object["status"] = r.mutation.status.to!string;
111             m.object["line"] = r.sloc.line;
112             m.object["column"] = r.sloc.column;
113             m.object["begin"] = r.mutationPoint.offset.begin;
114             m.object["end"] = r.mutationPoint.offset.end;
115 
116             try {
117                 MakeMutationTextResult mut_txt;
118                 auto abs_path = AbsolutePath(buildPath(fio.getOutputDir, current_file.file.Path));
119                 mut_txt = makeMutationText(fio.makeInput(abs_path),
120                         r.mutationPoint.offset, r.mutation.kind, r.lang);
121                 m.object["value"] = mut_txt.mutation;
122             } catch (Exception e) {
123                 logger.warning(e.msg);
124             }
125 
126             current_file_mutants ~= m;
127         }
128 
129         if (sections.contains(ReportSection.all_mut) || sections.contains(ReportSection.alive)
130                 && r.mutation.status.among(Mutation.Status.alive, Mutation.Status.noCoverage)
131                 || sections.contains(ReportSection.killed)
132                 && r.mutation.status == Mutation.Status.killed) {
133             appendMutant;
134         }
135     }
136 
137     void endFileEvent() @trusted {
138         if (current_file_mutants.empty) {
139             return;
140         }
141 
142         JSONValue s;
143         s = [
144             "filename": current_file.file,
145             "checksum": format("%x", current_file.fileChecksum),
146         ];
147         s["mutants"] = JSONValue(current_file_mutants), report["files"].array ~= s;
148 
149         current_file_mutants = null;
150     }
151 
152     void postProcessEvent(ref Database db) @trusted {
153         import std.datetime : Clock;
154         import std.path : buildPath;
155         import std.stdio : File;
156         import dextool.plugin.mutate.backend.report.analyzers : reportStatistics,
157             reportDiff, DiffReport, reportMutationScoreHistory,
158             reportDeadTestCases, reportTestCaseStats, reportTestCaseUniqueness,
159             reportTrendByCodeChange;
160 
161         if (ReportSection.summary in sections) {
162             const stat = reportStatistics(db, kinds);
163             JSONValue s = ["alive" : stat.alive];
164             s.object["no_coverage"] = stat.noCoverage;
165             s.object["alive_nomut"] = stat.aliveNoMut;
166             s.object["killed"] = stat.killed;
167             s.object["timeout"] = stat.timeout;
168             s.object["untested"] = stat.untested;
169             s.object["killed_by_compiler"] = stat.killedByCompiler;
170             s.object["total"] = stat.total;
171             s.object["score"] = stat.score;
172             s.object["nomut_score"] = stat.suppressedOfTotal;
173             s.object["total_compile_time_s"] = stat.totalTime.compile.total!"seconds";
174             s.object["total_test_time_s"] = stat.totalTime.test.total!"seconds";
175             s.object["killed_by_compiler_time_s"] = stat.killedByCompilerTime.sum.total!"seconds";
176             s.object["predicted_done"] = (Clock.currTime + stat.predictedDone).toISOExtString;
177             s.object["worklist"] = stat.worklist;
178 
179             report["stat"] = s;
180         }
181 
182         if (ReportSection.diff in sections) {
183             auto r = reportDiff(db, kinds, diff, fio.getOutputDir);
184             JSONValue s = ["score" : r.score];
185             report["diff"] = s;
186         }
187 
188         if (ReportSection.trend in sections) {
189             const history = reportMutationScoreHistory(db);
190             const byCodeChange = reportTrendByCodeChange(db, kinds);
191             JSONValue d;
192             d["code_change_score"] = byCodeChange.value.get;
193             d["code_change_score_error"] = byCodeChange.error.get;
194 
195             d["history_score"] = history.estimate.predScore;
196             d["score_history"] = toJson(history);
197             report["trend"] = d;
198         }
199 
200         if (ReportSection.tc_killed_no_mutants in sections) {
201             auto r = reportDeadTestCases(db);
202             JSONValue s;
203             s["ratio"] = r.ratio;
204             s["number"] = r.testCases.length;
205             s["test_cases"] = r.testCases.map!(a => a.name).array;
206             report["killed_no_mutants"] = s;
207         }
208 
209         if (ReportSection.tc_stat in sections) {
210             auto r = reportTestCaseStats(db, kinds);
211             JSONValue s;
212             foreach (a; r.testCases.byValue) {
213                 JSONValue v = ["ratio" : a.ratio];
214                 v["killed"] = a.info.killedMutants;
215                 s[a.tc.name] = v;
216             }
217 
218             if (!r.testCases.empty) {
219                 report["test_case_stat"] = s;
220             }
221         }
222 
223         if (ReportSection.tc_unique in sections) {
224             auto r = reportTestCaseUniqueness(db, kinds);
225             if (!r.uniqueKills.empty) {
226                 JSONValue s;
227                 foreach (a; r.uniqueKills.byKeyValue) {
228                     s[db.testCaseApi.getTestCaseName(a.key)] = a.value.map!((a => a.get)).array;
229                 }
230                 report["test_case_unique"] = s;
231             }
232 
233             if (!r.noUniqueKills.empty) {
234                 report["test_case_no_unique"] = r.noUniqueKills.toRange.map!(
235                         a => db.testCaseApi.getTestCaseName(a)).array;
236             }
237         }
238 
239         File(buildPath(logDir, "report.json"), "w").write(report.toJSON(true));
240     }
241 }
242 
243 private:
244 
245 import dextool.plugin.mutate.backend.report.analyzers : MutationScoreHistory;
246 
247 JSONValue[] toJson(const MutationScoreHistory data) {
248     import std.conv : to;
249 
250     auto app = appender!(JSONValue[])();
251     foreach (a; data.data) {
252         JSONValue s;
253         s["date"] = a.timeStamp.to!string;
254         s["score"] = a.score.get;
255         app.put(s);
256     }
257 
258     return app.data;
259 }