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 }