1 /** 2 Copyright: Copyright (c) 2017, 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 This module contains functionality for reporting the mutations. Both the 11 summary and details for each mutation. 12 */ 13 module dextool.plugin.mutate.backend.report; 14 15 import std.exception : collectException; 16 import logger = std.experimental.logger; 17 18 import dextool.type; 19 20 import dextool.plugin.mutate.backend.database : Database, IterateMutantRow; 21 import dextool.plugin.mutate.backend.interface_ : FilesysIO, SafeInput; 22 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportLevel, 23 ReportSection; 24 import dextool.plugin.mutate.backend.type : Mutation, Offset; 25 26 import dextool.plugin.mutate.backend.report.type : SimpleWriter, ReportEvent; 27 import dextool.plugin.mutate.backend.report.utility : MakeMutationTextResult, 28 window, windowSize, makeMutationText; 29 30 ExitStatusType runReport(ref Database db, const MutationKind[] kind, const ReportKind report_kind, 31 const ReportLevel report_level, const ReportSection[] sections, FilesysIO fio) @safe nothrow { 32 import std.stdio : write; 33 import dextool.plugin.mutate.backend.utility; 34 35 const auto kinds = dextool.plugin.mutate.backend.utility.toInternal(kind); 36 37 try { 38 auto genrep = ReportGenerator.make(kind, report_kind, report_level, sections, fio); 39 genrep.mutationKindEvent(kind is null ? [MutationKind.any] : kind); 40 41 genrep.locationStartEvent; 42 db.iterateMutants(kinds, &genrep.locationEvent); 43 genrep.locationEndEvent; 44 45 genrep.locationStatEvent; 46 47 genrep.statEvent(db); 48 } 49 catch (Exception e) { 50 logger.error(e.msg).collectException; 51 } 52 53 return ExitStatusType.Ok; 54 } 55 56 @safe: 57 private: 58 59 /** 60 * Expects the event to come in the following order: 61 * - mutationKindEvent 62 * - locationStartEvent 63 * - locationEvent 64 * - locationEndEvent 65 * - statStartEvent 66 * - statEvent 67 * - statEndEvent 68 */ 69 struct ReportGenerator { 70 import std.algorithm : each; 71 import dextool.plugin.mutate.backend.report.markdown; 72 import dextool.plugin.mutate.backend.report.plain; 73 import dextool.plugin.mutate.backend.report.csv; 74 75 ReportEvent[] listeners; 76 77 static auto make(const MutationKind[] kind, const ReportKind report_kind, 78 const ReportLevel report_level, const ReportSection[] sections, FilesysIO fio) { 79 import dextool.plugin.mutate.backend.utility; 80 81 auto kinds = dextool.plugin.mutate.backend.utility.toInternal(kind); 82 ReportEvent[] listeners; 83 84 final switch (report_kind) { 85 case ReportKind.plain: 86 listeners = [new ReportPlain(kinds, report_level, sections, fio)]; 87 break; 88 case ReportKind.markdown: 89 listeners = [new ReportMarkdown(kinds, report_level, fio)]; 90 break; 91 case ReportKind.compiler: 92 listeners = [new ReportCompiler(kinds, report_level, fio)]; 93 break; 94 case ReportKind.json: 95 listeners = [new ReportJson(report_level, fio)]; 96 break; 97 case ReportKind.csv: 98 listeners = [new ReportCSV(kinds, report_level, fio)]; 99 break; 100 } 101 102 return ReportGenerator(listeners); 103 } 104 105 void mutationKindEvent(const MutationKind[] kind_) { 106 listeners.each!(a => a.mutationKindEvent(kind_)); 107 } 108 109 void locationStartEvent() { 110 listeners.each!(a => a.locationStartEvent); 111 } 112 113 // trusted: trusting that d2sqlite3 and sqlite3 is memory safe. 114 void locationEvent(const ref IterateMutantRow r) @trusted { 115 listeners.each!(a => a.locationEvent(r)); 116 } 117 118 void locationEndEvent() { 119 listeners.each!(a => a.locationEndEvent); 120 } 121 122 void locationStatEvent() { 123 listeners.each!(a => a.locationStatEvent); 124 } 125 126 void statEvent(ref Database db) { 127 listeners.each!(a => a.statEvent(db)); 128 } 129 } 130 131 struct CompilerConsole(Writer) { 132 import std.ascii : newline; 133 import std.format : formattedWrite, format; 134 import std.range : put; 135 136 private Writer w; 137 138 this(Writer w) { 139 this.w = w; 140 } 141 142 auto build() { 143 return CompilerMsgBuilder!Writer(w); 144 } 145 } 146 147 /** Build a compiler msg that follows how GCC would print to the console. 148 * 149 * Note that it should write to stderr. 150 */ 151 struct CompilerMsgBuilder(Writer) { 152 import std.ascii : newline; 153 import std.format : formattedWrite; 154 import std.range : put; 155 156 private { 157 Writer w; 158 string file_; 159 long line_; 160 long column_; 161 } 162 163 this(Writer w) { 164 this.w = w; 165 } 166 167 auto file(string a) { 168 file_ = a; 169 return this; 170 } 171 172 auto line(long a) { 173 line_ = a; 174 return this; 175 } 176 177 auto column(long a) { 178 column_ = a; 179 return this; 180 } 181 182 auto begin(ARGS...)(auto ref ARGS args) { 183 // for now the severity is hard coded to warning because nothing else 184 // needs to be supported. 185 formattedWrite(w, "%s:%s:%s: warning: ", file_, line_, column_); 186 formattedWrite(w, args); 187 put(w, newline); 188 return this; 189 } 190 191 auto note(ARGS...)(auto ref ARGS args) { 192 formattedWrite(w, "%s:%s:%s: note: ", file_, line_, column_); 193 formattedWrite(w, args); 194 put(w, newline); 195 return this; 196 } 197 198 auto fixit(long offset, string mutation) { 199 // Example of a fixit hint from gcc: 200 // fix-it:"foo.cpp":{5:12-5:17}:"argc" 201 // the second value in the location is bytes (starting from 1) from the 202 // start of the line. 203 formattedWrite(w, `fix-it:"%s":{%s:%s-%s:%s}:"%s"`, file_, line_, 204 column_, line_, column_ + offset, mutation); 205 put(w, newline); 206 return this; 207 } 208 209 void end() { 210 } 211 } 212 213 /** Report mutations as gcc would do for compilation warnings with fixit hints. 214 * 215 * #SPC-plugin_mutate_report_for_tool_integration 216 */ 217 @safe final class ReportCompiler : ReportEvent { 218 import std.algorithm : each; 219 import std.conv : to; 220 import std.format : format; 221 import dextool.plugin.mutate.backend.utility; 222 223 const Mutation.Kind[] kinds; 224 const ReportLevel report_level; 225 FilesysIO fio; 226 227 CompilerConsole!SimpleWriter compiler; 228 229 this(Mutation.Kind[] kinds, ReportLevel report_level, FilesysIO fio) { 230 this.kinds = kinds; 231 this.report_level = report_level; 232 this.fio = fio; 233 } 234 235 override void mutationKindEvent(const MutationKind[]) { 236 compiler = CompilerConsole!SimpleWriter(delegate(const(char)[] s) @trusted{ 237 import std.stdio : stderr, write; 238 239 stderr.write(s); 240 }); 241 } 242 243 override void locationStartEvent() { 244 } 245 246 override void locationEvent(const ref IterateMutantRow r) @trusted { 247 import dextool.plugin.mutate.backend.generate_mutant : makeMutation; 248 249 void report() { 250 AbsolutePath abs_path; 251 MakeMutationTextResult mut_txt; 252 try { 253 abs_path = AbsolutePath(FileName(r.file), DirName(fio.getOutputDir)); 254 mut_txt = makeMutationText(fio.makeInput(abs_path), 255 r.mutationPoint.offset, r.mutation.kind); 256 } 257 catch (Exception e) { 258 logger.warning(e.msg); 259 } 260 261 // dfmt off 262 auto b = compiler.build 263 .file(abs_path) 264 .line(r.sloc.line) 265 .column(r.sloc.column) 266 .begin("%s: replace '%s' with '%s'", r.mutation.kind.toUser, 267 window(mut_txt.original, windowSize), 268 window(mut_txt.mutation, windowSize)) 269 .note("status:%s id:%s", r.mutation.status, r.id); 270 271 if (mut_txt.original.length > windowSize) 272 b = b.note("replace '%s'", mut_txt.original); 273 if (mut_txt.mutation.length > windowSize) 274 b = b.note("with '%s'", mut_txt.mutation); 275 276 b.fixit(r.mutationPoint.offset.end - r.mutationPoint.offset.begin, mut_txt.mutation) 277 .end; 278 // dfmt on 279 } 280 281 try { 282 // summary is the default and according to the specification of the 283 // default for tool integration alive mutations shall be printed. 284 final switch (report_level) { 285 case ReportLevel.summary: 286 break; 287 case ReportLevel.alive: 288 if (r.mutation.status == Mutation.Status.alive) { 289 report(); 290 } 291 break; 292 case ReportLevel.all: 293 report(); 294 break; 295 } 296 } 297 catch (Exception e) { 298 logger.trace(e.msg).collectException; 299 } 300 } 301 302 override void locationEndEvent() { 303 } 304 305 override void locationStatEvent() { 306 } 307 308 override void statEvent(ref Database db) { 309 } 310 } 311 312 /** 313 * Expects locations to be grouped by file. 314 * 315 * TODO this is ugly. Use a JSON serializer instead. 316 */ 317 @safe final class ReportJson : ReportEvent { 318 import std.array : array; 319 import std.algorithm : map, joiner; 320 import std.conv : to; 321 import std.format : format; 322 import std.json; 323 324 const ReportLevel report_level; 325 FilesysIO fio; 326 327 JSONValue report; 328 JSONValue current_file; 329 330 Path last_file; 331 332 this(ReportLevel report_level, FilesysIO fio) { 333 this.report_level = report_level; 334 this.fio = fio; 335 } 336 337 override void mutationKindEvent(const MutationKind[] kinds) { 338 report = ["types" : kinds.map!(a => a.to!string).array, "files" : []]; 339 } 340 341 override void locationStartEvent() { 342 } 343 344 override void locationEvent(const ref IterateMutantRow r) @trusted { 345 bool new_file; 346 347 if (last_file.length == 0) { 348 current_file = ["filename" : r.file, "checksum" : format("%x", r.fileChecksum)]; 349 new_file = true; 350 } else if (last_file != r.file) { 351 report["files"].array ~= current_file; 352 current_file = ["filename" : r.file, "checksum" : format("%x", r.fileChecksum)]; 353 new_file = true; 354 } 355 356 auto appendMutant() { 357 JSONValue m = ["id" : r.id.to!long]; 358 m.object["kind"] = r.mutation.kind.to!string; 359 m.object["status"] = r.mutation.status.to!string; 360 m.object["line"] = r.sloc.line; 361 m.object["column"] = r.sloc.column; 362 m.object["begin"] = r.mutationPoint.offset.begin; 363 m.object["end"] = r.mutationPoint.offset.end; 364 365 try { 366 MakeMutationTextResult mut_txt; 367 auto abs_path = AbsolutePath(FileName(r.file), DirName(fio.getOutputDir)); 368 mut_txt = makeMutationText(fio.makeInput(abs_path), 369 r.mutationPoint.offset, r.mutation.kind); 370 m.object["value"] = mut_txt.mutation; 371 } 372 catch (Exception e) { 373 logger.warning(e.msg); 374 } 375 if (new_file) { 376 last_file = r.file; 377 current_file.object["mutants"] = JSONValue([m]); 378 } else { 379 current_file["mutants"].array ~= m; 380 } 381 } 382 383 final switch (report_level) { 384 case ReportLevel.summary: 385 break; 386 case ReportLevel.alive: 387 if (r.mutation.status == Mutation.Status.alive) { 388 appendMutant; 389 } 390 break; 391 case ReportLevel.all: 392 appendMutant; 393 break; 394 } 395 } 396 397 override void locationEndEvent() @trusted { 398 report["files"].array ~= current_file; 399 } 400 401 override void locationStatEvent() { 402 import std.stdio : writeln; 403 404 writeln(report.toJSON(true)); 405 } 406 407 override void statEvent(ref Database db) { 408 } 409 }