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, empty; 18 import std.conv : to; 19 import std.datetime : SysTime, Duration; 20 import std.exception : collectException; 21 import std.format : format; 22 import std.range : take, retro, only; 23 import std.typecons : Flag, Yes, No, Tuple, Nullable, tuple; 24 25 import my.named_type; 26 import my.optional; 27 import my.set; 28 29 import dextool.plugin.mutate.backend.database : Database, spinSql, MutationId, 30 MarkedMutant, TestCaseId, MutationStatusId; 31 import dextool.plugin.mutate.backend.diff_parser : Diff; 32 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult, 33 makeMutationText, makeMutation; 34 import dextool.plugin.mutate.backend.interface_ : FilesysIO; 35 import dextool.plugin.mutate.backend.report.utility : window, windowSize, 36 statusToString, kindToString; 37 import dextool.plugin.mutate.backend.type : Mutation, Offset, TestCase, TestGroup; 38 import dextool.plugin.mutate.backend.utility : Profile; 39 import dextool.plugin.mutate.type : ReportKillSortOrder, ReportSection; 40 import dextool.type; 41 42 static import dextool.plugin.mutate.backend.database.type; 43 44 public import dextool.plugin.mutate.backend.report.utility : Table; 45 public import dextool.plugin.mutate.backend.type : MutantTimeProfile; 46 47 version (unittest) { 48 import unit_threaded.assertions; 49 } 50 51 @safe: 52 53 void reportMutationSubtypeStats(ref const long[MakeMutationTextResult] mut_stat, ref Table!4 tbl) @safe nothrow { 54 auto profile = Profile(ReportSection.mut_stat); 55 56 long total = mut_stat.byValue.sum; 57 58 foreach (v; mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value).take(20)) { 59 try { 60 auto percentage = (cast(double) v.value / cast(double) total) * 100.0; 61 62 // dfmt off 63 typeof(tbl).Row r = [ 64 percentage.to!string, 65 v.value.to!string, 66 format("`%s`", window(v.key.original, windowSize)), 67 format("`%s`", window(v.key.mutation, windowSize)), 68 ]; 69 // dfmt on 70 tbl.put(r); 71 } catch (Exception e) { 72 logger.warning(e.msg).collectException; 73 } 74 } 75 } 76 77 /** Test case score based on how many mutants they killed. 78 */ 79 struct TestCaseStat { 80 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 81 82 struct Info { 83 double ratio = 0.0; 84 TestCase tc; 85 TestCaseInfo info; 86 alias info this; 87 } 88 89 Info[TestCase] testCases; 90 91 /// Returns: the test cases sorted from most kills to least kills. 92 auto toSortedRange() { 93 static bool cmp(T)(ref T a, ref T b) { 94 if (a.killedMutants > b.killedMutants) 95 return true; 96 else if (a.killedMutants < b.killedMutants) 97 return false; 98 else if (a.tc.name > b.tc.name) 99 return true; 100 else if (a.tc.name < b.tc.name) 101 return false; 102 return false; 103 } 104 105 return testCases.byValue.array.sort!cmp; 106 } 107 } 108 109 /** Update the table with the score of test cases and how many mutants they killed. 110 * 111 * Params: 112 * take_ = how many from the top should be moved to the table 113 * sort_order = ctrl if the top or bottom of the test cases should be reported 114 * tbl = table to write the data to 115 */ 116 void toTable(ref TestCaseStat st, const long take_, 117 const ReportKillSortOrder sort_order, ref Table!3 tbl) @safe nothrow { 118 auto takeOrder(RangeT)(RangeT range) { 119 final switch (sort_order) { 120 case ReportKillSortOrder.top: 121 return range.take(take_).array; 122 case ReportKillSortOrder.bottom: 123 return range.retro.take(take_).array; 124 } 125 } 126 127 foreach (v; takeOrder(st.toSortedRange)) { 128 try { 129 typeof(tbl).Row r = [ 130 (100.0 * v.ratio).to!string, v.info.killedMutants.to!string, 131 v.tc.name 132 ]; 133 tbl.put(r); 134 } catch (Exception e) { 135 logger.warning(e.msg).collectException; 136 } 137 } 138 } 139 140 /** Extract the number of source code mutants that a test case has killed and 141 * how much the kills contributed to the total. 142 */ 143 TestCaseStat reportTestCaseStats(ref Database db, const Mutation.Kind[] kinds) @safe nothrow { 144 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 145 146 auto profile = Profile(ReportSection.tc_stat); 147 148 const total = spinSql!(() { return db.mutantApi.totalSrcMutants(kinds).count; }); 149 // nothing to do. this also ensure that we do not divide by zero. 150 if (total == 0) 151 return TestCaseStat.init; 152 153 alias TcInfo = Tuple!(TestCase, "tc", TestCaseInfo, "info"); 154 alias TcInfo2 = Tuple!(TestCase, "tc", Nullable!TestCaseInfo, "info"); 155 TestCaseStat rval; 156 157 foreach (v; spinSql!(() { return db.testCaseApi.getDetectedTestCases; }).map!( 158 a => TcInfo2(a, spinSql!(() { 159 return db.testCaseApi.getTestCaseInfo(a, kinds); 160 }))) 161 .filter!(a => !a.info.isNull) 162 .map!(a => TcInfo(a.tc, a.info.get))) { 163 try { 164 const ratio = cast(double) v.info.killedMutants / cast(double) total; 165 rval.testCases[v.tc] = TestCaseStat.Info(ratio, v.tc, v.info); 166 } catch (Exception e) { 167 logger.warning(e.msg).collectException; 168 } 169 } 170 171 return rval; 172 } 173 174 /** The result of analysing the test cases to see how similare they are to each 175 * other. 176 */ 177 class TestCaseSimilarityAnalyse { 178 import dextool.plugin.mutate.backend.type : TestCase; 179 180 static struct Similarity { 181 TestCaseId testCase; 182 double similarity = 0.0; 183 /// Mutants that are similare between `testCase` and the parent. 184 MutationStatusId[] intersection; 185 /// Unique mutants that are NOT verified by `testCase`. 186 MutationStatusId[] difference; 187 } 188 189 Similarity[][TestCaseId] similarities; 190 } 191 192 /// The result of the similarity analyse 193 private struct Similarity { 194 /// The quota |A intersect B| / |A|. Thus it is how similare A is to B. If 195 /// B ever fully encloses A then the score is 1.0. 196 double similarity = 0.0; 197 MutationStatusId[] intersection; 198 MutationStatusId[] difference; 199 } 200 201 // The set similairty measures how much of lhs is in rhs. This is a 202 // directional metric. 203 private Similarity setSimilarity(MutationStatusId[] lhs_, MutationStatusId[] rhs_) { 204 auto lhs = lhs_.toSet; 205 auto rhs = rhs_.toSet; 206 auto intersect = lhs.intersect(rhs); 207 auto diff = lhs.setDifference(rhs); 208 return Similarity(cast(double) intersect.length / cast(double) lhs.length, 209 intersect.toArray, diff.toArray); 210 } 211 212 /** Analyse the similarity between test cases. 213 * 214 * TODO: the algorithm used is slow. Maybe matrix representation and sorted is better? 215 * 216 * Params: 217 * db = ? 218 * kinds = mutation kinds to use in the distance analyze 219 * limit = limit the number of test cases to the top `limit`. 220 */ 221 TestCaseSimilarityAnalyse reportTestCaseSimilarityAnalyse(ref Database db, 222 const Mutation.Kind[] kinds, ulong limit) @safe { 223 import std.container.binaryheap; 224 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 225 226 auto profile = Profile(ReportSection.tc_similarity); 227 228 // TODO: reduce the code duplication of the caches. 229 // The DB lookups must be cached or otherwise the algorithm becomes too 230 // slow for practical use. 231 232 MutationStatusId[][TestCaseId] kill_cache2; 233 MutationStatusId[] getKills(TestCaseId id) @trusted { 234 return kill_cache2.require(id, spinSql!(() { 235 return db.testCaseApi.testCaseKilledSrcMutants(kinds, id); 236 })); 237 } 238 239 alias TcKills = Tuple!(TestCaseId, "id", MutationStatusId[], "kills"); 240 241 const test_cases = spinSql!(() { 242 return db.testCaseApi.getDetectedTestCaseIds; 243 }); 244 245 auto rval = new typeof(return); 246 247 foreach (tc_kill; test_cases.map!(a => TcKills(a, getKills(a))) 248 .filter!(a => a.kills.length != 0)) { 249 auto app = appender!(TestCaseSimilarityAnalyse.Similarity[])(); 250 foreach (tc; test_cases.filter!(a => a != tc_kill.id) 251 .map!(a => TcKills(a, getKills(a))) 252 .filter!(a => a.kills.length != 0)) { 253 auto distance = setSimilarity(tc_kill.kills, tc.kills); 254 if (distance.similarity > 0) 255 app.put(TestCaseSimilarityAnalyse.Similarity(tc.id, 256 distance.similarity, distance.intersection, distance.difference)); 257 } 258 if (app.data.length != 0) { 259 () @trusted { 260 rval.similarities[tc_kill.id] = heapify!((a, 261 b) => a.similarity < b.similarity)(app.data).take(limit).array; 262 }(); 263 } 264 } 265 266 return rval; 267 } 268 269 /// Statistics about dead test cases. 270 struct TestCaseDeadStat { 271 import std.range : isOutputRange; 272 273 /// The ratio of dead TC of the total. 274 double ratio = 0.0; 275 TestCase[] testCases; 276 long total; 277 278 long numDeadTC() @safe pure nothrow const @nogc scope { 279 return testCases.length; 280 } 281 282 string toString() @safe const { 283 auto buf = appender!string; 284 toString(buf); 285 return buf.data; 286 } 287 288 void toString(Writer)(ref Writer w) @safe const 289 if (isOutputRange!(Writer, char)) { 290 import std.ascii : newline; 291 import std.format : formattedWrite; 292 import std.range : put; 293 294 if (total > 0) 295 formattedWrite(w, "%s/%s = %s of all test cases\n", numDeadTC, total, ratio); 296 foreach (tc; testCases) { 297 put(w, tc.name); 298 if (tc.location.length > 0) { 299 put(w, " | "); 300 put(w, tc.location); 301 } 302 put(w, newline); 303 } 304 } 305 } 306 307 void toTable(ref TestCaseDeadStat st, ref Table!2 tbl) @safe pure nothrow { 308 foreach (tc; st.testCases) { 309 typeof(tbl).Row r = [tc.name, tc.location]; 310 tbl.put(r); 311 } 312 } 313 314 /** Returns: report of test cases that has killed zero mutants. 315 */ 316 TestCaseDeadStat reportDeadTestCases(ref Database db) @safe { 317 auto profile = Profile(ReportSection.tc_killed_no_mutants); 318 319 TestCaseDeadStat r; 320 r.total = db.testCaseApi.getNumOfTestCases; 321 r.testCases = db.testCaseApi.getTestCasesWithZeroKills; 322 if (r.total > 0) 323 r.ratio = cast(double) r.numDeadTC / cast(double) r.total; 324 return r; 325 } 326 327 /// Only the mutation score thus a subset of all statistics. 328 struct MutationScore { 329 import core.time : Duration; 330 331 long alive; 332 long killed; 333 long timeout; 334 long total; 335 long noCoverage; 336 long equivalent; 337 long skipped; 338 long memOverload; 339 MutantTimeProfile totalTime; 340 341 // Nr of mutants that are alive but tagged with nomut. 342 long aliveNoMut; 343 344 double score() @safe pure nothrow const @nogc { 345 if ((total - aliveNoMut) > 0) { 346 return cast(double)(killed + timeout + memOverload) / cast(double)(total - aliveNoMut); 347 } 348 return 0.0; 349 } 350 } 351 352 MutationScore reportScore(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow { 353 auto profile = Profile("reportScore"); 354 355 typeof(return) rval; 356 rval.alive = spinSql!(() => db.mutantApi.aliveSrcMutants(kinds, file)).count; 357 rval.killed = spinSql!(() => db.mutantApi.killedSrcMutants(kinds, file)).count; 358 rval.timeout = spinSql!(() => db.mutantApi.timeoutSrcMutants(kinds, file)).count; 359 rval.aliveNoMut = spinSql!(() => db.mutantApi.aliveNoMutSrcMutants(kinds, file)).count; 360 rval.noCoverage = spinSql!(() => db.mutantApi.noCovSrcMutants(kinds, file)).count; 361 rval.equivalent = spinSql!(() => db.mutantApi.equivalentMutants(kinds, file)).count; 362 rval.skipped = spinSql!(() => db.mutantApi.skippedMutants(kinds, file)).count; 363 rval.memOverload = spinSql!(() => db.mutantApi.memOverloadMutants(kinds, file)).count; 364 365 const total = spinSql!(() => db.mutantApi.totalSrcMutants(kinds, file)); 366 rval.totalTime = total.time; 367 rval.total = total.count; 368 369 return rval; 370 } 371 372 /// Statistics for a group of mutants. 373 struct MutationStat { 374 import core.time : Duration; 375 import std.range : isOutputRange; 376 377 long untested; 378 long killedByCompiler; 379 long worklist; 380 381 long alive() @safe pure nothrow const @nogc { 382 return scoreData.alive; 383 } 384 385 long noCoverage() @safe pure nothrow const @nogc { 386 return scoreData.noCoverage; 387 } 388 389 /// Nr of mutants that are alive but tagged with nomut. 390 long aliveNoMut() @safe pure nothrow const @nogc { 391 return scoreData.aliveNoMut; 392 } 393 394 long killed() @safe pure nothrow const @nogc { 395 return scoreData.killed; 396 } 397 398 long timeout() @safe pure nothrow const @nogc { 399 return scoreData.timeout; 400 } 401 402 long equivalent() @safe pure nothrow const @nogc { 403 return scoreData.equivalent; 404 } 405 406 long skipped() @safe pure nothrow const @nogc { 407 return scoreData.skipped; 408 } 409 410 long memOverload() @safe pure nothrow const @nogc { 411 return scoreData.memOverload; 412 } 413 414 long total() @safe pure nothrow const @nogc { 415 return scoreData.total; 416 } 417 418 MutantTimeProfile totalTime() @safe pure nothrow const @nogc { 419 return scoreData.totalTime; 420 } 421 422 MutationScore scoreData; 423 MutantTimeProfile killedByCompilerTime; 424 Duration predictedDone; 425 426 /// Adjust the score with the alive mutants that are suppressed. 427 double score() @safe pure nothrow const @nogc { 428 return scoreData.score; 429 } 430 431 /// Suppressed mutants of the total mutants. 432 double suppressedOfTotal() @safe pure nothrow const @nogc { 433 if (total > 0) { 434 return (cast(double)(aliveNoMut) / cast(double) total); 435 } 436 return 0.0; 437 } 438 439 string toString() @safe const { 440 auto buf = appender!string; 441 toString(buf); 442 return buf.data; 443 } 444 445 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 446 import core.time : dur; 447 import std.ascii : newline; 448 import std.datetime : Clock; 449 import std.format : formattedWrite; 450 import std.range : put; 451 import dextool.plugin.mutate.backend.utility; 452 453 immutable align_ = 19; 454 455 formattedWrite(w, "%-*s %s\n", align_, "Time spent:", totalTime); 456 if (untested > 0 && predictedDone > 0.dur!"msecs") { 457 const pred = Clock.currTime + predictedDone; 458 formattedWrite(w, "Remaining: %s (%s)\n", predictedDone, pred.toISOExtString); 459 } 460 if (killedByCompiler > 0) { 461 formattedWrite(w, "%-*s %s\n", align_ * 3, 462 "Time spent on mutants killed by compiler:", killedByCompilerTime); 463 } 464 465 put(w, newline); 466 467 // mutation score and details 468 formattedWrite(w, "%-*s %.3s\n", align_, "Score:", score); 469 470 formattedWrite(w, "%-*s %s\n", align_, "Total:", total); 471 if (untested > 0) { 472 formattedWrite(w, "%-*s %s\n", align_, "Untested:", untested); 473 } 474 formattedWrite(w, "%-*s %s\n", align_, "Alive:", alive); 475 formattedWrite(w, "%-*s %s\n", align_, "Killed:", killed); 476 if (skipped > 0) 477 formattedWrite(w, "%-*s %s\n", align_, "Skipped:", skipped); 478 if (equivalent > 0) 479 formattedWrite(w, "%-*s %s\n", align_, "Equivalent:", equivalent); 480 formattedWrite(w, "%-*s %s\n", align_, "Timeout:", timeout); 481 formattedWrite(w, "%-*s %s\n", align_, "Killed by compiler:", killedByCompiler); 482 if (worklist > 0) { 483 formattedWrite(w, "%-*s %s\n", align_, "Worklist:", worklist); 484 } 485 486 if (aliveNoMut > 0) { 487 formattedWrite(w, "%-*s %s (%.3s)\n", align_, 488 "Suppressed (nomut):", aliveNoMut, suppressedOfTotal); 489 } 490 } 491 } 492 493 MutationStat reportStatistics(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow { 494 import core.time : dur; 495 import dextool.plugin.mutate.backend.utility; 496 497 auto profile = Profile(ReportSection.summary); 498 499 const untested = spinSql!(() => db.mutantApi.unknownSrcMutants(kinds, file)); 500 const worklist = spinSql!(() => db.worklistApi.getCount); 501 const killedByCompiler = spinSql!(() => db.mutantApi.killedByCompilerSrcMutants(kinds, file)); 502 503 MutationStat st; 504 st.scoreData = reportScore(db, kinds, file); 505 st.untested = untested.count; 506 st.killedByCompiler = killedByCompiler.count; 507 st.worklist = worklist; 508 509 st.predictedDone = () { 510 auto avg = calcAvgPerMutant(db, kinds); 511 return (st.worklist * avg.total!"msecs").dur!"msecs"; 512 }(); 513 st.killedByCompilerTime = killedByCompiler.time; 514 515 return st; 516 } 517 518 struct MarkedMutantsStat { 519 Table!6 tbl; 520 } 521 522 MarkedMutantsStat reportMarkedMutants(ref Database db, const Mutation.Kind[] kinds, 523 string file = null) @safe { 524 MarkedMutantsStat st; 525 st.tbl.heading = [ 526 "File", "Line", "Column", "Mutation", "Status", "Rationale" 527 ]; 528 529 foreach (m; db.markMutantApi.getMarkedMutants()) { 530 typeof(st.tbl).Row r = [ 531 m.path, m.sloc.line.to!string, m.sloc.column.to!string, 532 m.mutText, statusToString(m.toStatus), m.rationale.get 533 ]; 534 st.tbl.put(r); 535 } 536 return st; 537 } 538 539 struct TestCaseOverlapStat { 540 import std.format : formattedWrite; 541 import std.range : put; 542 import my.hash; 543 544 long overlap; 545 long total; 546 double ratio = 0.0; 547 548 // map between test cases and the mutants they have killed. 549 TestCaseId[][Murmur3] tc_mut; 550 // map between mutation IDs and the test cases that killed them. 551 long[][Murmur3] mutid_mut; 552 string[TestCaseId] name_tc; 553 554 string sumToString() @safe const { 555 return format("%s/%s = %s test cases", overlap, total, ratio); 556 } 557 558 void sumToString(Writer)(ref Writer w) @trusted const { 559 formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio); 560 } 561 562 string toString() @safe const { 563 auto buf = appender!string; 564 toString(buf); 565 return buf.data; 566 } 567 568 void toString(Writer)(ref Writer w) @safe const { 569 sumToString(w); 570 571 foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 572 bool first = true; 573 // TODO this is a bit slow. use a DB row iterator instead. 574 foreach (name; tcs.value.map!(id => name_tc[id])) { 575 if (first) { 576 () @trusted { 577 formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length); 578 }(); 579 first = false; 580 } else { 581 () @trusted { formattedWrite(w, "%s\n", name); }(); 582 } 583 } 584 put(w, "\n"); 585 } 586 } 587 } 588 589 /** Report test cases that completly overlap each other. 590 * 591 * Returns: a string with statistics. 592 */ 593 template toTable(Flag!"colWithMutants" colMutants) { 594 static if (colMutants) { 595 alias TableT = Table!3; 596 } else { 597 alias TableT = Table!2; 598 } 599 alias RowT = TableT.Row; 600 601 void toTable(ref TestCaseOverlapStat st, ref TableT tbl) { 602 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 603 bool first = true; 604 // TODO this is a bit slow. use a DB row iterator instead. 605 foreach (name; tcs.value.map!(id => st.name_tc[id])) { 606 RowT r; 607 r[0] = name; 608 if (first) { 609 auto muts = st.mutid_mut[tcs.key]; 610 r[1] = muts.length.to!string; 611 static if (colMutants) { 612 r[2] = format("%-(%s,%)", muts); 613 } 614 first = false; 615 } 616 617 tbl.put(r); 618 } 619 static if (colMutants) 620 RowT r = ["", "", ""]; 621 else 622 RowT r = ["", ""]; 623 tbl.put(r); 624 } 625 } 626 } 627 628 /// Test cases that kill exactly the same mutants. 629 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe { 630 import my.hash; 631 632 auto profile = Profile(ReportSection.tc_full_overlap); 633 634 TestCaseOverlapStat st; 635 st.total = db.testCaseApi.getNumOfTestCases; 636 637 foreach (tc_id; db.testCaseApi.getTestCasesWithAtLeastOneKill(kinds)) { 638 auto muts = db.testCaseApi.getTestCaseMutantKills(tc_id, kinds) 639 .sort.map!(a => cast(long) a).array; 640 auto m3 = makeMurmur3(cast(ubyte[]) muts); 641 if (auto v = m3 in st.tc_mut) 642 (*v) ~= tc_id; 643 else { 644 st.tc_mut[m3] = [tc_id]; 645 st.mutid_mut[m3] = muts; 646 } 647 st.name_tc[tc_id] = db.testCaseApi.getTestCaseName(tc_id); 648 } 649 650 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 651 st.overlap += tcs.value.count; 652 } 653 654 if (st.total > 0) 655 st.ratio = cast(double) st.overlap / cast(double) st.total; 656 657 return st; 658 } 659 660 class TestGroupSimilarity { 661 static struct TestGroup { 662 string description; 663 string name; 664 665 /// What the user configured as regex. Useful when e.g. generating reports 666 /// for a user. 667 string userInput; 668 669 int opCmp(ref const TestGroup s) const { 670 return cmp(name, s.name); 671 } 672 } 673 674 static struct Similarity { 675 /// The test group that the `key` is compared to. 676 TestGroup comparedTo; 677 /// How similare the `key` is to `comparedTo`. 678 double similarity = 0.0; 679 /// Mutants that are similare between `testCase` and the parent. 680 MutationStatusId[] intersection; 681 /// Unique mutants that are NOT verified by `testCase`. 682 MutationStatusId[] difference; 683 } 684 685 Similarity[][TestGroup] similarities; 686 } 687 688 /** Analyze the similarity between the test groups. 689 * 690 * Assuming that a limit on how many test groups to report isn't interesting 691 * because they are few so it is never a problem. 692 * 693 */ 694 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db, 695 const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe { 696 auto profile = Profile(ReportSection.tc_groups_similarity); 697 698 alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", 699 MutationStatusId[], "kills"); 700 701 const test_cases = spinSql!(() { 702 return db.testCaseApi.getDetectedTestCaseIds; 703 }).map!(a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() { 704 return db.testCaseApi.getTestCase(a).get; 705 }))).array; 706 707 MutationStatusId[] gatherKilledMutants(const(TestGroup) tg) { 708 auto kills = appender!(MutationStatusId[])(); 709 foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) { 710 kills.put(spinSql!(() { 711 return db.testCaseApi.testCaseKilledSrcMutants(kinds, tc.id); 712 })); 713 } 714 return kills.data; 715 } 716 717 TgKills[] test_group_kills; 718 foreach (const tg; test_groups) { 719 auto kills = gatherKilledMutants(tg); 720 if (kills.length != 0) 721 test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description, 722 tg.name, tg.userInput), kills); 723 } 724 725 // calculate similarity between all test groups. 726 auto rval = new typeof(return); 727 728 foreach (tg_parent; test_group_kills) { 729 auto app = appender!(TestGroupSimilarity.Similarity[])(); 730 foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) { 731 auto similarity = setSimilarity(tg_parent.kills, tg_other.kills); 732 if (similarity.similarity > 0) 733 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup, 734 similarity.similarity, similarity.intersection, similarity.difference)); 735 if (app.data.length != 0) 736 rval.similarities[tg_parent.testGroup] = app.data; 737 } 738 } 739 740 return rval; 741 } 742 743 class TestGroupStat { 744 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 745 746 /// Human readable description for the test group. 747 string description; 748 /// Statistics for a test group. 749 MutationStat stats; 750 /// Map between test cases and their test group. 751 TestCase[] testCases; 752 /// Lookup for converting a id to a filename 753 Path[FileId] files; 754 /// Mutants alive in a file. 755 MutantInfo[][FileId] alive; 756 /// Mutants killed in a file. 757 MutantInfo[][FileId] killed; 758 } 759 760 import std.regex : Regex; 761 762 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) { 763 import std.regex : matchFirst; 764 765 auto m = matchFirst(tc.name, tg); 766 // the regex must match the full test case thus checking that 767 // nothing is left before or after 768 if (!m.empty && m.pre.length == 0 && m.post.length == 0) { 769 return true; 770 } 771 return false; 772 } 773 774 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds, 775 const(TestGroup) test_g) @safe { 776 auto profile = Profile(ReportSection.tc_groups); 777 778 static struct TcStat { 779 Set!MutationStatusId alive; 780 Set!MutationStatusId killed; 781 Set!MutationStatusId timeout; 782 Set!MutationStatusId total; 783 784 // killed by the specific test case 785 Set!MutationStatusId tcKilled; 786 } 787 788 auto r = new TestGroupStat; 789 r.description = test_g.description; 790 TcStat tc_stat; 791 792 // map test cases to this test group 793 foreach (tc; db.testCaseApi.getDetectedTestCases) { 794 if (tc.isTestCaseInTestGroup(test_g.re)) 795 r.testCases ~= tc; 796 } 797 798 // collect mutation statistics for each test case group 799 foreach (const tc; r.testCases) { 800 foreach (const id; db.testCaseApi.testCaseMutationPointAliveSrcMutants(kinds, tc)) 801 tc_stat.alive.add(id); 802 foreach (const id; db.testCaseApi.testCaseMutationPointKilledSrcMutants(kinds, tc)) 803 tc_stat.killed.add(id); 804 foreach (const id; db.testCaseApi.testCaseMutationPointTimeoutSrcMutants(kinds, tc)) 805 tc_stat.timeout.add(id); 806 foreach (const id; db.testCaseApi.testCaseMutationPointTotalSrcMutants(kinds, tc)) 807 tc_stat.total.add(id); 808 foreach (const id; db.testCaseApi.testCaseKilledSrcMutants(kinds, tc)) 809 tc_stat.tcKilled.add(id); 810 } 811 812 // update the mutation stat for the test group 813 r.stats.scoreData.alive = tc_stat.alive.length; 814 r.stats.scoreData.killed = tc_stat.killed.length; 815 r.stats.scoreData.timeout = tc_stat.timeout.length; 816 r.stats.scoreData.total = tc_stat.total.length; 817 818 // associate mutants with their file 819 foreach (const m; db.mutantApi.getMutantsInfo(kinds, tc_stat.tcKilled.toArray)) { 820 auto fid = db.getFileId(m.id); 821 r.killed[fid.get] ~= m; 822 823 if (fid.get !in r.files) { 824 r.files[fid.get] = Path.init; 825 r.files[fid.get] = db.getFile(fid.get).get; 826 } 827 } 828 829 foreach (const m; db.mutantApi.getMutantsInfo(kinds, tc_stat.alive.toArray)) { 830 auto fid = db.getFileId(m.id); 831 r.alive[fid.get] ~= m; 832 833 if (fid.get !in r.files) { 834 r.files[fid.get] = Path.init; 835 r.files[fid.get] = db.getFile(fid.get).get; 836 } 837 } 838 839 return r; 840 } 841 842 /// High interest mutants. 843 class MutantSample { 844 import dextool.plugin.mutate.backend.database : FileId, MutantInfo, 845 MutationStatus, MutationEntry, MutationStatusTime; 846 847 MutationEntry[MutationStatusId] mutants; 848 849 /// The mutant that had its status updated the furthest back in time. 850 //MutationStatusTime[] oldest; 851 852 /// The mutant that has survived the longest in the system. 853 MutationStatus[] highestPrio; 854 855 /// The latest mutants that where added and survived. 856 MutationStatusTime[] latest; 857 } 858 859 /// Returns: samples of mutants that are of high interest to the user. 860 MutantSample reportSelectedAliveMutants(ref Database db, const(Mutation.Kind)[] kinds, 861 long historyNr) { 862 auto profile = Profile(ReportSection.mut_recommend_kill); 863 864 auto rval = new typeof(return); 865 866 rval.highestPrio = db.mutantApi.getHighestPrioMutant(kinds, Mutation.Status.alive, historyNr); 867 foreach (const mutst; rval.highestPrio) { 868 auto ids = db.mutantApi.getMutationIds(kinds, [mutst.statusId]); 869 if (ids.length != 0) 870 rval.mutants[mutst.statusId] = db.mutantApi.getMutation(ids[0]).get; 871 } 872 873 //rval.oldest = db.mutantApi.getOldestMutants(kinds, historyNr, [EnumMembers!(Mutation.Status)].filter!(a => a != Mutation.Status.noCoverage).array); 874 //foreach (const mutst; rval.oldest) { 875 // auto ids = db.mutantApi.getMutationIds(kinds, [mutst.id]); 876 // if (ids.length != 0) 877 // rval.mutants[mutst.id] = db.mutantApi.getMutation(ids[0]).get; 878 //} 879 880 return rval; 881 } 882 883 class DiffReport { 884 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 885 import dextool.plugin.mutate.backend.diff_parser : Diff; 886 887 /// The mutation score. 888 double score = 0.0; 889 890 /// The raw diff for a file 891 Diff.Line[][FileId] rawDiff; 892 893 /// Lookup for converting a id to a filename 894 Path[FileId] files; 895 /// Mutants alive in a file. 896 MutantInfo[][FileId] alive; 897 /// Mutants killed in a file. 898 MutantInfo[][FileId] killed; 899 /// Test cases that killed mutants. 900 TestCase[] testCases; 901 902 override string toString() @safe const { 903 import std.format : formattedWrite; 904 import std.range : put; 905 906 auto w = appender!string; 907 908 foreach (file; files.byKeyValue) { 909 put(w, file.value.toString); 910 foreach (mut; alive[file.key]) 911 formattedWrite(w, " %s\n", mut); 912 foreach (mut; killed[file.key]) 913 formattedWrite(w, " %s\n", mut); 914 } 915 916 formattedWrite(w, "Test Cases killing mutants"); 917 foreach (tc; testCases) 918 formattedWrite(w, " %s", tc); 919 920 return w.data; 921 } 922 } 923 924 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds, 925 ref Diff diff, AbsolutePath workdir) { 926 import dextool.plugin.mutate.backend.type : SourceLoc; 927 928 auto profile = Profile(ReportSection.diff); 929 930 auto rval = new DiffReport; 931 932 Set!MutationStatusId total; 933 Set!MutationId alive; 934 Set!MutationId killed; 935 936 foreach (kv; diff.toRange(workdir)) { 937 auto fid = db.getFileId(kv.key); 938 if (fid.isNull) { 939 logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key); 940 continue; 941 } 942 943 bool hasMutants; 944 foreach (id; kv.value 945 .toRange 946 .map!(line => spinSql!(() => db.mutantApi.getMutationsOnLine(kinds, 947 fid.get, SourceLoc(line)))) 948 .joiner 949 .filter!(a => a !in total)) { 950 hasMutants = true; 951 total.add(id); 952 953 const info = db.mutantApi.getMutantsInfo(kinds, [id])[0]; 954 if (info.status == Mutation.Status.alive) { 955 rval.alive[fid.get] ~= info; 956 alive.add(info.id); 957 } else if (info.status.among(Mutation.Status.killed, Mutation.Status.timeout)) { 958 rval.killed[fid.get] ~= info; 959 killed.add(info.id); 960 } 961 } 962 963 if (hasMutants) { 964 rval.files[fid.get] = kv.key; 965 rval.rawDiff[fid.get] = diff.rawDiff[kv.key]; 966 } else { 967 logger.info("This file in the diff has no mutants on changed lines: ", kv.key); 968 } 969 } 970 971 Set!TestCase test_cases; 972 foreach (tc; killed.toRange.map!(a => db.testCaseApi.getTestCases(a)).joiner) { 973 test_cases.add(tc); 974 } 975 976 rval.testCases = test_cases.toArray.sort.array; 977 978 if (total.length == 0) { 979 rval.score = 1.0; 980 } else { 981 // TODO: use total to compute e.g. a standard deviation or some other 982 // useful statistical metric to convey a "confidence" of the value. 983 rval.score = cast(double) killed.length / cast(double)(killed.length + alive.length); 984 } 985 986 return rval; 987 } 988 989 struct MinimalTestSet { 990 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 991 992 long total; 993 994 /// Minimal set that achieve the mutation test score. 995 TestCase[] minimalSet; 996 /// Test cases that do not contribute to the mutation test score. 997 TestCase[] redundant; 998 /// Map between test case name and sum of all the test time of the mutants it killed. 999 TestCaseInfo[string] testCaseTime; 1000 } 1001 1002 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) { 1003 import dextool.plugin.mutate.backend.database : TestCaseInfo; 1004 1005 auto profile = Profile(ReportSection.tc_min_set); 1006 1007 alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info"); 1008 1009 MinimalTestSet rval; 1010 1011 Set!MutationId killedMutants; 1012 1013 // start by picking test cases that have the fewest kills. 1014 foreach (const val; db.testCaseApi 1015 .getDetectedTestCases 1016 .map!(a => tuple(a, db.testCaseApi.getTestCaseId(a))) 1017 .filter!(a => !a[1].isNull) 1018 .map!(a => TcIdInfo(a[0], a[1].get, db.testCaseApi.getTestCaseInfo(a[0], kinds).get)) 1019 .filter!(a => a.info.killedMutants != 0) 1020 .array 1021 .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) { 1022 rval.testCaseTime[val.tc.name] = val.info; 1023 1024 const killed = killedMutants.length; 1025 foreach (const id; db.testCaseApi.getTestCaseMutantKills(val.id, kinds)) { 1026 killedMutants.add(id); 1027 } 1028 1029 if (killedMutants.length > killed) 1030 rval.minimalSet ~= val.tc; 1031 else 1032 rval.redundant ~= val.tc; 1033 } 1034 1035 rval.total = rval.minimalSet.length + rval.redundant.length; 1036 1037 return rval; 1038 } 1039 1040 struct TestCaseUniqueness { 1041 MutationStatusId[][TestCaseId] uniqueKills; 1042 1043 // test cases that have no unique kills. These are candidates for being 1044 // refactored/removed. 1045 Set!TestCaseId noUniqueKills; 1046 } 1047 1048 /// Returns: a report of the mutants that a test case is the only one that kills. 1049 TestCaseUniqueness reportTestCaseUniqueness(ref Database db, const Mutation.Kind[] kinds) { 1050 import dextool.plugin.mutate.backend.database.type : MutationStatusId; 1051 1052 auto profile = Profile(ReportSection.tc_unique); 1053 1054 // any time a mutant is killed by more than one test case it is removed. 1055 TestCaseId[MutationStatusId] killedBy; 1056 // killed by multiple test cases 1057 Set!MutationStatusId multiKill; 1058 1059 foreach (tc_id; db.testCaseApi.getTestCasesWithAtLeastOneKill(kinds)) { 1060 auto muts = db.testCaseApi.testCaseKilledSrcMutants(kinds, tc_id); 1061 foreach (m; muts.filter!(a => a !in multiKill)) { 1062 if (m in killedBy) { 1063 killedBy.remove(m); 1064 multiKill.add(m); 1065 } else { 1066 killedBy[m] = tc_id; 1067 } 1068 } 1069 } 1070 1071 typeof(return) rval; 1072 Set!TestCaseId uniqueTc; 1073 foreach (kv; killedBy.byKeyValue) { 1074 rval.uniqueKills[kv.value] ~= kv.key; 1075 uniqueTc.add(kv.value); 1076 } 1077 foreach (tc_id; db.testCaseApi.getDetectedTestCaseIds.filter!(a => !uniqueTc.contains(a))) 1078 rval.noUniqueKills.add(tc_id); 1079 1080 return rval; 1081 } 1082 1083 /// Estimate the mutation score. 1084 struct EstimateMutationScore { 1085 import my.signal_theory.kalman : KalmanFilter; 1086 1087 private KalmanFilter kf; 1088 1089 void update(const double a) { 1090 kf.updateEstimate(a); 1091 } 1092 1093 /// The estimated mutation score. 1094 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value() @safe pure nothrow const @nogc { 1095 return typeof(return)(kf.currentEstimate); 1096 } 1097 1098 /// The error in the estimate. The unit is the same as `estimate`. 1099 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error() @safe pure nothrow const @nogc { 1100 return typeof(return)(kf.estimateError); 1101 } 1102 } 1103 1104 /// Estimate the mutation score. 1105 struct EstimateScore { 1106 import my.signal_theory.kalman : KalmanFilter; 1107 1108 // 0.5 because then it starts in the middle of range possible values. 1109 // 0.01 such that the trend is "slowly" changing over the last 100 mutants. 1110 // 0.001 is to "insensitive" for an on the fly analysis so it mostly just 1111 // end up being the current mutation score. 1112 private EstimateMutationScore estimate = EstimateMutationScore(KalmanFilter(0.5, 0.5, 0.01)); 1113 1114 /// Update the estimate with the status of a mutant. 1115 void update(const Mutation.Status s) { 1116 import std.algorithm : among; 1117 1118 if (s.among(Mutation.Status.unknown, Mutation.Status.killedByCompiler)) { 1119 return; 1120 } 1121 1122 const v = () { 1123 final switch (s) with (Mutation.Status) { 1124 case unknown: 1125 goto case; 1126 case killedByCompiler: 1127 return 0.5; // shouldnt happen but... 1128 case skipped: 1129 goto case; 1130 case noCoverage: 1131 goto case; 1132 case alive: 1133 return 0.0; 1134 case killed: 1135 goto case; 1136 case timeout: 1137 goto case; 1138 case memOverload: 1139 goto case; 1140 case equivalent: 1141 return 1.0; 1142 } 1143 }(); 1144 1145 estimate.update(v); 1146 } 1147 1148 /// The estimated mutation score. 1149 auto value() @safe pure nothrow const @nogc { 1150 return estimate.value; 1151 } 1152 1153 /// The error in the estimate. The unit is the same as `estimate`. 1154 auto error() @safe pure nothrow const @nogc { 1155 return estimate.error; 1156 } 1157 } 1158 1159 /// Estimated trend based on the latest code changes. 1160 struct ScoreTrendByCodeChange { 1161 static struct Point { 1162 SysTime timeStamp; 1163 1164 /// The estimated mutation score. 1165 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value; 1166 1167 /// The error in the estimate. The unit is the same as `estimate`. 1168 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error; 1169 } 1170 1171 Point[] sample; 1172 1173 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value() @safe pure nothrow const @nogc { 1174 if (sample.empty) 1175 return typeof(return).init; 1176 return sample[$ - 1].value; 1177 } 1178 1179 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error() @safe pure nothrow const @nogc { 1180 if (sample.empty) 1181 return typeof(return).init; 1182 return sample[$ - 1].error; 1183 } 1184 } 1185 1186 /** Estimate the mutation score by running a kalman filter over the mutants in 1187 * the order they have been tested. It gives a rough estimate of where the test 1188 * suites quality is going over time. 1189 * 1190 */ 1191 ScoreTrendByCodeChange reportTrendByCodeChange(ref Database db, const Mutation.Kind[] kinds) @trusted nothrow { 1192 auto app = appender!(ScoreTrendByCodeChange.Point[])(); 1193 EstimateScore estimate; 1194 1195 try { 1196 SysTime lastAdded; 1197 SysTime last; 1198 bool first = true; 1199 void fn(const Mutation.Status s, const SysTime added) { 1200 estimate.update(s); 1201 debug logger.trace(estimate.estimate.kf).collectException; 1202 1203 if (first) 1204 lastAdded = added; 1205 1206 if (added != lastAdded) { 1207 app.put(ScoreTrendByCodeChange.Point(added, estimate.value, estimate.error)); 1208 lastAdded = added; 1209 } 1210 1211 last = added; 1212 first = false; 1213 } 1214 1215 db.iterateMutantStatus(kinds, &fn); 1216 app.put(ScoreTrendByCodeChange.Point(last, estimate.value, estimate.error)); 1217 } catch (Exception e) { 1218 logger.warning(e.msg).collectException; 1219 } 1220 return ScoreTrendByCodeChange(app.data); 1221 } 1222 1223 /** History of how the mutation score have evolved over time. 1224 * 1225 * The history is ordered in ascending by date. Each day is the average of the 1226 * recorded mutation score. 1227 */ 1228 struct MutationScoreHistory { 1229 import dextool.plugin.mutate.backend.database.type : MutationScore; 1230 1231 enum Trend { 1232 undecided, 1233 negative, 1234 positive 1235 } 1236 1237 static struct Estimate { 1238 SysTime x; 1239 double avg = 0; 1240 SysTime predX; 1241 double predScore = 0; 1242 Trend trend; 1243 } 1244 1245 /// only one score for each date. 1246 MutationScore[] data; 1247 Estimate estimate; 1248 1249 this(MutationScore[] data) { 1250 import std.algorithm : sum, map, min; 1251 1252 this.data = data; 1253 if (data.length < 6) 1254 return; 1255 1256 const values = data[$ - 5 .. $]; 1257 { 1258 const avg = sum(values.map!(a => a.score.get)) / 5.0; 1259 const xDiff = values[$ - 1].timeStamp - values[0].timeStamp; 1260 const dy = (values[$ - 1].score.get - avg) / (xDiff.total!"days" / 2.0); 1261 1262 estimate.x = values[0].timeStamp + xDiff / 2; 1263 estimate.avg = avg; 1264 estimate.predX = values[$ - 1].timeStamp + xDiff / 2; 1265 estimate.predScore = min(1.0, dy * xDiff.total!"days" / 2.0 + values[$ - 1].score.get); 1266 } 1267 1268 { 1269 // small changes / fluctuations are ignored 1270 immutable limit = 0.001; 1271 const diff = estimate.predScore - values[$ - 1].score.get; 1272 if (diff < -limit) 1273 estimate.trend = Trend.negative; 1274 else if (diff > limit) 1275 estimate.trend = Trend.positive; 1276 } 1277 } 1278 1279 const(MutationScoreHistory) rollingAvg() @safe const { 1280 immutable avgDays = 7; 1281 if (data.length < avgDays) 1282 return this; 1283 1284 auto app = appender!(MutationScore[])(); 1285 foreach (i; 0 .. data.length - avgDays) 1286 app.put(MutationScore(data[i + avgDays].timeStamp, 1287 typeof(MutationScore.score)(data[i .. i + avgDays].map!(a => a.score.get) 1288 .sum / cast(double) avgDays))); 1289 return MutationScoreHistory(app.data); 1290 } 1291 } 1292 1293 MutationScoreHistory reportMutationScoreHistory(ref Database db) @safe { 1294 return reportMutationScoreHistory(db.getMutationScoreHistory); 1295 } 1296 1297 private MutationScoreHistory reportMutationScoreHistory( 1298 dextool.plugin.mutate.backend.database.type.MutationScore[] data) { 1299 import std.datetime : DateTime, Date, SysTime; 1300 import dextool.plugin.mutate.backend.database.type : MutationScore; 1301 1302 auto pretty = appender!(MutationScore[])(); 1303 1304 if (data.length < 2) { 1305 return MutationScoreHistory(data); 1306 } 1307 1308 auto last = (cast(DateTime) data[0].timeStamp).date; 1309 double acc = data[0].score.get; 1310 double nr = 1; 1311 foreach (a; data[1 .. $]) { 1312 auto curr = (cast(DateTime) a.timeStamp).date; 1313 if (curr == last) { 1314 acc += a.score.get; 1315 nr++; 1316 } else { 1317 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1318 last = curr; 1319 acc = a.score.get; 1320 nr = 1; 1321 } 1322 } 1323 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1324 1325 return MutationScoreHistory(pretty.data); 1326 } 1327 1328 @("shall calculate the mean of the mutation scores") 1329 unittest { 1330 import core.time : days; 1331 import std.datetime : DateTime; 1332 import dextool.plugin.mutate.backend.database.type : MutationScore; 1333 1334 auto data = appender!(MutationScore[])(); 1335 auto d = DateTime(2000, 6, 1, 10, 30, 0); 1336 1337 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(10.0))); 1338 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(5.0))); 1339 data.put(MutationScore(SysTime(d + 1.days), typeof(MutationScore.score)(5.0))); 1340 1341 auto res = reportMutationScoreHistory(data.data); 1342 1343 res.data[0].score.get.shouldEqual(7.5); 1344 res.data[1].score.get.shouldEqual(5.0); 1345 } 1346 1347 /** Sync status is how old the information about mutants and their status is 1348 * compared to when the tests or source code where last changed. 1349 */ 1350 struct SyncStatus { 1351 import dextool.plugin.mutate.backend.database : MutationStatusTime; 1352 1353 SysTime test; 1354 SysTime code; 1355 SysTime coverage; 1356 MutationStatusTime[] mutants; 1357 } 1358 1359 SyncStatus reportSyncStatus(ref Database db, const(Mutation.Kind)[] kinds, const long nrMutants) { 1360 import std.datetime : Clock; 1361 import std.traits : EnumMembers; 1362 import dextool.plugin.mutate.backend.database : TestFile, TestFileChecksum, TestFilePath; 1363 1364 typeof(return) rval; 1365 rval.test = spinSql!(() => db.testFileApi.getNewestTestFile) 1366 .orElse(TestFile(TestFilePath.init, TestFileChecksum.init, Clock.currTime)).timeStamp; 1367 rval.code = spinSql!(() => db.getNewestFile).orElse(Clock.currTime); 1368 rval.coverage = spinSql!(() => db.coverageApi.getCoverageTimeStamp).orElse(Clock.currTime); 1369 rval.mutants = spinSql!(() => db.mutantApi.getOldestMutants(kinds, 1370 nrMutants, 1371 [EnumMembers!(Mutation.Status)].filter!(a => a != Mutation.Status.noCoverage).array)); 1372 return rval; 1373 } 1374 1375 struct TestCaseClassifier { 1376 long threshold; 1377 } 1378 1379 TestCaseClassifier makeTestCaseClassifier(ref Database db, const long minThreshold) { 1380 import std.algorithm : maxElement, max, minElement; 1381 import std.datetime : dur; 1382 import std.math : abs; 1383 import dextool.plugin.mutate.backend.report.kmean; 1384 1385 auto profile = Profile("test case classifier"); 1386 1387 // the distribution is bimodal (U shaped) with one or more tops depending 1388 // on the architecture. The left most edge is the leaf functionality and 1389 // the rest of the edges are the main data flows. 1390 // 1391 // Even though the formula below assume a normal distribution and, 1392 // obviously, this isn't one the result is totally fine because the purpuse 1393 // is to classify "bad" test cases by checking if all mutants that they 1394 // kill are above the threshold. The threshold, as calculcated, thus 1395 // centers around the mean and moves further to the right the further the 1396 // edges are. It also, suitably, handle multiple edges because the only 1397 // important factor is to not get "too close" to the left most edge. That 1398 // would lead to false classifications. 1399 1400 auto tcKills = db.mutantApi 1401 .getAllTestCaseKills 1402 .filter!"a>0" 1403 .map!(a => Point(cast(double) a)) 1404 .array; 1405 // no use in a classifier if there are too mutants. 1406 if (tcKills.length < 100) 1407 return TestCaseClassifier(minThreshold); 1408 1409 // 0.1 is good enough because it is then rounded. 1410 auto iter = KmeanIterator!Point(0.1); 1411 iter.clusters ~= Cluster!Point(0); 1412 // div by 2 reduces the number of iterations for a typical sample. 1413 iter.clusters ~= Cluster!Point(cast(double) tcKills.map!(a => a.value).maxElement / 2.0); 1414 1415 iter.fit(tcKills, 1000, 10.dur!"seconds"); 1416 1417 TestCaseClassifier rval; 1418 rval.threshold = 1 + cast(long)( 1419 iter.clusters.map!"a.mean".minElement + abs( 1420 iter.clusters[0].mean - iter.clusters[1].mean) / 2.0); 1421 1422 logger.tracef("calculated threshold: %s iterations:%s time:%s cluster.mean: %s", 1423 rval.threshold, iter.iterations, iter.time, iter.clusters.map!(a => a.mean)); 1424 rval.threshold = max(rval.threshold, minThreshold); 1425 1426 return rval; 1427 } 1428 1429 struct TestCaseMetadata { 1430 static struct Location { 1431 string file; 1432 Optional!uint line; 1433 } 1434 1435 string[TestCase] text; 1436 Location[TestCase] loc; 1437 1438 /// If the user has manually marked a test case as redundant or not. 1439 bool[TestCase] redundant; 1440 } 1441 1442 TestCaseMetadata parseTestCaseMetadata(AbsolutePath metadataPath) @trusted { 1443 import std.json; 1444 import std.file : readText; 1445 1446 TestCaseMetadata rval; 1447 JSONValue jraw; 1448 try { 1449 jraw = parseJSON(readText(metadataPath.toString)); 1450 } catch (Exception e) { 1451 logger.warning("Error reading ", metadataPath); 1452 logger.info(e.msg); 1453 return rval; 1454 } 1455 1456 try { 1457 foreach (jtc; jraw.array) { 1458 TestCase tc; 1459 1460 try { 1461 if (auto v = "name" in jtc) { 1462 tc = TestCase(v.str); 1463 } else { 1464 logger.warning("Missing `name` in ", jtc.toPrettyString); 1465 continue; 1466 } 1467 1468 if (auto v = "text" in jtc) 1469 rval.text[tc] = v.str; 1470 if (auto v = "location" in jtc) { 1471 TestCaseMetadata.Location loc; 1472 if (auto f = "file" in *v) 1473 loc.file = f.str; 1474 if (auto l = "line" in *v) 1475 loc.line = some(cast(uint) l.integer); 1476 rval.loc[tc] = loc; 1477 } 1478 1479 if (auto v = "redundant" in jtc) 1480 rval.redundant[tc] = v.boolean; 1481 } catch (Exception e) { 1482 logger.warning("Error parsing ", jtc.toPrettyString); 1483 logger.warning(e.msg); 1484 } 1485 } 1486 } catch (Exception e) { 1487 logger.warning("Error parsing ", jraw.toPrettyString); 1488 logger.warning(e.msg); 1489 } 1490 1491 return rval; 1492 } 1493 1494 alias AverageTimePerMutant = NamedType!(Duration, Tag!"AverageTimePerMutant", 1495 Duration.init, TagStringable, ImplicitConvertable); 1496 1497 /// Based on the last 100 tested mutants. 1498 AverageTimePerMutant calcAvgPerMutant(ref Database db, const Mutation.Kind[] kinds) nothrow { 1499 import core.time : dur; 1500 1501 auto times = spinSql!(() => db.mutantApi.getLatestMutantTimes(kinds, 100)); 1502 if (times.length == 0) 1503 return AverageTimePerMutant.init; 1504 1505 const avg = (times.map!(a => a.compileTime) 1506 .sum 1507 .total!"msecs" + times.map!(a => a.testTime) 1508 .sum 1509 .total!"msecs") / times.length; 1510 return avg.dur!"msecs".AverageTimePerMutant; 1511 }