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