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