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 logger = std.experimental.logger; 13 import std.exception : collectException; 14 import std.typecons : Flag, Yes, No; 15 16 import dextool.plugin.mutate.backend.database : Database, spinSqlQuery, MutationId; 17 import dextool.plugin.mutate.backend.diff_parser : Diff; 18 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 19 import dextool.plugin.mutate.backend.type : Mutation, Offset, TestCase, Language, TestGroup; 20 import dextool.plugin.mutate.type : ReportKillSortOrder; 21 import dextool.plugin.mutate.type : ReportLevel, ReportSection; 22 import dextool.type; 23 24 public import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, 25 makeMutationText; 26 27 // 5 because it covers all the operators and true/false 28 immutable windowSize = 5; 29 30 immutable invalidFile = "Dextool: Invalid UTF-8 content"; 31 32 @safe: 33 34 /// Create a range from `a` that has at most maxlen+3 letters in it. 35 string window(T)(T a, size_t maxlen = windowSize) { 36 import std.algorithm : filter, among; 37 import std.conv : text; 38 import std.range : take, chain; 39 import std.uni : byGrapheme, byCodePoint; 40 import dextool.plugin.mutate.backend.type : invalidUtf8; 41 42 try { 43 return chain(a.byGrapheme.take(maxlen) 44 .byCodePoint.filter!(a => !a.among('\n')).text, a.length > maxlen ? "..." : null) 45 .text; 46 } catch (Exception e) { 47 return invalidUtf8; 48 } 49 } 50 51 ReportSection[] toSections(const ReportLevel l) { 52 ReportSection[] secs; 53 final switch (l) with (ReportSection) { 54 case ReportLevel.summary: 55 secs = [summary]; 56 break; 57 case ReportLevel.alive: 58 secs = [summary, mut_stat, tc_killed_no_mutants, tc_full_overlap, alive]; 59 break; 60 case ReportLevel.all: 61 secs = [ 62 summary, mut_stat, all_mut, tc_killed, tc_killed_no_mutants, 63 tc_full_overlap 64 ]; 65 break; 66 } 67 68 return secs; 69 } 70 71 string toInternal(ubyte[] data) @safe nothrow { 72 import std.utf : validate; 73 74 try { 75 auto result = () @trusted { return cast(string) data; }(); 76 validate(result); 77 return result; 78 } catch (Exception e) { 79 } 80 81 return invalidFile; 82 } 83 84 void reportMutationSubtypeStats(ref const long[MakeMutationTextResult] mut_stat, ref Table!4 tbl) @safe nothrow { 85 import std.algorithm : sum, map, sort, filter; 86 import std.array : array; 87 import std.conv : to; 88 import std.format : format; 89 import std.range : take; 90 91 long total = mut_stat.byValue.sum; 92 93 foreach (v; mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value).take(20)) { 94 try { 95 auto percentage = (cast(double) v.value / cast(double) total) * 100.0; 96 97 // dfmt off 98 typeof(tbl).Row r = [ 99 percentage.to!string, 100 v.value.to!string, 101 format("`%s`", window(v.key.original, windowSize)), 102 format("`%s`", window(v.key.mutation, windowSize)), 103 ]; 104 // dfmt on 105 tbl.put(r); 106 } catch (Exception e) { 107 logger.warning(e.msg).collectException; 108 } 109 } 110 } 111 112 /** Update the table with the score of test cases and how many mutants they killed. 113 * 114 * Params: 115 * db = ? 116 * kinds = type of mutants to count for the test case 117 * take_ = how many from the top should be moved to the table 118 * sort_order = ctrl if the top or bottom of the test cases should be reported 119 * tbl = table to write the data to 120 */ 121 void reportTestCaseStats(ref Database db, const Mutation.Kind[] kinds, 122 const long take_, const ReportKillSortOrder sort_order, ref Table!3 tbl) @safe nothrow { 123 import std.algorithm : sort, map; 124 import std.array : array; 125 import std.conv : to; 126 import std.range : take, retro; 127 import std.typecons : Tuple; 128 import dextool.plugin.mutate.backend.database : spinSqlQuery; 129 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 130 131 alias TcInfo = Tuple!(string, "name", TestCaseInfo, "tc"); 132 133 const total = spinSqlQuery!(() { return db.totalMutants(kinds).count; }); 134 135 // nothing to do. this also ensure that we do not divide by zero. 136 if (total == 0) 137 return; 138 139 static bool cmp(T)(ref T a, ref T b) { 140 if (a.tc.killedMutants > b.tc.killedMutants) 141 return true; 142 else if (a.tc.killedMutants < b.tc.killedMutants) 143 return false; 144 else if (a.name > b.name) 145 return true; 146 else if (a.name < b.name) 147 return false; 148 return false; 149 } 150 151 auto takeOrder(RangeT)(RangeT range) { 152 final switch (sort_order) { 153 case ReportKillSortOrder.top: 154 return range.take(take_).array; 155 case ReportKillSortOrder.bottom: 156 return range.array.retro.take(take_).array; 157 } 158 } 159 160 const test_cases = spinSqlQuery!(() { return db.getDetectedTestCases; }); 161 162 foreach (v; takeOrder(test_cases.map!(a => TcInfo(a.name, spinSqlQuery!(() { 163 return db.getTestCaseInfo(a, kinds); 164 }))) 165 .array 166 .sort!cmp)) { 167 try { 168 auto percentage = (cast(double) v.tc.killedMutants / cast(double) total) * 100.0; 169 typeof(tbl).Row r = [ 170 percentage.to!string, v.tc.killedMutants.to!string, v.name 171 ]; 172 tbl.put(r); 173 } catch (Exception e) { 174 logger.warning(e.msg).collectException; 175 } 176 } 177 } 178 179 /** The result of analysing the test cases to see how similare they are to each 180 * other. 181 */ 182 class TestCaseSimilarityAnalyse { 183 import dextool.plugin.mutate.backend.type : TestCase; 184 185 static struct Similarity { 186 TestCase testCase; 187 double similarity; 188 /// Mutants that are similare between `testCase` and the parent. 189 MutationId[] intersection; 190 /// Unique mutants that are NOT verified by `testCase`. 191 MutationId[] difference; 192 } 193 194 Similarity[][TestCase] similarities; 195 } 196 197 /// The result of the similarity analyse 198 private struct Similarity { 199 /// The quota |A intersect B| / |A|. Thus it is how similare A is to B. If 200 /// B ever fully encloses A then the score is 1.0. 201 double similarity; 202 MutationId[] intersection; 203 MutationId[] difference; 204 } 205 206 // The set similairty measures how much of lhs is in rhs. This is a 207 // directional metric. 208 private Similarity setSimilarity(MutationId[] lhs_, MutationId[] rhs_) { 209 import std.array : array; 210 import dextool.set; 211 212 auto lhs = setFromList(lhs_); 213 auto rhs = setFromList(rhs_); 214 auto intersect = lhs.intersect(rhs); 215 auto diff = lhs.setDifference(rhs); 216 return Similarity(cast(double) intersect.length / cast(double) lhs.length, 217 intersect.byKey.array, diff.byKey.array); 218 } 219 220 /** Analyse the similarity between test cases. 221 * 222 * TODO: the algorithm used is slow. Maybe matrix representation and sorted is better? 223 * 224 * Params: 225 * db = ? 226 * kinds = mutation kinds to use in the distance analyze 227 * limit = limit the number of test cases to the top `limit`. 228 */ 229 TestCaseSimilarityAnalyse reportTestCaseSimilarityAnalyse(ref Database db, 230 const Mutation.Kind[] kinds, ulong limit) @safe { 231 import std.algorithm : map, filter; 232 import std.array : array, appender; 233 import std.container.binaryheap; 234 import std.conv : to; 235 import std.range : take; 236 import std.typecons : Tuple; 237 import dextool.cachetools; 238 import dextool.plugin.mutate.backend.database : spinSqlQuery; 239 import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId; 240 241 // TODO: reduce the code duplication of the caches. 242 // The DB lookups must be cached or otherwise the algorithm becomes too slow for practical use. 243 244 MutationId[][TestCaseId] kill_cache2; 245 MutationId[] getKills(TestCaseId id) @trusted { 246 return kill_cache2.require(id, spinSqlQuery!(() { 247 return db.getTestCaseMutantKills(id, kinds); 248 })); 249 } 250 251 TestCase[TestCaseId] tc_cache2; 252 TestCase getTestCase(TestCaseId id) @trusted { 253 return tc_cache2.require(id, spinSqlQuery!(() { 254 return db.getTestCase(id); 255 })); 256 } 257 258 alias TcKills = Tuple!(TestCaseId, "id", MutationId[], "kills"); 259 260 const test_cases = spinSqlQuery!(() { return db.getDetectedTestCaseIds; }); 261 262 auto rval = new typeof(return); 263 264 foreach (tc_kill; test_cases.map!(a => TcKills(a, getKills(a))) 265 .filter!(a => a.kills.length != 0)) { 266 auto app = appender!(TestCaseSimilarityAnalyse.Similarity[])(); 267 foreach (tc; test_cases.filter!(a => a != tc_kill.id) 268 .map!(a => TcKills(a, getKills(a))) 269 .filter!(a => a.kills.length != 0)) { 270 auto distance = setSimilarity(tc_kill.kills, tc.kills); 271 if (distance.similarity > 0) 272 app.put(TestCaseSimilarityAnalyse.Similarity(getTestCase(tc.id), 273 distance.similarity, distance.intersection, distance.difference)); 274 } 275 if (app.data.length != 0) { 276 () @trusted { 277 rval.similarities[getTestCase(tc_kill.id)] = heapify!((a, 278 b) => a.similarity < b.similarity)(app.data).take(limit).array; 279 }(); 280 } 281 } 282 283 return rval; 284 } 285 286 /// Statistics about dead test cases. 287 struct TestCaseDeadStat { 288 import std.range : isOutputRange; 289 290 /// The ratio of dead TC of the total. 291 double ratio; 292 TestCase[] testCases; 293 long total; 294 295 long numDeadTC() @safe pure nothrow const @nogc scope { 296 return testCases.length; 297 } 298 299 string toString() @safe const { 300 import std.array : appender; 301 302 auto buf = appender!string; 303 toString(buf); 304 return buf.data; 305 } 306 307 void toString(Writer)(ref Writer w) @safe const 308 if (isOutputRange!(Writer, char)) { 309 import std.ascii : newline; 310 import std.format : formattedWrite; 311 import std.range : put; 312 313 if (total > 0) 314 formattedWrite(w, "%s/%s = %s of all test cases\n", numDeadTC, total, ratio); 315 foreach (tc; testCases) { 316 put(w, tc.name); 317 if (tc.location.length > 0) { 318 put(w, " | "); 319 put(w, tc.location); 320 } 321 put(w, newline); 322 } 323 } 324 } 325 326 void toTable(ref TestCaseDeadStat st, ref Table!2 tbl) @safe pure nothrow { 327 foreach (tc; st.testCases) { 328 typeof(tbl).Row r = [tc.name, tc.location]; 329 tbl.put(r); 330 } 331 } 332 333 /** Returns: report of test cases that has killed zero mutants. 334 */ 335 TestCaseDeadStat reportDeadTestCases(ref Database db) @safe { 336 TestCaseDeadStat r; 337 r.total = db.getNumOfTestCases; 338 r.testCases = db.getTestCasesWithZeroKills; 339 if (r.total > 0) 340 r.ratio = cast(double) r.numDeadTC / cast(double) r.total; 341 return r; 342 } 343 344 /// Information needed to present the mutant to an user. 345 struct MutationRepr { 346 import dextool.type : Path; 347 import dextool.plugin.mutate.backend.type : SourceLoc; 348 349 SourceLoc sloc; 350 Path file; 351 MakeMutationTextResult mutation; 352 } 353 354 alias Mutations = bool[MutationId]; 355 alias MutationsMap = Mutations[TestCase]; 356 alias MutationReprMap = MutationRepr[MutationId]; 357 358 void reportTestCaseKillMap(WriterTextT, WriterT)(ref const MutationsMap mut_stat, 359 ref const MutationReprMap mutrepr, WriterTextT writer_txt, WriterT writer) @safe { 360 import std.conv : to; 361 import std.range : put; 362 import std.format : format; 363 364 alias MutTable = Table!4; 365 alias Row = MutTable.Row; 366 367 foreach (tc_muts; mut_stat.byKeyValue) { 368 put(writer_txt, tc_muts.key.toString); 369 370 MutTable tbl; 371 tbl.heading = ["ID", "File Line:Column", "From", "To"]; 372 373 foreach (mut; tc_muts.value.byKey) { 374 Row row; 375 376 if (auto v = mut in mutrepr) { 377 row[1] = format("%s %s:%s", v.file, v.sloc.line, v.sloc.column); 378 row[2] = format("`%s`", window(v.mutation.original, windowSize)); 379 row[3] = format("`%s`", window(v.mutation.mutation, windowSize)); 380 } 381 382 row[0] = mut.to!string; 383 tbl.put(row); 384 } 385 386 put(writer, tbl); 387 } 388 } 389 390 void reportMutationTestCaseSuggestion(WriterT)(ref Database db, 391 const MutationId[] tc_sugg, WriterT writer) @safe { 392 import std.conv : to; 393 import std.range : put; 394 import std.format : format; 395 396 alias MutTable = Table!1; 397 alias Row = MutTable.Row; 398 399 foreach (mut_id; tc_sugg) { 400 MutTable tbl; 401 tbl.heading = [mut_id.to!string]; 402 403 try { 404 auto suggestions = db.getSurroundingTestCases(mut_id); 405 if (suggestions.length == 0) 406 continue; 407 408 foreach (tc; suggestions) { 409 Row row; 410 row[0] = format("`%s`", tc); 411 tbl.put(row); 412 } 413 put(writer, tbl); 414 } catch (Exception e) { 415 logger.warning(e.msg); 416 } 417 } 418 } 419 420 /// Statistics for a group of mutants. 421 struct MutationStat { 422 import core.time : Duration; 423 import std.range : isOutputRange; 424 425 long alive; 426 // Nr of mutants that are alive but tagged with nomut. 427 long aliveNoMut; 428 long killed; 429 long timeout; 430 long untested; 431 long killedByCompiler; 432 long total; 433 434 Duration totalTime; 435 Duration killedByCompilerTime; 436 Duration predictedDone; 437 438 /// Adjust the score with the alive mutants that are suppressed. 439 double score() @safe pure nothrow const @nogc { 440 if (total > 0) 441 return cast(double)(killed + timeout + aliveNoMut) / cast(double) total; 442 if (untested > 0) 443 return 0.0; 444 return 1.0; 445 } 446 447 /// Suppressed mutants of the total mutants. 448 double suppressedOfTotal() @safe pure nothrow const @nogc { 449 if (total > 0) 450 return (cast(double)(aliveNoMut) / cast(double) total); 451 return 0.0; 452 } 453 454 string toString() @safe const { 455 import std.array : appender; 456 457 auto buf = appender!string; 458 toString(buf); 459 return buf.data; 460 } 461 462 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 463 import core.time : dur; 464 import std.ascii : newline; 465 import std.datetime : Clock; 466 import std.format : formattedWrite; 467 import std.range : put; 468 import dextool.plugin.mutate.backend.utility; 469 470 immutable align_ = 8; 471 472 // execution time 473 if (untested > 0 && predictedDone > 0.dur!"msecs") 474 formattedWrite(w, "Predicted time until mutation testing is done: %s (%s)\n", 475 predictedDone, Clock.currTime + predictedDone); 476 formattedWrite(w, "%-*s %s\n", align_ * 4, "Mutation execution time:", totalTime); 477 if (killedByCompiler > 0) 478 formattedWrite(w, "%-*s %s\n", align_ * 4, 479 "Mutants killed by compiler:", killedByCompilerTime); 480 put(w, newline); 481 482 // mutation score and details 483 if (untested > 0) 484 formattedWrite(w, "Untested: %s\n", untested); 485 formattedWrite(w, "%-*s %.3s\n", align_, "Score:", score); 486 formattedWrite(w, "%-*s %s\n", align_, "Alive:", alive); 487 formattedWrite(w, "%-*s %s\n", align_, "Killed:", killed); 488 formattedWrite(w, "%-*s %s\n", align_, "Timeout:", timeout); 489 formattedWrite(w, "%-*s %s\n", align_, "Total:", total); 490 formattedWrite(w, "%-*s %s\n", align_, "Killed by compiler:", killedByCompiler); 491 if (aliveNoMut != 0) 492 formattedWrite(w, "%-*s %s (%.3s)\n", align_, 493 "Suppressed (nomut):", aliveNoMut, suppressedOfTotal); 494 } 495 } 496 497 MutationStat reportStatistics(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow { 498 import core.time : dur; 499 import std.algorithm : map, sum; 500 import std.range : only; 501 import dextool.plugin.mutate.backend.utility; 502 503 const alive = spinSqlQuery!(() { return db.aliveSrcMutants(kinds, file); }); 504 const alive_nomut = spinSqlQuery!(() { 505 return db.aliveNoMutSrcMutants(kinds, file); 506 }); 507 const killed = spinSqlQuery!(() { return db.killedSrcMutants(kinds, file); }); 508 const timeout = spinSqlQuery!(() { return db.timeoutSrcMutants(kinds, file); }); 509 const untested = spinSqlQuery!(() { return db.unknownSrcMutants(kinds, file); }); 510 const killed_by_compiler = spinSqlQuery!(() { 511 return db.killedByCompilerSrcMutants(kinds, file); 512 }); 513 const total = spinSqlQuery!(() { return db.totalSrcMutants(kinds, file); }); 514 515 MutationStat st; 516 st.alive = alive.count; 517 st.aliveNoMut = alive_nomut.count; 518 st.killed = killed.count; 519 st.timeout = timeout.count; 520 st.untested = untested.count; 521 st.total = total.count; 522 st.totalTime = total.time; 523 st.predictedDone = st.total > 0 ? (st.untested * (st.totalTime / st.total)) : 0.dur!"msecs"; 524 st.killedByCompilerTime = killed_by_compiler.time; 525 526 return st; 527 } 528 529 struct TestCaseOverlapStat { 530 import std.format : formattedWrite, format; 531 import std.range : put; 532 import dextool.hash; 533 import dextool.plugin.mutate.backend.database.type : TestCaseId; 534 535 long overlap; 536 long total; 537 double ratio; 538 539 // map between test cases and the mutants they have killed. 540 TestCaseId[][Murmur3] tc_mut; 541 // map between mutation IDs and the test cases that killed them. 542 long[][Murmur3] mutid_mut; 543 string[TestCaseId] name_tc; 544 545 string sumToString() @safe const { 546 return format("%s/%s = %s test cases", overlap, total, ratio); 547 } 548 549 void sumToString(Writer)(ref Writer w) @safe const { 550 formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio); 551 } 552 553 string toString() @safe const { 554 import std.array : appender; 555 556 auto buf = appender!string; 557 toString(buf); 558 return buf.data; 559 } 560 561 void toString(Writer)(ref Writer w) @safe const { 562 import std.algorithm : sort, map, filter, count; 563 import std.array : array; 564 565 sumToString(w); 566 567 foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 568 bool first = true; 569 // TODO this is a bit slow. use a DB row iterator instead. 570 foreach (name; tcs.value.map!(id => name_tc[id])) { 571 if (first) { 572 formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length); 573 first = false; 574 } else { 575 formattedWrite(w, "%s\n", name); 576 } 577 } 578 put(w, "\n"); 579 } 580 } 581 } 582 583 /** Report test cases that completly overlap each other. 584 * 585 * Returns: a string with statistics. 586 */ 587 template toTable(Flag!"colWithMutants" colMutants) { 588 static if (colMutants) { 589 alias TableT = Table!3; 590 } else { 591 alias TableT = Table!2; 592 } 593 alias RowT = TableT.Row; 594 595 void toTable(ref TestCaseOverlapStat st, ref TableT tbl) { 596 import std.algorithm : sort, map, filter, count; 597 import std.array : array; 598 import std.conv : to; 599 import std.format : format; 600 601 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 602 bool first = true; 603 // TODO this is a bit slow. use a DB row iterator instead. 604 foreach (name; tcs.value.map!(id => st.name_tc[id])) { 605 RowT r; 606 r[0] = name; 607 if (first) { 608 auto muts = st.mutid_mut[tcs.key]; 609 r[1] = muts.length.to!string; 610 static if (colMutants) { 611 r[2] = format("%-(%s,%)", muts); 612 } 613 first = false; 614 } 615 616 tbl.put(r); 617 } 618 static if (colMutants) 619 RowT r = ["", "", ""]; 620 else 621 RowT r = ["", ""]; 622 tbl.put(r); 623 } 624 } 625 } 626 627 /// Test cases that kill exactly the same mutants. 628 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe { 629 import std.algorithm : sort, map, filter, count; 630 import std.array : array; 631 import dextool.hash; 632 import dextool.plugin.mutate.backend.database.type : TestCaseId; 633 634 TestCaseOverlapStat st; 635 st.total = db.getNumOfTestCases; 636 637 foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) { 638 auto muts = db.getTestCaseMutantKills(tc_id, kinds).sort.map!(a => cast(long) a).array; 639 auto m3 = makeMurmur3(cast(ubyte[]) muts); 640 if (auto v = m3 in st.tc_mut) 641 (*v) ~= tc_id; 642 else { 643 st.tc_mut[m3] = [tc_id]; 644 st.mutid_mut[m3] = muts; 645 } 646 st.name_tc[tc_id] = db.getTestCaseName(tc_id); 647 } 648 649 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 650 st.overlap += tcs.value.count; 651 } 652 653 if (st.total > 0) 654 st.ratio = cast(double) st.overlap / cast(double) st.total; 655 656 return st; 657 } 658 659 class TestGroupSimilarity { 660 static struct TestGroup { 661 string description; 662 string name; 663 664 /// What the user configured as regex. Useful when e.g. generating reports 665 /// for a user. 666 string userInput; 667 668 int opCmp(ref const TestGroup s) const { 669 import std.algorithm : cmp; 670 671 return cmp(name, s.name); 672 } 673 } 674 675 static struct Similarity { 676 /// The test group that the `key` is compared to. 677 TestGroup comparedTo; 678 /// How similare the `key` is to `comparedTo`. 679 double similarity; 680 /// Mutants that are similare between `testCase` and the parent. 681 MutationId[] intersection; 682 /// Unique mutants that are NOT verified by `testCase`. 683 MutationId[] difference; 684 } 685 686 Similarity[][TestGroup] similarities; 687 } 688 689 /** Analyze the similarity between the test groups. 690 * 691 * Assuming that a limit on how many test groups to report isn't interesting 692 * because they are few so it is never a problem. 693 * 694 */ 695 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db, 696 const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe { 697 import std.algorithm : map, filter; 698 import std.array : appender, array; 699 import std.typecons : Tuple; 700 import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId; 701 702 alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", MutationId[], "kills"); 703 704 const test_cases = spinSqlQuery!(() { return db.getDetectedTestCaseIds; }).map!( 705 a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSqlQuery!(() { 706 return db.getTestCase(a); 707 }))).array; 708 709 MutationId[] gatherKilledMutants(const(TestGroup) tg) { 710 auto kills = appender!(MutationId[])(); 711 foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) { 712 kills.put(spinSqlQuery!(() { 713 return db.getTestCaseMutantKills(tc.id, kinds); 714 })); 715 } 716 return kills.data; 717 } 718 719 TgKills[] test_group_kills; 720 foreach (const tg; test_groups) { 721 auto kills = gatherKilledMutants(tg); 722 if (kills.length != 0) 723 test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description, 724 tg.name, tg.userInput), kills); 725 } 726 727 // calculate similarity between all test groups. 728 auto rval = new typeof(return); 729 730 foreach (tg_parent; test_group_kills) { 731 auto app = appender!(TestGroupSimilarity.Similarity[])(); 732 foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) { 733 auto similarity = setSimilarity(tg_parent.kills, tg_other.kills); 734 if (similarity.similarity > 0) 735 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup, 736 similarity.similarity, similarity.intersection, similarity.difference)); 737 if (app.data.length != 0) 738 rval.similarities[tg_parent.testGroup] = app.data; 739 } 740 } 741 742 return rval; 743 } 744 745 class TestGroupStat { 746 import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo; 747 748 /// Human readable description for the test group. 749 string description; 750 /// Statistics for a test group. 751 MutationStat stats; 752 /// Map between test cases and their test group. 753 TestCase[] testCases; 754 /// Lookup for converting a id to a filename 755 Path[FileId] files; 756 /// Mutants alive in a file. 757 MutantInfo[][FileId] alive; 758 /// Mutants killed in a file. 759 MutantInfo[][FileId] killed; 760 } 761 762 import std.regex : Regex; 763 764 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) { 765 import std.regex : matchFirst; 766 767 auto m = matchFirst(tc.name, tg); 768 // the regex must match the full test case thus checking that 769 // nothing is left before or after 770 if (!m.empty && m.pre.length == 0 && m.post.length == 0) { 771 return true; 772 } 773 return false; 774 } 775 776 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds, 777 const(TestGroup) test_g) @safe { 778 import std.algorithm : filter, map; 779 import std.array : appender; 780 import std.typecons : tuple; 781 import std.range : only; 782 import dextool.plugin.mutate.backend.database : MutationStatusId; 783 import dextool.set; 784 785 static struct TcStat { 786 Set!MutationStatusId alive; 787 Set!MutationStatusId killed; 788 Set!MutationStatusId timeout; 789 Set!MutationStatusId total; 790 791 // killed by the specific test case 792 Set!MutationStatusId tcKilled; 793 } 794 795 auto r = new TestGroupStat; 796 r.description = test_g.description; 797 TcStat tc_stat; 798 799 // map test cases to this test group 800 foreach (tc; db.getDetectedTestCases) { 801 if (tc.isTestCaseInTestGroup(test_g.re)) 802 r.testCases ~= tc; 803 } 804 805 // collect mutation statistics for each test case group 806 foreach (const tc; r.testCases) { 807 foreach (const id; db.testCaseMutationPointAliveSrcMutants(kinds, tc)) 808 tc_stat.alive.add(id); 809 foreach (const id; db.testCaseMutationPointKilledSrcMutants(kinds, tc)) 810 tc_stat.killed.add(id); 811 foreach (const id; db.testCaseMutationPointTimeoutSrcMutants(kinds, tc)) 812 tc_stat.timeout.add(id); 813 foreach (const id; db.testCaseMutationPointTotalSrcMutants(kinds, tc)) 814 tc_stat.total.add(id); 815 foreach (const id; db.testCaseKilledSrcMutants(kinds, tc)) 816 tc_stat.tcKilled.add(id); 817 } 818 819 // update the mutation stat for the test group 820 r.stats.alive = tc_stat.alive.length; 821 r.stats.killed = tc_stat.killed.length; 822 r.stats.timeout = tc_stat.timeout.length; 823 r.stats.total = tc_stat.total.length; 824 825 // associate mutants with their file 826 foreach (const m; db.getMutantsInfo(kinds, tc_stat.tcKilled.setToList!MutationStatusId)) { 827 auto fid = db.getFileId(m.id); 828 r.killed[fid] ~= m; 829 830 if (fid !in r.files) { 831 r.files[fid] = Path.init; 832 r.files[fid] = db.getFile(fid); 833 } 834 } 835 836 foreach (const m; db.getMutantsInfo(kinds, tc_stat.alive.setToList!MutationStatusId)) { 837 auto fid = db.getFileId(m.id); 838 r.alive[fid] ~= m; 839 840 if (fid !in r.files) { 841 r.files[fid] = Path.init; 842 r.files[fid] = db.getFile(fid); 843 } 844 } 845 846 return r; 847 } 848 849 /// High interest mutants. 850 class MutantSample { 851 import std.typecons : Nullable; 852 import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo, 853 MutationStatus, MutationStatusId, MutationEntry, MutationStatusTime; 854 855 MutationEntry[MutationStatusId] mutants; 856 857 /// The mutant that had its status updated the furthest back in time. 858 MutationStatusTime[] oldest; 859 860 /// The mutant that has survived the longest in the system. 861 MutationStatus[] hardestToKill; 862 863 /// The latest mutants that where added and survived. 864 MutationStatusTime[] latest; 865 } 866 867 /// Returns: samples of mutants that are of high interest to the user. 868 MutantSample reportSelectedAliveMutants(ref Database db, 869 const(Mutation.Kind)[] kinds, long history_nr) { 870 auto rval = new typeof(return); 871 872 rval.hardestToKill = db.getHardestToKillMutant(kinds, Mutation.Status.alive, history_nr); 873 foreach (const mutst; rval.hardestToKill) { 874 auto ids = db.getMutationIds(kinds, [mutst.statusId]); 875 if (ids.length != 0) 876 rval.mutants[mutst.statusId] = db.getMutation(ids[0]); 877 } 878 879 rval.oldest = db.getOldestMutants(kinds, history_nr); 880 foreach (const mutst; rval.oldest) { 881 auto ids = db.getMutationIds(kinds, [mutst.id]); 882 if (ids.length != 0) 883 rval.mutants[mutst.id] = db.getMutation(ids[0]); 884 } 885 886 return rval; 887 } 888 889 class DiffReport { 890 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 891 import dextool.plugin.mutate.backend.diff_parser : Diff; 892 893 /// The mutation score. 894 double score; 895 896 /// The raw diff for a file 897 Diff.Line[][FileId] rawDiff; 898 899 /// Lookup for converting a id to a filename 900 Path[FileId] files; 901 /// Mutants alive in a file. 902 MutantInfo[][FileId] alive; 903 /// Mutants killed in a file. 904 MutantInfo[][FileId] killed; 905 /// Test cases that killed mutants. 906 TestCase[] testCases; 907 908 override string toString() @safe const { 909 import std.algorithm : map; 910 import std.array : appender; 911 import std.format : formattedWrite; 912 import std.range : put; 913 914 auto w = appender!string; 915 916 foreach (file; files.byKeyValue) { 917 put(w, file.value); 918 foreach (mut; alive[file.key]) 919 formattedWrite(w, " %s\n", mut); 920 foreach (mut; killed[file.key]) 921 formattedWrite(w, " %s\n", mut); 922 } 923 924 formattedWrite(w, "Test Cases killing mutants"); 925 foreach (tc; testCases) 926 formattedWrite(w, " %s", tc); 927 928 return w.data; 929 } 930 } 931 932 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds, 933 ref Diff diff, AbsolutePath workdir) { 934 import std.array : array; 935 import std.algorithm : map, joiner, sort; 936 import dextool.plugin.mutate.backend.database : MutationId; 937 import dextool.plugin.mutate.backend.type : SourceLoc; 938 import dextool.set; 939 940 auto rval = new DiffReport; 941 942 Set!MutationId killing_mutants; 943 944 long total; 945 long killed; 946 947 foreach (kv; diff.toRange(workdir)) { 948 auto fid = db.getFileId(kv.key); 949 if (fid.isNull) { 950 logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key); 951 continue; 952 } 953 954 bool has_mutants; 955 foreach (line; setToRange!uint(kv.value)) { 956 auto muts = db.getMutantsInfo(kinds, db.getMutationsOnLine(kinds, 957 fid, SourceLoc(line, 0))); 958 foreach (m; muts) { 959 has_mutants = true; 960 if (m.status == Mutation.Status.alive) 961 rval.alive[fid] ~= m; 962 else { 963 rval.killed[fid] ~= m; 964 killing_mutants.add(m.id); 965 ++killed; 966 } 967 ++total; 968 } 969 } 970 971 if (has_mutants) { 972 rval.files[fid] = kv.key; 973 rval.rawDiff[fid] = diff.rawDiff[kv.absPath]; 974 } else { 975 logger.info("This file in the diff has no mutants on changed lines: ", kv.key); 976 } 977 } 978 979 Set!TestCase test_cases; 980 foreach (tc; killing_mutants.setToRange!MutationId 981 .map!(a => db.getTestCases(a)) 982 .joiner) 983 test_cases.add(tc); 984 985 rval.testCases = test_cases.setToList!TestCase.sort.array; 986 987 if (total == 0) { 988 rval.score = 1.0; 989 } else { 990 rval.score = cast(double) killed / cast(double) total; 991 } 992 993 return rval; 994 } 995 996 struct MinimalTestSet { 997 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 998 999 long total; 1000 1001 /// Minimal set that achieve the mutation test score. 1002 TestCase[] minimalSet; 1003 /// Test cases that do not contribute to the mutation test score. 1004 TestCase[] redundant; 1005 /// Map between test case name and sum of all the test time of the mutants it killed. 1006 TestCaseInfo[string] testCaseTime; 1007 } 1008 1009 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) { 1010 import std.algorithm : map, filter, sort; 1011 import std.array : array; 1012 import std.typecons : Tuple, tuple; 1013 import dextool.plugin.mutate.backend.database : TestCaseId, TestCaseInfo; 1014 import dextool.set; 1015 1016 alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info"); 1017 1018 MinimalTestSet rval; 1019 1020 Set!MutationId killedMutants; 1021 1022 // start by picking test cases that have the fewest kills. 1023 foreach (const val; db.getDetectedTestCases 1024 .map!(a => tuple(a, db.getTestCaseId(a))) 1025 .filter!(a => !a[1].isNull) 1026 .map!(a => TcIdInfo(a[0], a[1], db.getTestCaseInfo(a[0], kinds))) 1027 .filter!(a => a.info.killedMutants != 0) 1028 .array 1029 .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) { 1030 rval.testCaseTime[val.tc.name] = val.info; 1031 1032 const killed = killedMutants.length; 1033 foreach (const id; db.getTestCaseMutantKills(val.id, kinds)) { 1034 killedMutants.add(id); 1035 } 1036 1037 if (killedMutants.length > killed) 1038 rval.minimalSet ~= val.tc; 1039 else 1040 rval.redundant ~= val.tc; 1041 } 1042 1043 rval.total = rval.minimalSet.length + rval.redundant.length; 1044 1045 return rval; 1046 } 1047 1048 struct Table(int columnsNr) { 1049 alias Row = string[columnsNr]; 1050 1051 Row heading_; 1052 Row[] rows; 1053 ulong[columnsNr] columnWidth; 1054 1055 this(const Row heading) { 1056 this.heading = heading; 1057 updateColumns(heading); 1058 } 1059 1060 bool empty() @safe pure nothrow const @nogc { 1061 return rows.length == 0; 1062 } 1063 1064 void heading(const Row r) { 1065 heading_ = r; 1066 updateColumns(r); 1067 } 1068 1069 void put(const Row r) { 1070 rows ~= r; 1071 updateColumns(r); 1072 } 1073 1074 import std.format : FormatSpec; 1075 1076 void toString(Writer, Char)(scope Writer w, FormatSpec!Char fmt) const { 1077 import std.ascii : newline; 1078 import std.range : enumerate, repeat; 1079 import std.format : formattedWrite; 1080 import std.range.primitives : put; 1081 1082 immutable sep = "|"; 1083 immutable lhs_sep = "| "; 1084 immutable mid_sep = " | "; 1085 immutable rhs_sep = " |"; 1086 1087 void printRow(const ref Row r) { 1088 foreach (const r_; r[].enumerate) { 1089 if (r_.index == 0) 1090 put(w, lhs_sep); 1091 else 1092 put(w, mid_sep); 1093 formattedWrite(w, "%-*s", columnWidth[r_.index], r_.value); 1094 } 1095 put(w, rhs_sep); 1096 put(w, newline); 1097 } 1098 1099 printRow(heading_); 1100 1101 immutable dash = "-"; 1102 foreach (len; columnWidth) { 1103 put(w, sep); 1104 put(w, repeat(dash, len + 2)); 1105 } 1106 put(w, sep); 1107 put(w, newline); 1108 1109 foreach (const ref r; rows) { 1110 printRow(r); 1111 } 1112 } 1113 1114 private void updateColumns(const ref Row r) { 1115 import std.algorithm : filter, count, map; 1116 import std.range : enumerate; 1117 import std.utf : byCodeUnit; 1118 import std.typecons : tuple; 1119 1120 foreach (a; r[].enumerate 1121 .map!(a => tuple(a.index, a.value.byCodeUnit.count)) 1122 .filter!(a => a[1] > columnWidth[a[0]])) { 1123 columnWidth[a[0]] = a[1]; 1124 } 1125 } 1126 }