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 }