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.utility; 11 12 import std.exception : collectException; 13 import logger = std.experimental.logger; 14 15 import dextool.type; 16 17 import dextool.plugin.mutate.backend.type : Mutation, Offset, TestCase; 18 import dextool.plugin.mutate.backend.database : Database; 19 import dextool.plugin.mutate.backend.interface_ : FilesysIO, SafeInput; 20 21 @safe: 22 23 // 5 because it covers all the operators and true/false 24 immutable windowSize = 5; 25 26 immutable originalIsCorrupt = "deXtool: unable to open the file or it has changed since mutation where performed"; 27 28 immutable invalidFile = "Dextool: Invalid UTF-8 content"; 29 30 /// Create a range from `a` that has at most maxlen+3 letters in it. 31 auto window(T)(T a, size_t maxlen) { 32 import std.algorithm : filter, among, joiner; 33 import std.range : take, only, chain; 34 35 // dfmt off 36 return chain(a.take(maxlen).filter!(a => !a.among('\n')), 37 only(a.length > maxlen ? "..." : null).joiner); 38 // dfmt on 39 } 40 41 struct MakeMutationTextResult { 42 string original = originalIsCorrupt; 43 string mutation; 44 45 nothrow @safe size_t toHash() { 46 import std.digest.murmurhash; 47 48 MurmurHash3!32 hash; 49 hash.put(cast(const(ubyte)[]) original); 50 hash.put(cast(const(ubyte)[]) mutation); 51 auto h = hash.finish; 52 return ((h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3]); 53 } 54 55 bool opEquals(const this o) const nothrow @safe { 56 return original == o.original && mutation == o.mutation; 57 } 58 } 59 60 auto makeMutationText(SafeInput file_, const Offset offs, Mutation.Kind kind) nothrow { 61 import dextool.plugin.mutate.backend.generate_mutant : makeMutation; 62 63 MakeMutationTextResult rval; 64 65 try { 66 if (offs.end < file_.read.length) { 67 rval.original = file_.read[offs.begin .. offs.end].toInternal; 68 } 69 70 auto mut = makeMutation(kind); 71 rval.mutation = mut.mutate(rval.original); 72 } 73 catch (Exception e) { 74 logger.warning(e.msg).collectException; 75 } 76 77 return rval; 78 } 79 80 string toInternal(ubyte[] data) @safe nothrow { 81 import std.utf : validate; 82 83 try { 84 auto result = () @trusted{ return cast(string) data; }(); 85 validate(result); 86 return result; 87 } 88 catch (Exception e) { 89 } 90 91 return invalidFile; 92 } 93 94 void reportMutationSubtypeStats(ref const long[MakeMutationTextResult] mut_stat, ref Table!4 tbl) @safe nothrow { 95 import std.conv : to; 96 import std.format : format; 97 import std.algorithm : sum, map, sort, filter; 98 99 long total = mut_stat.byValue.sum; 100 101 import std.array : array; 102 import std.range : take; 103 import std.typecons : Tuple; 104 105 // trusted because it is marked as @safe in dmd-2.078.1 106 // TODO remove this trusted when upgrading the minimal compiler 107 // can be simplified to: 108 // foreach (v, alive.byKeyValue.array.sort!((a, b) => a.value > b.value)).... 109 auto kv = () @trusted{ 110 return mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value) 111 .take(20).map!(a => Tuple!(MakeMutationTextResult, "key", long, 112 "value")(a.key, a.value)).array; 113 }(); 114 115 foreach (v; kv) { 116 try { 117 auto percentage = (cast(double) v.value / cast(double) total) * 100.0; 118 119 // dfmt off 120 typeof(tbl).Row r = [ 121 percentage.to!string, 122 v.value.to!string, 123 format("`%s`", window(v.key.original, windowSize)), 124 format("`%s`", window(v.key.mutation, windowSize)), 125 ]; 126 // dfmt on 127 tbl.put(r); 128 } 129 catch (Exception e) { 130 logger.warning(e.msg).collectException; 131 } 132 } 133 } 134 135 void reportTestCaseStats(ref const long[TestCase] mut_stat, ref Table!3 tbl, long take_) @safe nothrow { 136 import std.algorithm : sum, sort; 137 import std.array : array; 138 import std.conv : to; 139 import std.range : take; 140 141 long total = mut_stat.byValue.sum; 142 143 foreach (v; mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value).take(take_)) { 144 try { 145 auto percentage = (cast(double) v.value / cast(double) total) * 100.0; 146 typeof(tbl).Row r = [percentage.to!string, v.value.to!string, v.key]; 147 tbl.put(r); 148 } 149 catch (Exception e) { 150 logger.warning(e.msg).collectException; 151 } 152 } 153 } 154 155 import dextool.plugin.mutate.backend.database : MutationId; 156 157 /// Information needed to present the mutant to an user. 158 struct MutationRepr { 159 import dextool.type : Path; 160 import dextool.plugin.mutate.backend.type : SourceLoc; 161 162 SourceLoc sloc; 163 Path file; 164 MakeMutationTextResult mutation; 165 } 166 167 alias Mutations = bool[MutationId]; 168 alias MutationsMap = Mutations[TestCase]; 169 alias MutationReprMap = MutationRepr[MutationId]; 170 171 void reportTestCaseKillMap(WriterTextT, WriterT)(ref const MutationsMap mut_stat, 172 ref const MutationReprMap mutrepr, WriterTextT writer_txt, WriterT writer) @safe { 173 import std.conv : to; 174 import std.range : put; 175 import std.format : format; 176 177 alias MutTable = Table!4; 178 alias Row = MutTable.Row; 179 180 foreach (tc_muts; mut_stat.byKeyValue) { 181 put(writer_txt, tc_muts.key); 182 183 MutTable tbl; 184 tbl.heading = ["ID", "File Line:Column", "From", "To"]; 185 186 foreach (mut; tc_muts.value.byKey) { 187 Row row; 188 189 if (auto v = mut in mutrepr) { 190 row[1] = format("%s %s:%s", v.file, v.sloc.line, v.sloc.column); 191 row[2] = format("`%s`", window(v.mutation.original, windowSize)); 192 row[3] = format("`%s`", window(v.mutation.mutation, windowSize)); 193 } 194 195 row[0] = mut.to!string; 196 tbl.put(row); 197 } 198 199 put(writer, tbl); 200 } 201 } 202 203 void reportMutationTestCaseSuggestion(WriterT)(ref Database db, 204 const MutationId[] tc_sugg, WriterT writer) @safe { 205 import std.conv : to; 206 import std.range : put; 207 import std.format : format; 208 209 alias MutTable = Table!1; 210 alias Row = MutTable.Row; 211 212 foreach (mut_id; tc_sugg) { 213 MutTable tbl; 214 tbl.heading = [mut_id.to!string]; 215 216 try { 217 auto suggestions = db.getSurroundingTestCases(mut_id); 218 if (suggestions.length == 0) 219 continue; 220 221 foreach (tc; suggestions) { 222 Row row; 223 row[0] = format("`%s`", tc); 224 tbl.put(row); 225 } 226 put(writer, tbl); 227 } 228 catch (Exception e) { 229 logger.warning(e.msg); 230 } 231 } 232 } 233 234 void reportStatistics(ReportT)(ref Database db, const Mutation.Kind[] kinds, ref ReportT item) @safe nothrow { 235 import core.time : dur; 236 import std.algorithm : map, filter, sum; 237 import std.range : only; 238 import std.datetime : Clock; 239 import dextool.plugin.mutate.backend.utility; 240 241 auto alive = db.aliveMutants(kinds); 242 auto killed = db.killedMutants(kinds); 243 auto timeout = db.timeoutMutants(kinds); 244 auto untested = db.unknownMutants(kinds); 245 auto killed_by_compiler = db.killedByCompilerMutants(kinds); 246 247 try { 248 immutable align_ = 8; 249 250 const auto total_time = only(alive, killed, timeout).filter!(a => !a.isNull) 251 .map!(a => a.time.total!"msecs").sum.dur!"msecs"; 252 const auto total_cnt = only(alive, killed, timeout).filter!(a => !a.isNull) 253 .map!(a => a.count).sum; 254 const auto killed_cnt = only(killed, timeout).filter!(a => !a.isNull) 255 .map!(a => a.count).sum; 256 const auto untested_cnt = untested.isNull ? 0 : untested.count; 257 const auto predicted = total_cnt > 0 ? (untested_cnt * (total_time / total_cnt)) 258 : 0.dur!"msecs"; 259 260 // execution time 261 if (untested_cnt > 0 && predicted > 0.dur!"msecs") 262 item.writefln("Predicted time until mutation testing is done: %s (%s)", 263 predicted, Clock.currTime + predicted); 264 item.writefln("%-*s %s", align_ * 4, "Mutation execution time:", total_time); 265 if (!killed_by_compiler.isNull) 266 item.tracef("%-*s %s", align_ * 4, "Mutants killed by compiler:", 267 killed_by_compiler.time); 268 269 item.writeln(""); 270 271 // mutation score and details 272 if (!untested.isNull && untested.count > 0) 273 item.writefln("Untested: %s", untested.count); 274 if (!alive.isNull) 275 item.writefln("%-*s %s", align_, "Alive:", alive.count); 276 if (!killed.isNull) 277 item.writefln("%-*s %s", align_, "Killed:", killed.count); 278 if (!timeout.isNull) 279 item.writefln("%-*s %s", align_, "Timeout:", timeout.count); 280 item.writefln("%-*s %s", align_, "Total:", total_cnt); 281 if (total_cnt > 0) 282 item.writefln("%-*s %s", align_, "Score:", 283 cast(double) killed_cnt / cast(double) total_cnt); 284 if (!killed_by_compiler.isNull) 285 item.tracef("%-*s %s", align_, "Killed by compiler:", killed_by_compiler.count); 286 } 287 catch (Exception e) { 288 logger.warning(e.msg).collectException; 289 } 290 } 291 292 struct Table(int columnsNr) { 293 alias Row = string[columnsNr]; 294 295 Row heading_; 296 Row[] rows; 297 ulong[columnsNr] columnWidth; 298 299 this(const Row heading) { 300 this.heading = heading; 301 updateColumns(heading); 302 } 303 304 void heading(const Row r) { 305 heading_ = r; 306 updateColumns(r); 307 } 308 309 void put(const Row r) { 310 rows ~= r; 311 updateColumns(r); 312 } 313 314 import std.format : FormatSpec; 315 316 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 317 import std.ascii : newline; 318 import std.range : enumerate, repeat; 319 import std.format : formattedWrite; 320 import std.range.primitives : put; 321 322 immutable sep = "|"; 323 immutable lhs_sep = "| "; 324 immutable mid_sep = " | "; 325 immutable rhs_sep = " |"; 326 327 void printRow(const ref Row r) { 328 foreach (const r_; r[].enumerate) { 329 if (r_.index == 0) 330 put(w, lhs_sep); 331 else 332 put(w, mid_sep); 333 formattedWrite(w, "%-*s", columnWidth[r_.index], r_.value); 334 } 335 put(w, rhs_sep); 336 put(w, newline); 337 } 338 339 printRow(heading_); 340 341 immutable dash = "-"; 342 foreach (len; columnWidth) { 343 put(w, sep); 344 put(w, repeat(dash, len + 2)); 345 } 346 put(w, sep); 347 put(w, newline); 348 349 foreach (const ref r; rows) { 350 printRow(r); 351 } 352 } 353 354 private void updateColumns(const ref Row r) { 355 import std.algorithm : filter, count, map; 356 import std.range : enumerate; 357 import std.utf : byCodeUnit; 358 import std.typecons : tuple; 359 360 foreach (a; r[].enumerate.map!(a => tuple(a.index, 361 a.value.byCodeUnit.count)).filter!(a => a[1] > columnWidth[a[0]])) { 362 columnWidth[a[0]] = a[1]; 363 } 364 } 365 }