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