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; 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 MutantTimeProfile totalTime; 339 340 // Nr of mutants that are alive but tagged with nomut. 341 long aliveNoMut; 342 343 double score() @safe pure nothrow const @nogc { 344 if (total > 0) { 345 return cast(double)(killed + timeout) / cast(double)(total - aliveNoMut); 346 } 347 return 0.0; 348 } 349 } 350 351 MutationScore reportScore(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow { 352 auto profile = Profile("reportScore"); 353 354 typeof(return) rval; 355 rval.alive = spinSql!(() => db.mutantApi.aliveSrcMutants(kinds, file)).count; 356 rval.killed = spinSql!(() => db.mutantApi.killedSrcMutants(kinds, file)).count; 357 rval.timeout = spinSql!(() => db.mutantApi.timeoutSrcMutants(kinds, file)).count; 358 rval.aliveNoMut = spinSql!(() => db.mutantApi.aliveNoMutSrcMutants(kinds, file)).count; 359 rval.noCoverage = spinSql!(() => db.mutantApi.noCovSrcMutants(kinds, file)).count; 360 rval.equivalent = spinSql!(() => db.mutantApi.equivalentMutants(kinds, file)).count; 361 rval.skipped = spinSql!(() => db.mutantApi.skippedMutants(kinds, file)).count; 362 363 const total = spinSql!(() => db.mutantApi.totalSrcMutants(kinds, file)); 364 rval.totalTime = total.time; 365 rval.total = total.count; 366 367 return rval; 368 } 369 370 /// Statistics for a group of mutants. 371 struct MutationStat { 372 import core.time : Duration; 373 import std.range : isOutputRange; 374 375 long untested; 376 long killedByCompiler; 377 long worklist; 378 379 long alive() @safe pure nothrow const @nogc { 380 return scoreData.alive; 381 } 382 383 long noCoverage() @safe pure nothrow const @nogc { 384 return scoreData.noCoverage; 385 } 386 387 /// Nr of mutants that are alive but tagged with nomut. 388 long aliveNoMut() @safe pure nothrow const @nogc { 389 return scoreData.aliveNoMut; 390 } 391 392 long killed() @safe pure nothrow const @nogc { 393 return scoreData.killed; 394 } 395 396 long timeout() @safe pure nothrow const @nogc { 397 return scoreData.timeout; 398 } 399 400 long equivalent() @safe pure nothrow const @nogc { 401 return scoreData.equivalent; 402 } 403 404 long skipped() @safe pure nothrow const @nogc { 405 return scoreData.skipped; 406 } 407 408 long total() @safe pure nothrow const @nogc { 409 return scoreData.total; 410 } 411 412 MutantTimeProfile totalTime() @safe pure nothrow const @nogc { 413 return scoreData.totalTime; 414 } 415 416 MutationScore scoreData; 417 MutantTimeProfile killedByCompilerTime; 418 Duration predictedDone; 419 420 /// Adjust the score with the alive mutants that are suppressed. 421 double score() @safe pure nothrow const @nogc { 422 return scoreData.score; 423 } 424 425 /// Suppressed mutants of the total mutants. 426 double suppressedOfTotal() @safe pure nothrow const @nogc { 427 if (total > 0) { 428 return (cast(double)(aliveNoMut) / cast(double) total); 429 } 430 return 0.0; 431 } 432 433 string toString() @safe const { 434 auto buf = appender!string; 435 toString(buf); 436 return buf.data; 437 } 438 439 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 440 import core.time : dur; 441 import std.ascii : newline; 442 import std.datetime : Clock; 443 import std.format : formattedWrite; 444 import std.range : put; 445 import dextool.plugin.mutate.backend.utility; 446 447 immutable align_ = 19; 448 449 formattedWrite(w, "%-*s %s\n", align_, "Time spent:", totalTime); 450 if (untested > 0 && predictedDone > 0.dur!"msecs") { 451 const pred = Clock.currTime + predictedDone; 452 formattedWrite(w, "Remaining: %s (%s)\n", predictedDone, pred.toISOExtString); 453 } 454 if (killedByCompiler > 0) { 455 formattedWrite(w, "%-*s %s\n", align_ * 3, 456 "Time spent on mutants killed by compiler:", killedByCompilerTime); 457 } 458 459 put(w, newline); 460 461 // mutation score and details 462 formattedWrite(w, "%-*s %.3s\n", align_, "Score:", score); 463 464 formattedWrite(w, "%-*s %s\n", align_, "Total:", total); 465 if (untested > 0) { 466 formattedWrite(w, "%-*s %s\n", align_, "Untested:", untested); 467 } 468 formattedWrite(w, "%-*s %s\n", align_, "Alive:", alive); 469 formattedWrite(w, "%-*s %s\n", align_, "Killed:", killed); 470 if (skipped > 0) 471 formattedWrite(w, "%-*s %s\n", align_, "Skipped:", skipped); 472 if (equivalent > 0) 473 formattedWrite(w, "%-*s %s\n", align_, "Equivalent:", equivalent); 474 formattedWrite(w, "%-*s %s\n", align_, "Timeout:", timeout); 475 formattedWrite(w, "%-*s %s\n", align_, "Killed by compiler:", killedByCompiler); 476 if (worklist > 0) { 477 formattedWrite(w, "%-*s %s\n", align_, "Worklist:", worklist); 478 } 479 480 if (aliveNoMut > 0) { 481 formattedWrite(w, "%-*s %s (%.3s)\n", align_, 482 "Suppressed (nomut):", aliveNoMut, suppressedOfTotal); 483 } 484 } 485 } 486 487 MutationStat reportStatistics(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow { 488 import core.time : dur; 489 import dextool.plugin.mutate.backend.utility; 490 491 auto profile = Profile(ReportSection.summary); 492 493 const untested = spinSql!(() => db.mutantApi.unknownSrcMutants(kinds, file)); 494 const worklist = spinSql!(() => db.worklistApi.getWorklistCount); 495 const killedByCompiler = spinSql!(() => db.mutantApi.killedByCompilerSrcMutants(kinds, file)); 496 497 MutationStat st; 498 st.scoreData = reportScore(db, kinds, file); 499 st.untested = untested.count; 500 st.killedByCompiler = killedByCompiler.count; 501 st.worklist = worklist; 502 503 st.predictedDone = st.total > 0 ? (st.worklist * (st.totalTime.sum / st.total)) : 0 504 .dur!"msecs"; 505 st.killedByCompilerTime = killedByCompiler.time; 506 507 return st; 508 } 509 510 struct MarkedMutantsStat { 511 Table!6 tbl; 512 } 513 514 MarkedMutantsStat reportMarkedMutants(ref Database db, const Mutation.Kind[] kinds, 515 string file = null) @safe { 516 MarkedMutantsStat st; 517 st.tbl.heading = [ 518 "File", "Line", "Column", "Mutation", "Status", "Rationale" 519 ]; 520 521 foreach (m; db.markMutantApi.getMarkedMutants()) { 522 typeof(st.tbl).Row r = [ 523 m.path, m.sloc.line.to!string, m.sloc.column.to!string, 524 m.mutText, statusToString(m.toStatus), m.rationale.get 525 ]; 526 st.tbl.put(r); 527 } 528 return st; 529 } 530 531 struct TestCaseOverlapStat { 532 import std.format : formattedWrite; 533 import std.range : put; 534 import my.hash; 535 536 long overlap; 537 long total; 538 double ratio = 0.0; 539 540 // map between test cases and the mutants they have killed. 541 TestCaseId[][Murmur3] tc_mut; 542 // map between mutation IDs and the test cases that killed them. 543 long[][Murmur3] mutid_mut; 544 string[TestCaseId] name_tc; 545 546 string sumToString() @safe const { 547 return format("%s/%s = %s test cases", overlap, total, ratio); 548 } 549 550 void sumToString(Writer)(ref Writer w) @trusted const { 551 formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio); 552 } 553 554 string toString() @safe const { 555 auto buf = appender!string; 556 toString(buf); 557 return buf.data; 558 } 559 560 void toString(Writer)(ref Writer w) @safe const { 561 sumToString(w); 562 563 foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 564 bool first = true; 565 // TODO this is a bit slow. use a DB row iterator instead. 566 foreach (name; tcs.value.map!(id => name_tc[id])) { 567 if (first) { 568 () @trusted { 569 formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length); 570 }(); 571 first = false; 572 } else { 573 () @trusted { formattedWrite(w, "%s\n", name); }(); 574 } 575 } 576 put(w, "\n"); 577 } 578 } 579 } 580 581 /** Report test cases that completly overlap each other. 582 * 583 * Returns: a string with statistics. 584 */ 585 template toTable(Flag!"colWithMutants" colMutants) { 586 static if (colMutants) { 587 alias TableT = Table!3; 588 } else { 589 alias TableT = Table!2; 590 } 591 alias RowT = TableT.Row; 592 593 void toTable(ref TestCaseOverlapStat st, ref TableT tbl) { 594 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 595 bool first = true; 596 // TODO this is a bit slow. use a DB row iterator instead. 597 foreach (name; tcs.value.map!(id => st.name_tc[id])) { 598 RowT r; 599 r[0] = name; 600 if (first) { 601 auto muts = st.mutid_mut[tcs.key]; 602 r[1] = muts.length.to!string; 603 static if (colMutants) { 604 r[2] = format("%-(%s,%)", muts); 605 } 606 first = false; 607 } 608 609 tbl.put(r); 610 } 611 static if (colMutants) 612 RowT r = ["", "", ""]; 613 else 614 RowT r = ["", ""]; 615 tbl.put(r); 616 } 617 } 618 } 619 620 /// Test cases that kill exactly the same mutants. 621 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe { 622 import my.hash; 623 624 auto profile = Profile(ReportSection.tc_full_overlap); 625 626 TestCaseOverlapStat st; 627 st.total = db.testCaseApi.getNumOfTestCases; 628 629 foreach (tc_id; db.testCaseApi.getTestCasesWithAtLeastOneKill(kinds)) { 630 auto muts = db.testCaseApi.getTestCaseMutantKills(tc_id, kinds) 631 .sort.map!(a => cast(long) a).array; 632 auto m3 = makeMurmur3(cast(ubyte[]) muts); 633 if (auto v = m3 in st.tc_mut) 634 (*v) ~= tc_id; 635 else { 636 st.tc_mut[m3] = [tc_id]; 637 st.mutid_mut[m3] = muts; 638 } 639 st.name_tc[tc_id] = db.testCaseApi.getTestCaseName(tc_id); 640 } 641 642 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 643 st.overlap += tcs.value.count; 644 } 645 646 if (st.total > 0) 647 st.ratio = cast(double) st.overlap / cast(double) st.total; 648 649 return st; 650 } 651 652 class TestGroupSimilarity { 653 static struct TestGroup { 654 string description; 655 string name; 656 657 /// What the user configured as regex. Useful when e.g. generating reports 658 /// for a user. 659 string userInput; 660 661 int opCmp(ref const TestGroup s) const { 662 return cmp(name, s.name); 663 } 664 } 665 666 static struct Similarity { 667 /// The test group that the `key` is compared to. 668 TestGroup comparedTo; 669 /// How similare the `key` is to `comparedTo`. 670 double similarity = 0.0; 671 /// Mutants that are similare between `testCase` and the parent. 672 MutationStatusId[] intersection; 673 /// Unique mutants that are NOT verified by `testCase`. 674 MutationStatusId[] difference; 675 } 676 677 Similarity[][TestGroup] similarities; 678 } 679 680 /** Analyze the similarity between the test groups. 681 * 682 * Assuming that a limit on how many test groups to report isn't interesting 683 * because they are few so it is never a problem. 684 * 685 */ 686 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db, 687 const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe { 688 auto profile = Profile(ReportSection.tc_groups_similarity); 689 690 alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", 691 MutationStatusId[], "kills"); 692 693 const test_cases = spinSql!(() { 694 return db.testCaseApi.getDetectedTestCaseIds; 695 }).map!(a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() { 696 return db.testCaseApi.getTestCase(a).get; 697 }))).array; 698 699 MutationStatusId[] gatherKilledMutants(const(TestGroup) tg) { 700 auto kills = appender!(MutationStatusId[])(); 701 foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) { 702 kills.put(spinSql!(() { 703 return db.testCaseApi.testCaseKilledSrcMutants(kinds, tc.id); 704 })); 705 } 706 return kills.data; 707 } 708 709 TgKills[] test_group_kills; 710 foreach (const tg; test_groups) { 711 auto kills = gatherKilledMutants(tg); 712 if (kills.length != 0) 713 test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description, 714 tg.name, tg.userInput), kills); 715 } 716 717 // calculate similarity between all test groups. 718 auto rval = new typeof(return); 719 720 foreach (tg_parent; test_group_kills) { 721 auto app = appender!(TestGroupSimilarity.Similarity[])(); 722 foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) { 723 auto similarity = setSimilarity(tg_parent.kills, tg_other.kills); 724 if (similarity.similarity > 0) 725 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup, 726 similarity.similarity, similarity.intersection, similarity.difference)); 727 if (app.data.length != 0) 728 rval.similarities[tg_parent.testGroup] = app.data; 729 } 730 } 731 732 return rval; 733 } 734 735 class TestGroupStat { 736 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 737 738 /// Human readable description for the test group. 739 string description; 740 /// Statistics for a test group. 741 MutationStat stats; 742 /// Map between test cases and their test group. 743 TestCase[] testCases; 744 /// Lookup for converting a id to a filename 745 Path[FileId] files; 746 /// Mutants alive in a file. 747 MutantInfo[][FileId] alive; 748 /// Mutants killed in a file. 749 MutantInfo[][FileId] killed; 750 } 751 752 import std.regex : Regex; 753 754 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) { 755 import std.regex : matchFirst; 756 757 auto m = matchFirst(tc.name, tg); 758 // the regex must match the full test case thus checking that 759 // nothing is left before or after 760 if (!m.empty && m.pre.length == 0 && m.post.length == 0) { 761 return true; 762 } 763 return false; 764 } 765 766 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds, 767 const(TestGroup) test_g) @safe { 768 auto profile = Profile(ReportSection.tc_groups); 769 770 static struct TcStat { 771 Set!MutationStatusId alive; 772 Set!MutationStatusId killed; 773 Set!MutationStatusId timeout; 774 Set!MutationStatusId total; 775 776 // killed by the specific test case 777 Set!MutationStatusId tcKilled; 778 } 779 780 auto r = new TestGroupStat; 781 r.description = test_g.description; 782 TcStat tc_stat; 783 784 // map test cases to this test group 785 foreach (tc; db.testCaseApi.getDetectedTestCases) { 786 if (tc.isTestCaseInTestGroup(test_g.re)) 787 r.testCases ~= tc; 788 } 789 790 // collect mutation statistics for each test case group 791 foreach (const tc; r.testCases) { 792 foreach (const id; db.testCaseApi.testCaseMutationPointAliveSrcMutants(kinds, tc)) 793 tc_stat.alive.add(id); 794 foreach (const id; db.testCaseApi.testCaseMutationPointKilledSrcMutants(kinds, tc)) 795 tc_stat.killed.add(id); 796 foreach (const id; db.testCaseApi.testCaseMutationPointTimeoutSrcMutants(kinds, tc)) 797 tc_stat.timeout.add(id); 798 foreach (const id; db.testCaseApi.testCaseMutationPointTotalSrcMutants(kinds, tc)) 799 tc_stat.total.add(id); 800 foreach (const id; db.testCaseApi.testCaseKilledSrcMutants(kinds, tc)) 801 tc_stat.tcKilled.add(id); 802 } 803 804 // update the mutation stat for the test group 805 r.stats.scoreData.alive = tc_stat.alive.length; 806 r.stats.scoreData.killed = tc_stat.killed.length; 807 r.stats.scoreData.timeout = tc_stat.timeout.length; 808 r.stats.scoreData.total = tc_stat.total.length; 809 810 // associate mutants with their file 811 foreach (const m; db.mutantApi.getMutantsInfo(kinds, tc_stat.tcKilled.toArray)) { 812 auto fid = db.getFileId(m.id); 813 r.killed[fid.get] ~= m; 814 815 if (fid.get !in r.files) { 816 r.files[fid.get] = Path.init; 817 r.files[fid.get] = db.getFile(fid.get).get; 818 } 819 } 820 821 foreach (const m; db.mutantApi.getMutantsInfo(kinds, tc_stat.alive.toArray)) { 822 auto fid = db.getFileId(m.id); 823 r.alive[fid.get] ~= m; 824 825 if (fid.get !in r.files) { 826 r.files[fid.get] = Path.init; 827 r.files[fid.get] = db.getFile(fid.get).get; 828 } 829 } 830 831 return r; 832 } 833 834 /// High interest mutants. 835 class MutantSample { 836 import dextool.plugin.mutate.backend.database : FileId, MutantInfo, 837 MutationStatus, MutationEntry, MutationStatusTime; 838 839 MutationEntry[MutationStatusId] mutants; 840 841 /// The mutant that had its status updated the furthest back in time. 842 MutationStatusTime[] oldest; 843 844 /// The mutant that has survived the longest in the system. 845 MutationStatus[] highestPrio; 846 847 /// The latest mutants that where added and survived. 848 MutationStatusTime[] latest; 849 } 850 851 /// Returns: samples of mutants that are of high interest to the user. 852 MutantSample reportSelectedAliveMutants(ref Database db, const(Mutation.Kind)[] kinds, 853 long historyNr) { 854 auto profile = Profile(ReportSection.mut_recommend_kill); 855 856 auto rval = new typeof(return); 857 858 rval.highestPrio = db.mutantApi.getHighestPrioMutant(kinds, Mutation.Status.alive, historyNr); 859 foreach (const mutst; rval.highestPrio) { 860 auto ids = db.mutantApi.getMutationIds(kinds, [mutst.statusId]); 861 if (ids.length != 0) 862 rval.mutants[mutst.statusId] = db.mutantApi.getMutation(ids[0]).get; 863 } 864 865 rval.oldest = db.mutantApi.getOldestMutants(kinds, historyNr); 866 foreach (const mutst; rval.oldest) { 867 auto ids = db.mutantApi.getMutationIds(kinds, [mutst.id]); 868 if (ids.length != 0) 869 rval.mutants[mutst.id] = db.mutantApi.getMutation(ids[0]).get; 870 } 871 872 return rval; 873 } 874 875 class DiffReport { 876 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 877 import dextool.plugin.mutate.backend.diff_parser : Diff; 878 879 /// The mutation score. 880 double score = 0.0; 881 882 /// The raw diff for a file 883 Diff.Line[][FileId] rawDiff; 884 885 /// Lookup for converting a id to a filename 886 Path[FileId] files; 887 /// Mutants alive in a file. 888 MutantInfo[][FileId] alive; 889 /// Mutants killed in a file. 890 MutantInfo[][FileId] killed; 891 /// Test cases that killed mutants. 892 TestCase[] testCases; 893 894 override string toString() @safe const { 895 import std.format : formattedWrite; 896 import std.range : put; 897 898 auto w = appender!string; 899 900 foreach (file; files.byKeyValue) { 901 put(w, file.value.toString); 902 foreach (mut; alive[file.key]) 903 formattedWrite(w, " %s\n", mut); 904 foreach (mut; killed[file.key]) 905 formattedWrite(w, " %s\n", mut); 906 } 907 908 formattedWrite(w, "Test Cases killing mutants"); 909 foreach (tc; testCases) 910 formattedWrite(w, " %s", tc); 911 912 return w.data; 913 } 914 } 915 916 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds, 917 ref Diff diff, AbsolutePath workdir) { 918 import dextool.plugin.mutate.backend.type : SourceLoc; 919 920 auto profile = Profile(ReportSection.diff); 921 922 auto rval = new DiffReport; 923 924 Set!MutationStatusId total; 925 Set!MutationId alive; 926 Set!MutationId killed; 927 928 foreach (kv; diff.toRange(workdir)) { 929 auto fid = db.getFileId(kv.key); 930 if (fid.isNull) { 931 logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key); 932 continue; 933 } 934 935 bool hasMutants; 936 foreach (id; kv.value 937 .toRange 938 .map!(line => spinSql!(() => db.mutantApi.getMutationsOnLine(kinds, 939 fid.get, SourceLoc(line)))) 940 .joiner 941 .filter!(a => a !in total)) { 942 hasMutants = true; 943 total.add(id); 944 945 const info = db.mutantApi.getMutantsInfo(kinds, [id])[0]; 946 if (info.status == Mutation.Status.alive) { 947 rval.alive[fid.get] ~= info; 948 alive.add(info.id); 949 } else if (info.status.among(Mutation.Status.killed, Mutation.Status.timeout)) { 950 rval.killed[fid.get] ~= info; 951 killed.add(info.id); 952 } 953 } 954 955 if (hasMutants) { 956 rval.files[fid.get] = kv.key; 957 rval.rawDiff[fid.get] = diff.rawDiff[kv.key]; 958 } else { 959 logger.info("This file in the diff has no mutants on changed lines: ", kv.key); 960 } 961 } 962 963 Set!TestCase test_cases; 964 foreach (tc; killed.toRange.map!(a => db.testCaseApi.getTestCases(a)).joiner) { 965 test_cases.add(tc); 966 } 967 968 rval.testCases = test_cases.toArray.sort.array; 969 970 if (total.length == 0) { 971 rval.score = 1.0; 972 } else { 973 // TODO: use total to compute e.g. a standard deviation or some other 974 // useful statistical metric to convey a "confidence" of the value. 975 rval.score = cast(double) killed.length / cast(double)(killed.length + alive.length); 976 } 977 978 return rval; 979 } 980 981 struct MinimalTestSet { 982 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 983 984 long total; 985 986 /// Minimal set that achieve the mutation test score. 987 TestCase[] minimalSet; 988 /// Test cases that do not contribute to the mutation test score. 989 TestCase[] redundant; 990 /// Map between test case name and sum of all the test time of the mutants it killed. 991 TestCaseInfo[string] testCaseTime; 992 } 993 994 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) { 995 import dextool.plugin.mutate.backend.database : TestCaseInfo; 996 997 auto profile = Profile(ReportSection.tc_min_set); 998 999 alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info"); 1000 1001 MinimalTestSet rval; 1002 1003 Set!MutationId killedMutants; 1004 1005 // start by picking test cases that have the fewest kills. 1006 foreach (const val; db.testCaseApi 1007 .getDetectedTestCases 1008 .map!(a => tuple(a, db.testCaseApi.getTestCaseId(a))) 1009 .filter!(a => !a[1].isNull) 1010 .map!(a => TcIdInfo(a[0], a[1].get, db.testCaseApi.getTestCaseInfo(a[0], kinds).get)) 1011 .filter!(a => a.info.killedMutants != 0) 1012 .array 1013 .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) { 1014 rval.testCaseTime[val.tc.name] = val.info; 1015 1016 const killed = killedMutants.length; 1017 foreach (const id; db.testCaseApi.getTestCaseMutantKills(val.id, kinds)) { 1018 killedMutants.add(id); 1019 } 1020 1021 if (killedMutants.length > killed) 1022 rval.minimalSet ~= val.tc; 1023 else 1024 rval.redundant ~= val.tc; 1025 } 1026 1027 rval.total = rval.minimalSet.length + rval.redundant.length; 1028 1029 return rval; 1030 } 1031 1032 struct TestCaseUniqueness { 1033 MutationStatusId[][TestCaseId] uniqueKills; 1034 1035 // test cases that have no unique kills. These are candidates for being 1036 // refactored/removed. 1037 Set!TestCaseId noUniqueKills; 1038 } 1039 1040 /// Returns: a report of the mutants that a test case is the only one that kills. 1041 TestCaseUniqueness reportTestCaseUniqueness(ref Database db, const Mutation.Kind[] kinds) { 1042 import dextool.plugin.mutate.backend.database.type : MutationStatusId; 1043 1044 auto profile = Profile(ReportSection.tc_unique); 1045 1046 // any time a mutant is killed by more than one test case it is removed. 1047 TestCaseId[MutationStatusId] killedBy; 1048 // killed by multiple test cases 1049 Set!MutationStatusId multiKill; 1050 1051 foreach (tc_id; db.testCaseApi.getTestCasesWithAtLeastOneKill(kinds)) { 1052 auto muts = db.testCaseApi.testCaseKilledSrcMutants(kinds, tc_id); 1053 foreach (m; muts.filter!(a => a !in multiKill)) { 1054 if (m in killedBy) { 1055 killedBy.remove(m); 1056 multiKill.add(m); 1057 } else { 1058 killedBy[m] = tc_id; 1059 } 1060 } 1061 } 1062 1063 typeof(return) rval; 1064 Set!TestCaseId uniqueTc; 1065 foreach (kv; killedBy.byKeyValue) { 1066 rval.uniqueKills[kv.value] ~= kv.key; 1067 uniqueTc.add(kv.value); 1068 } 1069 foreach (tc_id; db.testCaseApi.getDetectedTestCaseIds.filter!(a => !uniqueTc.contains(a))) 1070 rval.noUniqueKills.add(tc_id); 1071 1072 return rval; 1073 } 1074 1075 /// Estimate the mutation score. 1076 struct EstimateMutationScore { 1077 import my.signal_theory.kalman : KalmanFilter; 1078 1079 private KalmanFilter kf; 1080 1081 void update(const double a) { 1082 kf.updateEstimate(a); 1083 } 1084 1085 /// The estimated mutation score. 1086 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value() @safe pure nothrow const @nogc { 1087 return typeof(return)(kf.currentEstimate); 1088 } 1089 1090 /// The error in the estimate. The unit is the same as `estimate`. 1091 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error() @safe pure nothrow const @nogc { 1092 return typeof(return)(kf.estimateError); 1093 } 1094 } 1095 1096 /// Estimate the mutation score. 1097 struct EstimateScore { 1098 import my.signal_theory.kalman : KalmanFilter; 1099 1100 // 0.5 because then it starts in the middle of range possible values. 1101 // 0.01 such that the trend is "slowly" changing over the last 100 mutants. 1102 // 0.001 is to "insensitive" for an on the fly analysis so it mostly just 1103 // end up being the current mutation score. 1104 private EstimateMutationScore estimate = EstimateMutationScore(KalmanFilter(0.5, 0.5, 0.01)); 1105 1106 /// Update the estimate with the status of a mutant. 1107 void update(const Mutation.Status s) { 1108 import std.algorithm : among; 1109 1110 if (s.among(Mutation.Status.unknown, Mutation.Status.killedByCompiler)) { 1111 return; 1112 } 1113 1114 const v = () { 1115 final switch (s) with (Mutation.Status) { 1116 case unknown: 1117 goto case; 1118 case killedByCompiler: 1119 return 0.5; // shouldnt happen but... 1120 case skipped: 1121 goto case; 1122 case noCoverage: 1123 goto case; 1124 case alive: 1125 return 0.0; 1126 case killed: 1127 goto case; 1128 case timeout: 1129 goto case; 1130 case equivalent: 1131 return 1.0; 1132 } 1133 }(); 1134 1135 estimate.update(v); 1136 } 1137 1138 /// The estimated mutation score. 1139 auto value() @safe pure nothrow const @nogc { 1140 return estimate.value; 1141 } 1142 1143 /// The error in the estimate. The unit is the same as `estimate`. 1144 auto error() @safe pure nothrow const @nogc { 1145 return estimate.error; 1146 } 1147 } 1148 1149 /// Estimated trend based on the latest code changes. 1150 struct ScoreTrendByCodeChange { 1151 static struct Point { 1152 SysTime timeStamp; 1153 1154 /// The estimated mutation score. 1155 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value; 1156 1157 /// The error in the estimate. The unit is the same as `estimate`. 1158 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error; 1159 } 1160 1161 Point[] sample; 1162 1163 NamedType!(double, Tag!"EstimatedMutationScore", 0.0, TagStringable) value() @safe pure nothrow const @nogc { 1164 if (sample.empty) 1165 return typeof(return).init; 1166 return sample[$ - 1].value; 1167 } 1168 1169 NamedType!(double, Tag!"MutationScoreError", 0.0, TagStringable) error() @safe pure nothrow const @nogc { 1170 if (sample.empty) 1171 return typeof(return).init; 1172 return sample[$ - 1].error; 1173 } 1174 } 1175 1176 /** Estimate the mutation score by running a kalman filter over the mutants in 1177 * the order they have been tested. It gives a rough estimate of where the test 1178 * suites quality is going over time. 1179 * 1180 */ 1181 ScoreTrendByCodeChange reportTrendByCodeChange(ref Database db, const Mutation.Kind[] kinds) @trusted nothrow { 1182 auto app = appender!(ScoreTrendByCodeChange.Point[])(); 1183 EstimateScore estimate; 1184 1185 try { 1186 SysTime lastAdded; 1187 SysTime last; 1188 bool first = true; 1189 void fn(const Mutation.Status s, const SysTime added) { 1190 estimate.update(s); 1191 debug logger.trace(estimate.estimate.kf).collectException; 1192 1193 if (first) 1194 lastAdded = added; 1195 1196 if (added != lastAdded) { 1197 app.put(ScoreTrendByCodeChange.Point(added, estimate.value, estimate.error)); 1198 lastAdded = added; 1199 } 1200 1201 last = added; 1202 first = false; 1203 } 1204 1205 db.iterateMutantStatus(kinds, &fn); 1206 app.put(ScoreTrendByCodeChange.Point(last, estimate.value, estimate.error)); 1207 } catch (Exception e) { 1208 logger.warning(e.msg).collectException; 1209 } 1210 return ScoreTrendByCodeChange(app.data); 1211 } 1212 1213 /** History of how the mutation score have evolved over time. 1214 * 1215 * The history is ordered iascending by date. Each day is the average of the 1216 * recorded mutation score. 1217 */ 1218 struct MutationScoreHistory { 1219 import dextool.plugin.mutate.backend.database.type : MutationScore; 1220 1221 static struct Estimate { 1222 SysTime x; 1223 double avg = 0; 1224 SysTime predX; 1225 double predScore = 0; 1226 bool posTrend = 0; 1227 } 1228 1229 /// only one score for each date. 1230 MutationScore[] data; 1231 Estimate estimate; 1232 1233 this(MutationScore[] data) { 1234 import std.algorithm : sum, map, min; 1235 1236 this.data = data; 1237 if (data.length < 6) 1238 return; 1239 1240 const values = data[$ - 5 .. $]; 1241 const avg = sum(values.map!(a => a.score.get)) / 5.0; 1242 const xDiff = values[$ - 1].timeStamp - values[0].timeStamp; 1243 const dy = (values[$ - 1].score.get - avg) / (xDiff.total!"days" / 2.0); 1244 1245 estimate.x = values[0].timeStamp + xDiff / 2; 1246 estimate.avg = avg; 1247 estimate.predX = values[$ - 1].timeStamp + xDiff / 2; 1248 estimate.predScore = min(1.0, dy * xDiff.total!"days" / 2.0 + values[$ - 1].score.get); 1249 estimate.posTrend = estimate.predScore > values[$ - 1].score.get; 1250 } 1251 } 1252 1253 MutationScoreHistory reportMutationScoreHistory(ref Database db) @safe { 1254 return reportMutationScoreHistory(db.getMutationScoreHistory); 1255 } 1256 1257 private MutationScoreHistory reportMutationScoreHistory( 1258 dextool.plugin.mutate.backend.database.type.MutationScore[] data) { 1259 import std.datetime : DateTime, Date, SysTime; 1260 import dextool.plugin.mutate.backend.database.type : MutationScore; 1261 1262 auto pretty = appender!(MutationScore[])(); 1263 1264 if (data.length < 2) { 1265 return MutationScoreHistory(data); 1266 } 1267 1268 auto last = (cast(DateTime) data[0].timeStamp).date; 1269 double acc = data[0].score.get; 1270 double nr = 1; 1271 foreach (a; data[1 .. $]) { 1272 auto curr = (cast(DateTime) a.timeStamp).date; 1273 if (curr == last) { 1274 acc += a.score.get; 1275 nr++; 1276 } else { 1277 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1278 last = curr; 1279 acc = a.score.get; 1280 nr = 1; 1281 } 1282 } 1283 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1284 1285 return MutationScoreHistory(pretty.data); 1286 } 1287 1288 @("shall calculate the mean of the mutation scores") 1289 unittest { 1290 import core.time : days; 1291 import std.datetime : DateTime; 1292 import dextool.plugin.mutate.backend.database.type : MutationScore; 1293 1294 auto data = appender!(MutationScore[])(); 1295 auto d = DateTime(2000, 6, 1, 10, 30, 0); 1296 1297 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(10.0))); 1298 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(5.0))); 1299 data.put(MutationScore(SysTime(d + 1.days), typeof(MutationScore.score)(5.0))); 1300 1301 auto res = reportMutationScoreHistory(data.data); 1302 1303 res.data[0].score.get.shouldEqual(7.5); 1304 res.data[1].score.get.shouldEqual(5.0); 1305 } 1306 1307 /** Sync status is how old the information about mutants and their status is 1308 * compared to when the tests or source code where last changed. 1309 */ 1310 struct SyncStatus { 1311 import dextool.plugin.mutate.backend.database : MutationStatusTime; 1312 1313 SysTime test; 1314 SysTime code; 1315 SysTime coverage; 1316 MutationStatusTime[] mutants; 1317 } 1318 1319 SyncStatus reportSyncStatus(ref Database db, const(Mutation.Kind)[] kinds, const long nrMutants) { 1320 import std.datetime : Clock; 1321 import dextool.plugin.mutate.backend.database : TestFile, TestFileChecksum, TestFilePath; 1322 1323 typeof(return) rval; 1324 rval.test = spinSql!(() => db.testFileApi.getNewestTestFile) 1325 .orElse(TestFile(TestFilePath.init, TestFileChecksum.init, Clock.currTime)).timeStamp; 1326 rval.code = spinSql!(() => db.getNewestFile).orElse(Clock.currTime); 1327 rval.coverage = spinSql!(() => db.coverageApi.getCoverageTimeStamp).orElse(Clock.currTime); 1328 rval.mutants = spinSql!(() => db.mutantApi.getOldestMutants(kinds, nrMutants)); 1329 return rval; 1330 } 1331 1332 struct TestCaseClassifier { 1333 long threshold; 1334 } 1335 1336 TestCaseClassifier makeTestCaseClassifier(ref Database db, const long minThreshold) { 1337 import std.algorithm : maxElement, max, minElement; 1338 import std.datetime : dur; 1339 import std.math : abs; 1340 import dextool.plugin.mutate.backend.report.kmean; 1341 1342 auto profile = Profile("test case classifier"); 1343 1344 // the distribution is bimodal (U shaped) with one or more tops depending 1345 // on the architecture. The left most edge is the leaf functionality and 1346 // the rest of the edges are the main data flows. 1347 // 1348 // Even though the formula below assume a normal distribution and, 1349 // obviously, this isn't one the result is totally fine because the purpuse 1350 // is to classify "bad" test cases by checking if all mutants that they 1351 // kill are above the threshold. The threshold, as calculcated, thus 1352 // centers around the mean and moves further to the right the further the 1353 // edges are. It also, suitably, handle multiple edges because the only 1354 // important factor is to not get "too close" to the left most edge. That 1355 // would lead to false classifications. 1356 1357 auto tcKills = db.mutantApi 1358 .getAllTestCaseKills 1359 .filter!"a>0" 1360 .map!(a => Point(cast(double) a)) 1361 .array; 1362 // no use in a classifier if there are too mutants. 1363 if (tcKills.length < 100) 1364 return TestCaseClassifier(minThreshold); 1365 1366 // 0.1 is good enough because it is then rounded. 1367 auto iter = KmeanIterator!Point(0.1); 1368 iter.clusters ~= Cluster!Point(0); 1369 // div by 2 reduces the number of iterations for a typical sample. 1370 iter.clusters ~= Cluster!Point(cast(double) tcKills.map!(a => a.value).maxElement / 2.0); 1371 1372 iter.fit(tcKills, 1000, 10.dur!"seconds"); 1373 1374 TestCaseClassifier rval; 1375 rval.threshold = 1 + cast(long)( 1376 iter.clusters.map!"a.mean".minElement + abs( 1377 iter.clusters[0].mean - iter.clusters[1].mean) / 2.0); 1378 1379 logger.tracef("calculated threshold: %s iterations:%s time:%s cluster.mean: %s", 1380 rval.threshold, iter.iterations, iter.time, iter.clusters.map!(a => a.mean)); 1381 rval.threshold = max(rval.threshold, minThreshold); 1382 1383 return rval; 1384 } 1385 1386 struct TestCaseMetadata { 1387 static struct Location { 1388 string file; 1389 Optional!uint line; 1390 } 1391 1392 string[TestCase] text; 1393 Location[TestCase] loc; 1394 1395 /// If the user has manually marked a test case as redundant or not. 1396 bool[TestCase] redundant; 1397 } 1398 1399 TestCaseMetadata parseTestCaseMetadata(AbsolutePath metadataPath) @trusted { 1400 import std.json; 1401 import std.file : readText; 1402 1403 TestCaseMetadata rval; 1404 JSONValue jraw; 1405 try { 1406 jraw = parseJSON(readText(metadataPath.toString)); 1407 } catch (Exception e) { 1408 logger.warning("Error reading ", metadataPath); 1409 logger.info(e.msg); 1410 return rval; 1411 } 1412 1413 try { 1414 foreach (jtc; jraw.array) { 1415 TestCase tc; 1416 1417 try { 1418 if (auto v = "name" in jtc) { 1419 tc = TestCase(v.str); 1420 } else { 1421 logger.warning("Missing `name` in ", jtc.toPrettyString); 1422 continue; 1423 } 1424 1425 if (auto v = "text" in jtc) 1426 rval.text[tc] = v.str; 1427 if (auto v = "location" in jtc) { 1428 TestCaseMetadata.Location loc; 1429 if (auto f = "file" in *v) 1430 loc.file = f.str; 1431 if (auto l = "line" in *v) 1432 loc.line = some(cast(uint) l.integer); 1433 rval.loc[tc] = loc; 1434 } 1435 1436 if (auto v = "redundant" in jtc) 1437 rval.redundant[tc] = v.boolean; 1438 } catch (Exception e) { 1439 logger.warning("Error parsing ", jtc.toPrettyString); 1440 logger.warning(e.msg); 1441 } 1442 } 1443 } catch (Exception e) { 1444 logger.warning("Error parsing ", jraw.toPrettyString); 1445 logger.warning(e.msg); 1446 } 1447 1448 return rval; 1449 }