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 } 480 481 MarkedMutantsStat reportMarkedMutants(ref Database db, const Mutation.Kind[] kinds, 482 string file = null) @safe { 483 MarkedMutantsStat st; 484 st.tbl.heading = [ 485 "File", "Line", "Column", "Mutation", "Status", "Rationale" 486 ]; 487 488 foreach (m; db.getMarkedMutants()) { 489 typeof(st.tbl).Row r = [ 490 m.path, m.sloc.line.to!string, m.sloc.column.to!string, 491 m.mutText, statusToString(m.toStatus), m.rationale 492 ]; 493 st.tbl.put(r); 494 } 495 return st; 496 } 497 498 struct TestCaseOverlapStat { 499 import std.format : formattedWrite; 500 import std.range : put; 501 import dextool.hash; 502 import dextool.plugin.mutate.backend.database.type : TestCaseId; 503 504 long overlap; 505 long total; 506 double ratio; 507 508 // map between test cases and the mutants they have killed. 509 TestCaseId[][Murmur3] tc_mut; 510 // map between mutation IDs and the test cases that killed them. 511 long[][Murmur3] mutid_mut; 512 string[TestCaseId] name_tc; 513 514 string sumToString() @safe const { 515 return format("%s/%s = %s test cases", overlap, total, ratio); 516 } 517 518 void sumToString(Writer)(ref Writer w) @trusted const { 519 formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio); 520 } 521 522 string toString() @safe const { 523 auto buf = appender!string; 524 toString(buf); 525 return buf.data; 526 } 527 528 void toString(Writer)(ref Writer w) @safe const { 529 sumToString(w); 530 531 foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 532 bool first = true; 533 // TODO this is a bit slow. use a DB row iterator instead. 534 foreach (name; tcs.value.map!(id => name_tc[id])) { 535 if (first) { 536 () @trusted { 537 formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length); 538 }(); 539 first = false; 540 } else { 541 () @trusted { formattedWrite(w, "%s\n", name); }(); 542 } 543 } 544 put(w, "\n"); 545 } 546 } 547 } 548 549 /** Report test cases that completly overlap each other. 550 * 551 * Returns: a string with statistics. 552 */ 553 template toTable(Flag!"colWithMutants" colMutants) { 554 static if (colMutants) { 555 alias TableT = Table!3; 556 } else { 557 alias TableT = Table!2; 558 } 559 alias RowT = TableT.Row; 560 561 void toTable(ref TestCaseOverlapStat st, ref TableT tbl) { 562 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 563 bool first = true; 564 // TODO this is a bit slow. use a DB row iterator instead. 565 foreach (name; tcs.value.map!(id => st.name_tc[id])) { 566 RowT r; 567 r[0] = name; 568 if (first) { 569 auto muts = st.mutid_mut[tcs.key]; 570 r[1] = muts.length.to!string; 571 static if (colMutants) { 572 r[2] = format("%-(%s,%)", muts); 573 } 574 first = false; 575 } 576 577 tbl.put(r); 578 } 579 static if (colMutants) 580 RowT r = ["", "", ""]; 581 else 582 RowT r = ["", ""]; 583 tbl.put(r); 584 } 585 } 586 } 587 588 /// Test cases that kill exactly the same mutants. 589 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe { 590 import dextool.hash; 591 import dextool.plugin.mutate.backend.database.type : TestCaseId; 592 593 auto profile = Profile(ReportSection.tc_full_overlap); 594 595 TestCaseOverlapStat st; 596 st.total = db.getNumOfTestCases; 597 598 foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) { 599 auto muts = db.getTestCaseMutantKills(tc_id, kinds).sort.map!(a => cast(long) a).array; 600 auto m3 = makeMurmur3(cast(ubyte[]) muts); 601 if (auto v = m3 in st.tc_mut) 602 (*v) ~= tc_id; 603 else { 604 st.tc_mut[m3] = [tc_id]; 605 st.mutid_mut[m3] = muts; 606 } 607 st.name_tc[tc_id] = db.getTestCaseName(tc_id); 608 } 609 610 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 611 st.overlap += tcs.value.count; 612 } 613 614 if (st.total > 0) 615 st.ratio = cast(double) st.overlap / cast(double) st.total; 616 617 return st; 618 } 619 620 class TestGroupSimilarity { 621 static struct TestGroup { 622 string description; 623 string name; 624 625 /// What the user configured as regex. Useful when e.g. generating reports 626 /// for a user. 627 string userInput; 628 629 int opCmp(ref const TestGroup s) const { 630 return cmp(name, s.name); 631 } 632 } 633 634 static struct Similarity { 635 /// The test group that the `key` is compared to. 636 TestGroup comparedTo; 637 /// How similare the `key` is to `comparedTo`. 638 double similarity; 639 /// Mutants that are similare between `testCase` and the parent. 640 MutationId[] intersection; 641 /// Unique mutants that are NOT verified by `testCase`. 642 MutationId[] difference; 643 } 644 645 Similarity[][TestGroup] similarities; 646 } 647 648 /** Analyze the similarity between the test groups. 649 * 650 * Assuming that a limit on how many test groups to report isn't interesting 651 * because they are few so it is never a problem. 652 * 653 */ 654 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db, 655 const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe { 656 import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId; 657 658 auto profile = Profile(ReportSection.tc_groups_similarity); 659 660 alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", MutationId[], "kills"); 661 662 const test_cases = spinSql!(() { return db.getDetectedTestCaseIds; }).map!( 663 a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() { 664 return db.getTestCase(a); 665 }))).array; 666 667 MutationId[] gatherKilledMutants(const(TestGroup) tg) { 668 auto kills = appender!(MutationId[])(); 669 foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) { 670 kills.put(spinSql!(() { 671 return db.getTestCaseMutantKills(tc.id, kinds); 672 })); 673 } 674 return kills.data; 675 } 676 677 TgKills[] test_group_kills; 678 foreach (const tg; test_groups) { 679 auto kills = gatherKilledMutants(tg); 680 if (kills.length != 0) 681 test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description, 682 tg.name, tg.userInput), kills); 683 } 684 685 // calculate similarity between all test groups. 686 auto rval = new typeof(return); 687 688 foreach (tg_parent; test_group_kills) { 689 auto app = appender!(TestGroupSimilarity.Similarity[])(); 690 foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) { 691 auto similarity = setSimilarity(tg_parent.kills, tg_other.kills); 692 if (similarity.similarity > 0) 693 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup, 694 similarity.similarity, similarity.intersection, similarity.difference)); 695 if (app.data.length != 0) 696 rval.similarities[tg_parent.testGroup] = app.data; 697 } 698 } 699 700 return rval; 701 } 702 703 class TestGroupStat { 704 import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo; 705 706 /// Human readable description for the test group. 707 string description; 708 /// Statistics for a test group. 709 MutationStat stats; 710 /// Map between test cases and their test group. 711 TestCase[] testCases; 712 /// Lookup for converting a id to a filename 713 Path[FileId] files; 714 /// Mutants alive in a file. 715 MutantInfo[][FileId] alive; 716 /// Mutants killed in a file. 717 MutantInfo[][FileId] killed; 718 } 719 720 import std.regex : Regex; 721 722 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) { 723 import std.regex : matchFirst; 724 725 auto m = matchFirst(tc.name, tg); 726 // the regex must match the full test case thus checking that 727 // nothing is left before or after 728 if (!m.empty && m.pre.length == 0 && m.post.length == 0) { 729 return true; 730 } 731 return false; 732 } 733 734 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds, 735 const(TestGroup) test_g) @safe { 736 import dextool.plugin.mutate.backend.database : MutationStatusId; 737 import dextool.set; 738 739 auto profile = Profile(ReportSection.tc_groups); 740 741 static struct TcStat { 742 Set!MutationStatusId alive; 743 Set!MutationStatusId killed; 744 Set!MutationStatusId timeout; 745 Set!MutationStatusId total; 746 747 // killed by the specific test case 748 Set!MutationStatusId tcKilled; 749 } 750 751 auto r = new TestGroupStat; 752 r.description = test_g.description; 753 TcStat tc_stat; 754 755 // map test cases to this test group 756 foreach (tc; db.getDetectedTestCases) { 757 if (tc.isTestCaseInTestGroup(test_g.re)) 758 r.testCases ~= tc; 759 } 760 761 // collect mutation statistics for each test case group 762 foreach (const tc; r.testCases) { 763 foreach (const id; db.testCaseMutationPointAliveSrcMutants(kinds, tc)) 764 tc_stat.alive.add(id); 765 foreach (const id; db.testCaseMutationPointKilledSrcMutants(kinds, tc)) 766 tc_stat.killed.add(id); 767 foreach (const id; db.testCaseMutationPointTimeoutSrcMutants(kinds, tc)) 768 tc_stat.timeout.add(id); 769 foreach (const id; db.testCaseMutationPointTotalSrcMutants(kinds, tc)) 770 tc_stat.total.add(id); 771 foreach (const id; db.testCaseKilledSrcMutants(kinds, tc)) 772 tc_stat.tcKilled.add(id); 773 } 774 775 // update the mutation stat for the test group 776 r.stats.alive = tc_stat.alive.length; 777 r.stats.killed = tc_stat.killed.length; 778 r.stats.timeout = tc_stat.timeout.length; 779 r.stats.total = tc_stat.total.length; 780 781 // associate mutants with their file 782 foreach (const m; db.getMutantsInfo(kinds, tc_stat.tcKilled.toArray)) { 783 auto fid = db.getFileId(m.id); 784 r.killed[fid.get] ~= m; 785 786 if (fid.get !in r.files) { 787 r.files[fid.get] = Path.init; 788 r.files[fid.get] = db.getFile(fid.get); 789 } 790 } 791 792 foreach (const m; db.getMutantsInfo(kinds, tc_stat.alive.toArray)) { 793 auto fid = db.getFileId(m.id); 794 r.alive[fid.get] ~= m; 795 796 if (fid.get !in r.files) { 797 r.files[fid.get] = Path.init; 798 r.files[fid.get] = db.getFile(fid.get); 799 } 800 } 801 802 return r; 803 } 804 805 /// High interest mutants. 806 class MutantSample { 807 import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo, 808 MutationStatus, MutationStatusId, MutationEntry, MutationStatusTime; 809 810 MutationEntry[MutationStatusId] mutants; 811 812 /// The mutant that had its status updated the furthest back in time. 813 MutationStatusTime[] oldest; 814 815 /// The mutant that has survived the longest in the system. 816 MutationStatus[] hardestToKill; 817 818 /// The latest mutants that where added and survived. 819 MutationStatusTime[] latest; 820 } 821 822 /// Returns: samples of mutants that are of high interest to the user. 823 MutantSample reportSelectedAliveMutants(ref Database db, 824 const(Mutation.Kind)[] kinds, long history_nr) { 825 auto profile = Profile(ReportSection.mut_recommend_kill); 826 827 auto rval = new typeof(return); 828 829 rval.hardestToKill = db.getHardestToKillMutant(kinds, Mutation.Status.alive, history_nr); 830 foreach (const mutst; rval.hardestToKill) { 831 auto ids = db.getMutationIds(kinds, [mutst.statusId]); 832 if (ids.length != 0) 833 rval.mutants[mutst.statusId] = db.getMutation(ids[0]); 834 } 835 836 rval.oldest = db.getOldestMutants(kinds, history_nr); 837 foreach (const mutst; rval.oldest) { 838 auto ids = db.getMutationIds(kinds, [mutst.id]); 839 if (ids.length != 0) 840 rval.mutants[mutst.id] = db.getMutation(ids[0]); 841 } 842 843 return rval; 844 } 845 846 class DiffReport { 847 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 848 import dextool.plugin.mutate.backend.diff_parser : Diff; 849 850 /// The mutation score. 851 double score; 852 853 /// The raw diff for a file 854 Diff.Line[][FileId] rawDiff; 855 856 /// Lookup for converting a id to a filename 857 Path[FileId] files; 858 /// Mutants alive in a file. 859 MutantInfo[][FileId] alive; 860 /// Mutants killed in a file. 861 MutantInfo[][FileId] killed; 862 /// Test cases that killed mutants. 863 TestCase[] testCases; 864 865 override string toString() @safe const { 866 import std.format : formattedWrite; 867 import std.range : put; 868 869 auto w = appender!string; 870 871 foreach (file; files.byKeyValue) { 872 put(w, file.value); 873 foreach (mut; alive[file.key]) 874 formattedWrite(w, " %s\n", mut); 875 foreach (mut; killed[file.key]) 876 formattedWrite(w, " %s\n", mut); 877 } 878 879 formattedWrite(w, "Test Cases killing mutants"); 880 foreach (tc; testCases) 881 formattedWrite(w, " %s", tc); 882 883 return w.data; 884 } 885 } 886 887 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds, 888 ref Diff diff, AbsolutePath workdir) { 889 import dextool.plugin.mutate.backend.database : MutationId, MutationStatusId; 890 import dextool.plugin.mutate.backend.type : SourceLoc; 891 import dextool.set; 892 893 auto profile = Profile(ReportSection.diff); 894 895 auto rval = new DiffReport; 896 897 Set!MutationStatusId total; 898 // used for deriving what test cases killed mutants in the diff. 899 Set!MutationId killing_mutants; 900 901 foreach (kv; diff.toRange(workdir)) { 902 auto fid = db.getFileId(kv.key); 903 if (fid.isNull) { 904 logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key); 905 continue; 906 } 907 908 bool has_mutants; 909 foreach (id; kv.value 910 .toRange 911 .map!(line => spinSql!(() => db.getMutationsOnLine(kinds, 912 fid.get, SourceLoc(line)))) 913 .joiner 914 .filter!(id => id !in total)) { 915 has_mutants = true; 916 total.add(id); 917 918 const info = db.getMutantsInfo(kinds, [id])[0]; 919 if (info.status == Mutation.Status.alive) { 920 rval.alive[fid.get] ~= info; 921 } else { 922 rval.killed[fid.get] ~= info; 923 killing_mutants.add(info.id); 924 } 925 } 926 927 if (has_mutants) { 928 rval.files[fid.get] = kv.key; 929 rval.rawDiff[fid.get] = diff.rawDiff[kv.key]; 930 } else { 931 logger.info("This file in the diff has no mutants on changed lines: ", kv.key); 932 } 933 } 934 935 Set!TestCase test_cases; 936 foreach (tc; killing_mutants.toRange.map!(a => db.getTestCases(a)).joiner) 937 test_cases.add(tc); 938 939 rval.testCases = test_cases.toArray.sort.array; 940 941 if (total.length == 0) { 942 rval.score = 1.0; 943 } else { 944 rval.score = cast(double) killing_mutants.length / cast(double) total.length; 945 } 946 947 return rval; 948 } 949 950 struct MinimalTestSet { 951 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 952 953 long total; 954 955 /// Minimal set that achieve the mutation test score. 956 TestCase[] minimalSet; 957 /// Test cases that do not contribute to the mutation test score. 958 TestCase[] redundant; 959 /// Map between test case name and sum of all the test time of the mutants it killed. 960 TestCaseInfo[string] testCaseTime; 961 } 962 963 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) { 964 import dextool.plugin.mutate.backend.database : TestCaseId, TestCaseInfo; 965 import dextool.set; 966 967 auto profile = Profile(ReportSection.tc_min_set); 968 969 alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info"); 970 971 MinimalTestSet rval; 972 973 Set!MutationId killedMutants; 974 975 // start by picking test cases that have the fewest kills. 976 foreach (const val; db.getDetectedTestCases 977 .map!(a => tuple(a, db.getTestCaseId(a))) 978 .filter!(a => !a[1].isNull) 979 .map!(a => TcIdInfo(a[0], a[1], db.getTestCaseInfo(a[0], kinds))) 980 .filter!(a => a.info.killedMutants != 0) 981 .array 982 .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) { 983 rval.testCaseTime[val.tc.name] = val.info; 984 985 const killed = killedMutants.length; 986 foreach (const id; db.getTestCaseMutantKills(val.id, kinds)) { 987 killedMutants.add(id); 988 } 989 990 if (killedMutants.length > killed) 991 rval.minimalSet ~= val.tc; 992 else 993 rval.redundant ~= val.tc; 994 } 995 996 rval.total = rval.minimalSet.length + rval.redundant.length; 997 998 return rval; 999 } 1000 1001 struct TestCaseUniqueness { 1002 MutationId[][TestCase] uniqueKills; 1003 1004 // test cases that have no unique kills. These are candidates for being 1005 // refactored/removed. 1006 TestCase[] noUniqueKills; 1007 } 1008 1009 /// Returns: a report of the mutants that a test case is the only one that kills. 1010 TestCaseUniqueness reportTestCaseUniqueness(ref Database db, const Mutation.Kind[] kinds) { 1011 import dextool.plugin.mutate.backend.database.type : TestCaseId; 1012 import dextool.set; 1013 1014 auto profile = Profile(ReportSection.tc_unique); 1015 1016 /// any time a mutant is killed by more than one test case it is removed. 1017 TestCaseId[MutationId] killedBy; 1018 Set!MutationId blacklist; 1019 1020 foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) { 1021 auto muts = db.getTestCaseMutantKills(tc_id, kinds); 1022 foreach (m; muts.filter!(a => !blacklist.contains(a))) { 1023 if (m in killedBy) { 1024 killedBy.remove(m); 1025 blacklist.add(m); 1026 } else { 1027 killedBy[m] = tc_id; 1028 } 1029 } 1030 } 1031 1032 // use a cache to reduce the database access 1033 TestCase[TestCaseId] idToTc; 1034 TestCase getTestCase(TestCaseId id) @trusted { 1035 return idToTc.require(id, spinSql!(() { return db.getTestCase(id).get; })); 1036 } 1037 1038 typeof(return) rval; 1039 Set!TestCaseId uniqueTc; 1040 foreach (kv; killedBy.byKeyValue) { 1041 rval.uniqueKills[getTestCase(kv.value)] ~= kv.key; 1042 uniqueTc.add(kv.value); 1043 } 1044 foreach (tc_id; db.getDetectedTestCaseIds.filter!(a => !uniqueTc.contains(a))) { 1045 rval.noUniqueKills ~= getTestCase(tc_id); 1046 } 1047 1048 return rval; 1049 } 1050 1051 private: 1052 1053 /** Measure how long a report takes to generate and print it as trace data. 1054 * 1055 * This is an example from clang-tidy for how it could be reported to the user. 1056 * For now it is *just* reported as it is running. 1057 * 1058 * ===-------------------------------------------------------------------------=== 1059 * clang-tidy checks profiling 1060 * ===-------------------------------------------------------------------------=== 1061 * Total Execution Time: 0.0021 seconds (0.0021 wall clock) 1062 * 1063 * ---User Time--- --System Time-- --User+System-- ---Wall Time--- --- Name --- 1064 * 0.0000 ( 0.1%) 0.0000 ( 0.0%) 0.0000 ( 0.0%) 0.0000 ( 0.1%) readability-misplaced-array-index 1065 * 0.0000 ( 0.2%) 0.0000 ( 0.0%) 0.0000 ( 0.1%) 0.0000 ( 0.1%) abseil-duration-division 1066 * 0.0012 (100.0%) 0.0009 (100.0%) 0.0021 (100.0%) 0.0021 (100.0%) Total 1067 */ 1068 struct Profile { 1069 import std.datetime.stopwatch : StopWatch; 1070 1071 ReportSection kind; 1072 StopWatch sw; 1073 1074 this(ReportSection kind) @safe nothrow @nogc { 1075 this.kind = kind; 1076 sw.start; 1077 } 1078 1079 ~this() @safe nothrow { 1080 try { 1081 sw.stop; 1082 logger.tracef("profiling:%s wall time:%s", kind, sw.peek); 1083 } catch (Exception e) { 1084 } 1085 } 1086 }