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 ignoreFluctuations, 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) @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().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); 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 * limit = limit the number of test cases to the top `limit`. 219 */ 220 TestCaseSimilarityAnalyse reportTestCaseSimilarityAnalyse(ref Database db, ulong limit) @safe { 221 import std.container.binaryheap; 222 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 223 224 auto profile = Profile(ReportSection.tc_similarity); 225 226 // TODO: reduce the code duplication of the caches. 227 // The DB lookups must be cached or otherwise the algorithm becomes too 228 // slow for practical use. 229 230 MutationStatusId[][TestCaseId] kill_cache2; 231 MutationStatusId[] getKills(TestCaseId id) @trusted { 232 return kill_cache2.require(id, spinSql!(() { 233 return db.testCaseApi.testCaseKilledSrcMutants(id); 234 })); 235 } 236 237 alias TcKills = Tuple!(TestCaseId, "id", MutationStatusId[], "kills"); 238 239 const test_cases = spinSql!(() { 240 return db.testCaseApi.getDetectedTestCaseIds; 241 }); 242 243 auto rval = new typeof(return); 244 245 foreach (tc_kill; test_cases.map!(a => TcKills(a, getKills(a))) 246 .filter!(a => a.kills.length != 0)) { 247 auto app = appender!(TestCaseSimilarityAnalyse.Similarity[])(); 248 foreach (tc; test_cases.filter!(a => a != tc_kill.id) 249 .map!(a => TcKills(a, getKills(a))) 250 .filter!(a => a.kills.length != 0)) { 251 auto distance = setSimilarity(tc_kill.kills, tc.kills); 252 if (distance.similarity > 0) 253 app.put(TestCaseSimilarityAnalyse.Similarity(tc.id, 254 distance.similarity, distance.intersection, distance.difference)); 255 } 256 if (app.data.length != 0) { 257 () @trusted { 258 rval.similarities[tc_kill.id] = heapify!((a, 259 b) => a.similarity < b.similarity)(app.data).take(limit).array; 260 }(); 261 } 262 } 263 264 return rval; 265 } 266 267 /// Statistics about dead test cases. 268 struct TestCaseDeadStat { 269 import std.range : isOutputRange; 270 271 /// The ratio of dead TC of the total. 272 double ratio = 0.0; 273 TestCase[] testCases; 274 long total; 275 276 long numDeadTC() @safe pure nothrow const @nogc scope { 277 return testCases.length; 278 } 279 280 string toString() @safe const { 281 auto buf = appender!string; 282 toString(buf); 283 return buf.data; 284 } 285 286 void toString(Writer)(ref Writer w) @safe const 287 if (isOutputRange!(Writer, char)) { 288 import std.ascii : newline; 289 import std.format : formattedWrite; 290 import std.range : put; 291 292 if (total > 0) 293 formattedWrite(w, "%s/%s = %s of all test cases\n", numDeadTC, total, ratio); 294 foreach (tc; testCases) { 295 put(w, tc.name); 296 if (tc.location.length > 0) { 297 put(w, " | "); 298 put(w, tc.location); 299 } 300 put(w, newline); 301 } 302 } 303 } 304 305 void toTable(ref TestCaseDeadStat st, ref Table!2 tbl) @safe pure nothrow { 306 foreach (tc; st.testCases) { 307 typeof(tbl).Row r = [tc.name, tc.location]; 308 tbl.put(r); 309 } 310 } 311 312 /** Returns: report of test cases that has killed zero mutants. 313 */ 314 TestCaseDeadStat reportDeadTestCases(ref Database db) @safe { 315 auto profile = Profile(ReportSection.tc_killed_no_mutants); 316 317 TestCaseDeadStat r; 318 r.total = db.testCaseApi.getNumOfTestCases; 319 r.testCases = db.testCaseApi.getTestCasesWithZeroKills; 320 if (r.total > 0) 321 r.ratio = cast(double) r.numDeadTC / cast(double) r.total; 322 return r; 323 } 324 325 /// Only the mutation score thus a subset of all statistics. 326 struct MutationScore { 327 import core.time : Duration; 328 329 long alive; 330 long killed; 331 long timeout; 332 long total; 333 long noCoverage; 334 long equivalent; 335 long skipped; 336 long memOverload; 337 MutantTimeProfile totalTime; 338 339 // Nr of mutants that are alive but tagged with nomut. 340 long aliveNoMut; 341 342 double score() @safe pure nothrow const @nogc { 343 if ((total - aliveNoMut) > 0) { 344 return cast(double)(killed + timeout + memOverload) / cast(double)(total - aliveNoMut); 345 } 346 return 0.0; 347 } 348 } 349 350 MutationScore reportScore(ref Database db, string file = null) @safe nothrow { 351 auto profile = Profile("reportScore"); 352 353 typeof(return) rval; 354 rval.alive = spinSql!(() => db.mutantApi.aliveSrcMutants(file)).count; 355 rval.killed = spinSql!(() => db.mutantApi.killedSrcMutants(file)).count; 356 rval.timeout = spinSql!(() => db.mutantApi.timeoutSrcMutants(file)).count; 357 rval.aliveNoMut = spinSql!(() => db.mutantApi.aliveNoMutSrcMutants(file)).count; 358 rval.noCoverage = spinSql!(() => db.mutantApi.noCovSrcMutants(file)).count; 359 rval.equivalent = spinSql!(() => db.mutantApi.equivalentMutants(file)).count; 360 rval.skipped = spinSql!(() => db.mutantApi.skippedMutants(file)).count; 361 rval.memOverload = spinSql!(() => db.mutantApi.memOverloadMutants(file)).count; 362 363 const total = spinSql!(() => db.mutantApi.totalSrcMutants(file)); 364 rval.totalTime = total.time; 365 rval.total = total.count; 366 367 return rval; 368 } 369 370 struct FileScore { 371 double score; 372 Path file; 373 bool hasMutants; 374 } 375 376 FileScore[] reportScores(ref Database db, Path[] files) @safe nothrow { 377 auto profile = Profile("reportScores"); 378 auto app = appender!(FileScore[]); 379 380 foreach (file; files) { 381 const res = reportScore(db, file.toString); 382 auto result = FileScore(res.score(), file, res.total > 0); 383 app.put(result); 384 } 385 386 return app.data; 387 } 388 389 /// Statistics for a group of mutants. 390 struct MutationStat { 391 import core.time : Duration; 392 import std.range : isOutputRange; 393 394 long untested; 395 long killedByCompiler; 396 long worklist; 397 398 long alive() @safe pure nothrow const @nogc { 399 return scoreData.alive; 400 } 401 402 long noCoverage() @safe pure nothrow const @nogc { 403 return scoreData.noCoverage; 404 } 405 406 /// Nr of mutants that are alive but tagged with nomut. 407 long aliveNoMut() @safe pure nothrow const @nogc { 408 return scoreData.aliveNoMut; 409 } 410 411 long killed() @safe pure nothrow const @nogc { 412 return scoreData.killed; 413 } 414 415 long timeout() @safe pure nothrow const @nogc { 416 return scoreData.timeout; 417 } 418 419 long equivalent() @safe pure nothrow const @nogc { 420 return scoreData.equivalent; 421 } 422 423 long skipped() @safe pure nothrow const @nogc { 424 return scoreData.skipped; 425 } 426 427 long memOverload() @safe pure nothrow const @nogc { 428 return scoreData.memOverload; 429 } 430 431 long total() @safe pure nothrow const @nogc { 432 return scoreData.total; 433 } 434 435 MutantTimeProfile totalTime() @safe pure nothrow const @nogc { 436 return scoreData.totalTime; 437 } 438 439 MutationScore scoreData; 440 MutantTimeProfile killedByCompilerTime; 441 Duration predictedDone; 442 443 /// Adjust the score with the alive mutants that are suppressed. 444 double score() @safe pure nothrow const @nogc { 445 return scoreData.score; 446 } 447 448 /// Suppressed mutants of the total mutants. 449 double suppressedOfTotal() @safe pure nothrow const @nogc { 450 if (total > 0) { 451 return (cast(double)(aliveNoMut) / cast(double) total); 452 } 453 return 0.0; 454 } 455 456 string toString() @safe const { 457 auto buf = appender!string; 458 toString(buf); 459 return buf.data; 460 } 461 462 void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) { 463 import core.time : dur; 464 import std.ascii : newline; 465 import std.datetime : Clock; 466 import std.format : formattedWrite; 467 import std.range : put; 468 import dextool.plugin.mutate.backend.utility; 469 470 immutable align_ = 19; 471 472 formattedWrite(w, "%-*s %s\n", align_, "Time spent:", totalTime); 473 if (untested > 0 && predictedDone > 0.dur!"msecs") { 474 const pred = Clock.currTime + predictedDone; 475 formattedWrite(w, "Remaining: %s (%s)\n", predictedDone, pred.toISOExtString); 476 } 477 if (killedByCompiler > 0) { 478 formattedWrite(w, "%-*s %s\n", align_ * 3, 479 "Time spent on mutants killed by compiler:", killedByCompilerTime); 480 } 481 482 put(w, newline); 483 484 // mutation score and details 485 formattedWrite(w, "%-*s %.3s\n", align_, "Score:", score); 486 487 formattedWrite(w, "%-*s %s\n", align_, "Total:", total); 488 if (untested > 0) { 489 formattedWrite(w, "%-*s %s\n", align_, "Untested:", untested); 490 } 491 formattedWrite(w, "%-*s %s\n", align_, "Alive:", alive); 492 formattedWrite(w, "%-*s %s\n", align_, "Killed:", killed); 493 if (skipped > 0) 494 formattedWrite(w, "%-*s %s\n", align_, "Skipped:", skipped); 495 if (equivalent > 0) 496 formattedWrite(w, "%-*s %s\n", align_, "Equivalent:", equivalent); 497 formattedWrite(w, "%-*s %s\n", align_, "Timeout:", timeout); 498 formattedWrite(w, "%-*s %s\n", align_, "Killed by compiler:", killedByCompiler); 499 if (worklist > 0) { 500 formattedWrite(w, "%-*s %s\n", align_, "Worklist:", worklist); 501 } 502 503 if (aliveNoMut > 0) { 504 formattedWrite(w, "%-*s %s (%.3s)\n", align_, 505 "Suppressed (nomut):", aliveNoMut, suppressedOfTotal); 506 } 507 } 508 } 509 510 MutationStat reportStatistics(ref Database db, string file = null) @safe nothrow { 511 import core.time : dur; 512 import dextool.plugin.mutate.backend.utility; 513 514 auto profile = Profile(ReportSection.summary); 515 516 const untested = spinSql!(() => db.mutantApi.unknownSrcMutants(file)); 517 const worklist = spinSql!(() => db.worklistApi.getCount); 518 const killedByCompiler = spinSql!(() => db.mutantApi.killedByCompilerSrcMutants(file)); 519 520 MutationStat st; 521 st.scoreData = reportScore(db, file); 522 st.untested = untested.count; 523 st.killedByCompiler = killedByCompiler.count; 524 st.worklist = worklist; 525 526 st.predictedDone = () { 527 auto avg = calcAvgPerMutant(db); 528 return (st.worklist * avg.total!"msecs").dur!"msecs"; 529 }(); 530 st.killedByCompilerTime = killedByCompiler.time; 531 532 return st; 533 } 534 535 struct MarkedMutantsStat { 536 Table!6 tbl; 537 } 538 539 MarkedMutantsStat reportMarkedMutants(ref Database db, string file = null) @safe { 540 MarkedMutantsStat st; 541 st.tbl.heading = [ 542 "File", "Line", "Column", "Mutation", "Status", "Rationale" 543 ]; 544 545 foreach (m; db.markMutantApi.getMarkedMutants()) { 546 typeof(st.tbl).Row r = [ 547 m.path, m.sloc.line.to!string, m.sloc.column.to!string, 548 m.mutText, statusToString(m.toStatus), m.rationale.get 549 ]; 550 st.tbl.put(r); 551 } 552 return st; 553 } 554 555 struct TestCaseOverlapStat { 556 import std.format : formattedWrite; 557 import std.range : put; 558 import my.hash; 559 560 long overlap; 561 long total; 562 double ratio = 0.0; 563 564 // map between test cases and the mutants they have killed. 565 TestCaseId[][Crc64Iso] tc_mut; 566 // map between mutation IDs and the test cases that killed them. 567 long[][Crc64Iso] mutid_mut; 568 string[TestCaseId] name_tc; 569 570 string sumToString() @safe const { 571 return format("%s/%s = %s test cases", overlap, total, ratio); 572 } 573 574 void sumToString(Writer)(ref Writer w) @trusted const { 575 formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio); 576 } 577 578 string toString() @safe const { 579 auto buf = appender!string; 580 toString(buf); 581 return buf.data; 582 } 583 584 void toString(Writer)(ref Writer w) @safe const { 585 sumToString(w); 586 587 foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 588 bool first = true; 589 // TODO this is a bit slow. use a DB row iterator instead. 590 foreach (name; tcs.value.map!(id => name_tc[id])) { 591 if (first) { 592 () @trusted { 593 formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length); 594 }(); 595 first = false; 596 } else { 597 () @trusted { formattedWrite(w, "%s\n", name); }(); 598 } 599 } 600 put(w, "\n"); 601 } 602 } 603 } 604 605 /** Report test cases that completly overlap each other. 606 * 607 * Returns: a string with statistics. 608 */ 609 template toTable(Flag!"colWithMutants" colMutants) { 610 static if (colMutants) { 611 alias TableT = Table!3; 612 } else { 613 alias TableT = Table!2; 614 } 615 alias RowT = TableT.Row; 616 617 void toTable(ref TestCaseOverlapStat st, ref TableT tbl) { 618 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 619 bool first = true; 620 // TODO this is a bit slow. use a DB row iterator instead. 621 foreach (name; tcs.value.map!(id => st.name_tc[id])) { 622 RowT r; 623 r[0] = name; 624 if (first) { 625 auto muts = st.mutid_mut[tcs.key]; 626 r[1] = muts.length.to!string; 627 static if (colMutants) { 628 r[2] = format("%-(%s,%)", muts); 629 } 630 first = false; 631 } 632 633 tbl.put(r); 634 } 635 static if (colMutants) 636 RowT r = ["", "", ""]; 637 else 638 RowT r = ["", ""]; 639 tbl.put(r); 640 } 641 } 642 } 643 644 /// Test cases that kill exactly the same mutants. 645 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db) @safe { 646 import my.hash; 647 648 auto profile = Profile(ReportSection.tc_full_overlap); 649 650 TestCaseOverlapStat st; 651 st.total = db.testCaseApi.getNumOfTestCases; 652 653 foreach (tc_id; db.testCaseApi.getTestCasesWithAtLeastOneKill) { 654 auto muts = db.testCaseApi.getTestCaseMutantKills(tc_id).sort.map!(a => cast(long) a).array; 655 auto iso = makeCrc64Iso(cast(ubyte[]) muts); 656 if (auto v = iso in st.tc_mut) 657 (*v) ~= tc_id; 658 else { 659 st.tc_mut[iso] = [tc_id]; 660 st.mutid_mut[iso] = muts; 661 } 662 st.name_tc[tc_id] = db.testCaseApi.getTestCaseName(tc_id); 663 } 664 665 foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) { 666 st.overlap += tcs.value.count; 667 } 668 669 if (st.total > 0) 670 st.ratio = cast(double) st.overlap / cast(double) st.total; 671 672 return st; 673 } 674 675 class TestGroupSimilarity { 676 static struct TestGroup { 677 string description; 678 string name; 679 680 /// What the user configured as regex. Useful when e.g. generating reports 681 /// for a user. 682 string userInput; 683 684 int opCmp(ref const TestGroup s) const { 685 return cmp(name, s.name); 686 } 687 } 688 689 static struct Similarity { 690 /// The test group that the `key` is compared to. 691 TestGroup comparedTo; 692 /// How similare the `key` is to `comparedTo`. 693 double similarity = 0.0; 694 /// Mutants that are similare between `testCase` and the parent. 695 MutationStatusId[] intersection; 696 /// Unique mutants that are NOT verified by `testCase`. 697 MutationStatusId[] difference; 698 } 699 700 Similarity[][TestGroup] similarities; 701 } 702 703 /** Analyze the similarity between the test groups. 704 * 705 * Assuming that a limit on how many test groups to report isn't interesting 706 * because they are few so it is never a problem. 707 * 708 */ 709 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db, const(TestGroup)[] test_groups) @safe { 710 auto profile = Profile(ReportSection.tc_groups_similarity); 711 712 alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", 713 MutationStatusId[], "kills"); 714 715 const test_cases = spinSql!(() { 716 return db.testCaseApi.getDetectedTestCaseIds; 717 }).map!(a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() { 718 return db.testCaseApi.getTestCase(a).get; 719 }))).array; 720 721 MutationStatusId[] gatherKilledMutants(const(TestGroup) tg) { 722 auto kills = appender!(MutationStatusId[])(); 723 foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) { 724 kills.put(spinSql!(() { 725 return db.testCaseApi.testCaseKilledSrcMutants(tc.id); 726 })); 727 } 728 return kills.data; 729 } 730 731 TgKills[] test_group_kills; 732 foreach (const tg; test_groups) { 733 auto kills = gatherKilledMutants(tg); 734 if (kills.length != 0) 735 test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description, 736 tg.name, tg.userInput), kills); 737 } 738 739 // calculate similarity between all test groups. 740 auto rval = new typeof(return); 741 742 foreach (tg_parent; test_group_kills) { 743 auto app = appender!(TestGroupSimilarity.Similarity[])(); 744 foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) { 745 auto similarity = setSimilarity(tg_parent.kills, tg_other.kills); 746 if (similarity.similarity > 0) 747 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup, 748 similarity.similarity, similarity.intersection, similarity.difference)); 749 if (app.data.length != 0) 750 rval.similarities[tg_parent.testGroup] = app.data; 751 } 752 } 753 754 return rval; 755 } 756 757 class TestGroupStat { 758 import dextool.plugin.mutate.backend.database : FileId, MutantInfo; 759 760 /// Human readable description for the test group. 761 string description; 762 /// Statistics for a test group. 763 MutationStat stats; 764 /// Map between test cases and their test group. 765 TestCase[] testCases; 766 /// Lookup for converting a id to a filename 767 Path[FileId] files; 768 /// Mutants alive in a file. 769 MutantInfo[][FileId] alive; 770 /// Mutants killed in a file. 771 MutantInfo[][FileId] killed; 772 } 773 774 import std.regex : Regex; 775 776 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) { 777 import std.regex : matchFirst; 778 779 auto m = matchFirst(tc.name, tg); 780 // the regex must match the full test case thus checking that 781 // nothing is left before or after 782 if (!m.empty && m.pre.length == 0 && m.post.length == 0) { 783 return true; 784 } 785 return false; 786 } 787 788 TestGroupStat reportTestGroups(ref Database db, const(TestGroup) test_g) @safe { 789 auto profile = Profile(ReportSection.tc_groups); 790 791 static struct TcStat { 792 Set!MutationStatusId alive; 793 Set!MutationStatusId killed; 794 Set!MutationStatusId timeout; 795 Set!MutationStatusId total; 796 797 // killed by the specific test case 798 Set!MutationStatusId tcKilled; 799 } 800 801 auto r = new TestGroupStat; 802 r.description = test_g.description; 803 TcStat tc_stat; 804 805 // map test cases to this test group 806 foreach (tc; db.testCaseApi.getDetectedTestCases) { 807 if (tc.isTestCaseInTestGroup(test_g.re)) 808 r.testCases ~= tc; 809 } 810 811 // collect mutation statistics for each test case group 812 foreach (const tc; r.testCases) { 813 foreach (const id; db.testCaseApi.testCaseMutationPointAliveSrcMutants(tc)) 814 tc_stat.alive.add(id); 815 foreach (const id; db.testCaseApi.testCaseMutationPointKilledSrcMutants(tc)) 816 tc_stat.killed.add(id); 817 foreach (const id; db.testCaseApi.testCaseMutationPointTimeoutSrcMutants(tc)) 818 tc_stat.timeout.add(id); 819 foreach (const id; db.testCaseApi.testCaseMutationPointTotalSrcMutants(tc)) 820 tc_stat.total.add(id); 821 foreach (const id; db.testCaseApi.testCaseKilledSrcMutants(tc)) 822 tc_stat.tcKilled.add(id); 823 } 824 825 // update the mutation stat for the test group 826 r.stats.scoreData.alive = tc_stat.alive.length; 827 r.stats.scoreData.killed = tc_stat.killed.length; 828 r.stats.scoreData.timeout = tc_stat.timeout.length; 829 r.stats.scoreData.total = tc_stat.total.length; 830 831 // associate mutants with their file 832 foreach (const m; db.mutantApi.getMutantsInfo(tc_stat.tcKilled.toArray)) { 833 auto fid = db.getFileId(m.id); 834 r.killed[fid.get] ~= m; 835 836 if (fid.get !in r.files) { 837 r.files[fid.get] = Path.init; 838 r.files[fid.get] = db.getFile(fid.get).get; 839 } 840 } 841 842 foreach (const m; db.mutantApi.getMutantsInfo(tc_stat.alive.toArray)) { 843 auto fid = db.getFileId(m.id); 844 r.alive[fid.get] ~= m; 845 846 if (fid.get !in r.files) { 847 r.files[fid.get] = Path.init; 848 r.files[fid.get] = db.getFile(fid.get).get; 849 } 850 } 851 852 return r; 853 } 854 855 /// High interest mutants. 856 class MutantSample { 857 import dextool.plugin.mutate.backend.database : FileId, MutantInfo, 858 MutationStatus, MutationEntry, MutationStatusTime; 859 860 MutationEntry[MutationStatusId] mutants; 861 862 /// The mutant that had its status updated the furthest back in time. 863 //MutationStatusTime[] oldest; 864 865 /// The mutant that has survived the longest in the system. 866 MutationStatus[] highestPrio; 867 } 868 869 /// Returns: samples of mutants that are of high interest to the user. 870 MutantSample reportSelectedAliveMutants(ref Database db, long historyNr) { 871 auto profile = Profile(ReportSection.mut_recommend_kill); 872 873 auto rval = new typeof(return); 874 875 rval.highestPrio = db.mutantApi.getHighestPrioMutant(Mutation.Status.alive, historyNr); 876 foreach (const mutst; rval.highestPrio) { 877 rval.mutants[mutst.statusId] = db.mutantApi.getMutation(mutst.statusId).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, ref Diff diff, AbsolutePath workdir) { 925 import dextool.plugin.mutate.backend.type : SourceLoc; 926 927 auto profile = Profile(ReportSection.diff); 928 929 auto rval = new DiffReport; 930 931 Set!MutationStatusId total; 932 Set!MutationStatusId alive; 933 Set!MutationStatusId killed; 934 935 foreach (kv; diff.toRange(workdir)) { 936 auto fid = db.getFileId(kv.key); 937 if (fid.isNull) { 938 logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key); 939 continue; 940 } 941 942 bool hasMutants; 943 foreach (id; kv.value 944 .toRange 945 .map!(line => spinSql!(() => db.mutantApi.getMutationsOnLine(fid.get, 946 SourceLoc(line)))) 947 .joiner 948 .filter!(a => a !in total)) { 949 hasMutants = true; 950 total.add(id); 951 952 const info = db.mutantApi.getMutantsInfo([id])[0]; 953 if (info.status == Mutation.Status.alive) { 954 rval.alive[fid.get] ~= info; 955 alive.add(info.id); 956 } else if (info.status.among(Mutation.Status.killed, Mutation.Status.timeout)) { 957 rval.killed[fid.get] ~= info; 958 killed.add(info.id); 959 } 960 } 961 962 if (hasMutants) { 963 rval.files[fid.get] = kv.key; 964 rval.rawDiff[fid.get] = diff.rawDiff[kv.key]; 965 } else { 966 logger.info("This file in the diff has no mutants on changed lines: ", kv.key); 967 } 968 } 969 970 Set!TestCase test_cases; 971 foreach (tc; killed.toRange.map!(a => db.testCaseApi.getTestCases(a)).joiner) { 972 test_cases.add(tc); 973 } 974 975 rval.testCases = test_cases.toArray.sort.array; 976 977 if (total.length == 0) { 978 rval.score = 1.0; 979 } else { 980 // TODO: use total to compute e.g. a standard deviation or some other 981 // useful statistical metric to convey a "confidence" of the value. 982 rval.score = cast(double) killed.length / cast(double)(killed.length + alive.length); 983 } 984 985 return rval; 986 } 987 988 struct MinimalTestSet { 989 import dextool.plugin.mutate.backend.database.type : TestCaseInfo; 990 991 long total; 992 993 /// Minimal set that achieve the mutation test score. 994 TestCase[] minimalSet; 995 /// Test cases that do not contribute to the mutation test score. 996 TestCase[] redundant; 997 /// Map between test case name and sum of all the test time of the mutants it killed. 998 TestCaseInfo[string] testCaseTime; 999 } 1000 1001 MinimalTestSet reportMinimalSet(ref Database db) { 1002 import dextool.plugin.mutate.backend.database : TestCaseInfo; 1003 1004 auto profile = Profile(ReportSection.tc_min_set); 1005 1006 alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info"); 1007 1008 MinimalTestSet rval; 1009 1010 // TODO: must change to MutationStatusId 1011 Set!MutationStatusId 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]).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)) { 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) { 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) { 1060 auto muts = db.testCaseApi.testCaseKilledSrcMutants(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 /// Trend based on the latest code changes 1160 struct ScoreTrendByCodeChange { 1161 static struct Point { 1162 Path file; 1163 1164 /// The mutation score. 1165 double value; 1166 } 1167 1168 static struct PointGroup { 1169 Point[] points; 1170 1171 double min() @safe pure nothrow const @nogc { 1172 import std.algorithm : minElement; 1173 1174 return points.map!"a.value".minElement; 1175 } 1176 } 1177 1178 PointGroup[SysTime] sample; 1179 1180 bool empty() @safe pure nothrow const @nogc { 1181 return sample.empty; 1182 } 1183 1184 } 1185 1186 /** Report the latest date a file was changed and the score. 1187 * 1188 * Files are grouped by day. 1189 * Files per day are sorted by lowest score first. 1190 */ 1191 ScoreTrendByCodeChange reportTrendByCodeChange(ref Database db) @trusted nothrow { 1192 import dextool.plugin.mutate.backend.database.type : FileScore; 1193 1194 Set!Path lastChangeFound; 1195 FileScore[Path] lastChange; 1196 1197 foreach (a; spinSql!(() => db.fileApi.getFileScoreHistory).array 1198 .sort!((a, b) => a.timeStamp > b.timeStamp) 1199 .filter!(a => a.file !in lastChangeFound)) { 1200 if (auto x = a.file in lastChange) { 1201 if (x.score.get == a.score.get) 1202 lastChange[a.file] = a; 1203 else 1204 lastChangeFound.add(a.file); 1205 } else { 1206 lastChange[a.file] = a; 1207 } 1208 } 1209 1210 typeof(return) rval; 1211 foreach (a; lastChange.byValue) { 1212 rval.sample.update(a.timeStamp, () => ScoreTrendByCodeChange.PointGroup( 1213 [ScoreTrendByCodeChange.Point(a.file, a.score.get)]), 1214 (ref ScoreTrendByCodeChange.PointGroup x) { 1215 x.points ~= ScoreTrendByCodeChange.Point(a.file, a.score.get); 1216 }); 1217 } 1218 1219 foreach (k; rval.sample.byKey) { 1220 rval.sample[k].points = rval.sample[k].points.sort!((a, b) => a.value < b.value).array; 1221 } 1222 1223 return rval; 1224 } 1225 1226 /** History of how the mutation score have evolved over time. 1227 * 1228 * The history is ordered in ascending by date. Each day is the average of the 1229 * recorded mutation score. 1230 */ 1231 struct MutationScoreHistory { 1232 import dextool.plugin.mutate.backend.database.type : MutationScore; 1233 1234 static immutable size_t avgShort = 7; 1235 static immutable size_t avgLong = 30; 1236 1237 /// only one score for each date. 1238 MutationScore[] data; 1239 1240 this(MutationScore[] data) { 1241 this.data = data; 1242 } 1243 1244 const(MutationScoreHistory) rollingAvg(const size_t avgDays) @safe const { 1245 if (data.length < avgDays) 1246 return MutationScoreHistory(null); 1247 1248 auto app = appender!(MutationScore[])(); 1249 foreach (i; 0 .. data.length - avgDays) 1250 app.put(MutationScore(data[i + avgDays].timeStamp, 1251 typeof(MutationScore.score)(data[i .. i + avgDays].map!(a => a.score.get) 1252 .sum / cast(double) avgDays))); 1253 return MutationScoreHistory(app.data); 1254 } 1255 } 1256 1257 MutationScoreHistory reportMutationScoreHistory(ref Database db) @safe { 1258 return reportMutationScoreHistory(db.getMutationScoreHistory); 1259 } 1260 1261 private MutationScoreHistory reportMutationScoreHistory( 1262 dextool.plugin.mutate.backend.database.type.MutationScore[] data) { 1263 import std.datetime : DateTime, Date, SysTime; 1264 import dextool.plugin.mutate.backend.database.type : MutationScore; 1265 1266 auto pretty = appender!(MutationScore[])(); 1267 1268 if (data.length < 2) { 1269 return MutationScoreHistory(data); 1270 } 1271 1272 auto last = (cast(DateTime) data[0].timeStamp).date; 1273 double acc = data[0].score.get; 1274 double nr = 1; 1275 foreach (a; data[1 .. $]) { 1276 auto curr = (cast(DateTime) a.timeStamp).date; 1277 if (curr == last) { 1278 acc += a.score.get; 1279 nr++; 1280 } else { 1281 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1282 last = curr; 1283 acc = a.score.get; 1284 nr = 1; 1285 } 1286 } 1287 pretty.put(MutationScore(SysTime(last), typeof(MutationScore.score)(acc / nr))); 1288 1289 return MutationScoreHistory(pretty.data); 1290 } 1291 1292 @("shall calculate the mean of the mutation scores") 1293 unittest { 1294 import core.time : days; 1295 import std.datetime : DateTime; 1296 import dextool.plugin.mutate.backend.database.type : MutationScore; 1297 1298 auto data = appender!(MutationScore[])(); 1299 auto d = DateTime(2000, 6, 1, 10, 30, 0); 1300 1301 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(10.0))); 1302 data.put(MutationScore(SysTime(d), typeof(MutationScore.score)(5.0))); 1303 data.put(MutationScore(SysTime(d + 1.days), typeof(MutationScore.score)(5.0))); 1304 1305 auto res = reportMutationScoreHistory(data.data); 1306 1307 res.data[0].score.get.shouldEqual(7.5); 1308 res.data[1].score.get.shouldEqual(5.0); 1309 } 1310 1311 /** Sync status is how old the information about mutants and their status is 1312 * compared to when the tests or source code where last changed. 1313 */ 1314 struct SyncStatus { 1315 import dextool.plugin.mutate.backend.database : MutationStatusTime; 1316 1317 SysTime test; 1318 SysTime code; 1319 SysTime coverage; 1320 MutationStatusTime[] mutants; 1321 } 1322 1323 SyncStatus reportSyncStatus(ref Database db, const long nrMutants) { 1324 import std.datetime : Clock; 1325 import std.traits : EnumMembers; 1326 import dextool.plugin.mutate.backend.database : TestFile, TestFileChecksum, TestFilePath; 1327 1328 typeof(return) rval; 1329 rval.test = spinSql!(() => db.testFileApi.getNewestTestFile) 1330 .orElse(TestFile(TestFilePath.init, TestFileChecksum.init, Clock.currTime)).timeStamp; 1331 rval.code = spinSql!(() => db.getNewestFile).orElse(Clock.currTime); 1332 rval.coverage = spinSql!(() => db.coverageApi.getCoverageTimeStamp).orElse(Clock.currTime); 1333 rval.mutants = spinSql!(() => db.mutantApi.getOldestMutants(nrMutants, 1334 [EnumMembers!(Mutation.Status)].filter!(a => a != Mutation.Status.noCoverage).array)); 1335 return rval; 1336 } 1337 1338 struct TestCaseClassifier { 1339 long threshold; 1340 } 1341 1342 TestCaseClassifier makeTestCaseClassifier(ref Database db, const long minThreshold) { 1343 import std.algorithm : maxElement, max, minElement; 1344 import std.datetime : dur; 1345 import std.math : abs; 1346 import dextool.plugin.mutate.backend.report.kmean; 1347 1348 auto profile = Profile("test case classifier"); 1349 1350 // the distribution is bimodal (U shaped) with one or more tops depending 1351 // on the architecture. The left most edge is the leaf functionality and 1352 // the rest of the edges are the main data flows. 1353 // 1354 // Even though the formula below assume a normal distribution and, 1355 // obviously, this isn't one the result is totally fine because the purpuse 1356 // is to classify "bad" test cases by checking if all mutants that they 1357 // kill are above the threshold. The threshold, as calculcated, thus 1358 // centers around the mean and moves further to the right the further the 1359 // edges are. It also, suitably, handle multiple edges because the only 1360 // important factor is to not get "too close" to the left most edge. That 1361 // would lead to false classifications. 1362 1363 auto tcKills = db.mutantApi 1364 .getAllTestCaseKills 1365 .filter!"a>0" 1366 .map!(a => Point(cast(double) a)) 1367 .array; 1368 // no use in a classifier if there are too mutants. 1369 if (tcKills.length < 100) 1370 return TestCaseClassifier(minThreshold); 1371 1372 // 0.1 is good enough because it is then rounded. 1373 auto iter = KmeanIterator!Point(0.1); 1374 iter.clusters ~= Cluster!Point(0); 1375 // div by 2 reduces the number of iterations for a typical sample. 1376 iter.clusters ~= Cluster!Point(cast(double) tcKills.map!(a => a.value).maxElement / 2.0); 1377 1378 iter.fit(tcKills, 1000, 10.dur!"seconds"); 1379 1380 TestCaseClassifier rval; 1381 rval.threshold = 1 + cast(long)( 1382 iter.clusters.map!"a.mean".minElement + abs( 1383 iter.clusters[0].mean - iter.clusters[1].mean) / 2.0); 1384 1385 logger.tracef("calculated threshold: %s iterations:%s time:%s cluster.mean: %s", 1386 rval.threshold, iter.iterations, iter.time, iter.clusters.map!(a => a.mean)); 1387 rval.threshold = max(rval.threshold, minThreshold); 1388 1389 return rval; 1390 } 1391 1392 struct TestCaseMetadata { 1393 static struct Location { 1394 string file; 1395 Optional!uint line; 1396 } 1397 1398 string[TestCase] text; 1399 Location[TestCase] loc; 1400 1401 /// If the user has manually marked a test case as redundant or not. 1402 bool[TestCase] redundant; 1403 } 1404 1405 TestCaseMetadata parseTestCaseMetadata(AbsolutePath metadataPath) @trusted { 1406 import std.json; 1407 import std.file : readText; 1408 1409 TestCaseMetadata rval; 1410 JSONValue jraw; 1411 try { 1412 jraw = parseJSON(readText(metadataPath.toString)); 1413 } catch (Exception e) { 1414 logger.warning("Error reading ", metadataPath); 1415 logger.info(e.msg); 1416 return rval; 1417 } 1418 1419 try { 1420 foreach (jtc; jraw.array) { 1421 TestCase tc; 1422 1423 try { 1424 if (auto v = "name" in jtc) { 1425 tc = TestCase(v.str); 1426 } else { 1427 logger.warning("Missing `name` in ", jtc.toPrettyString); 1428 continue; 1429 } 1430 1431 if (auto v = "text" in jtc) 1432 rval.text[tc] = v.str; 1433 if (auto v = "location" in jtc) { 1434 TestCaseMetadata.Location loc; 1435 if (auto f = "file" in *v) 1436 loc.file = f.str; 1437 if (auto l = "line" in *v) 1438 loc.line = some(cast(uint) l.integer); 1439 rval.loc[tc] = loc; 1440 } 1441 1442 if (auto v = "redundant" in jtc) 1443 rval.redundant[tc] = v.boolean; 1444 } catch (Exception e) { 1445 logger.warning("Error parsing ", jtc.toPrettyString); 1446 logger.warning(e.msg); 1447 } 1448 } 1449 } catch (Exception e) { 1450 logger.warning("Error parsing ", jraw.toPrettyString); 1451 logger.warning(e.msg); 1452 } 1453 1454 return rval; 1455 } 1456 1457 alias AverageTimePerMutant = NamedType!(Duration, Tag!"AverageTimePerMutant", 1458 Duration.init, TagStringable, ImplicitConvertable); 1459 1460 /// Based on the last 100 tested mutants. 1461 AverageTimePerMutant calcAvgPerMutant(ref Database db) nothrow { 1462 import core.time : dur; 1463 1464 auto times = spinSql!(() => db.mutantApi.getLatestMutantTimes(100)); 1465 if (times.length == 0) 1466 return AverageTimePerMutant.init; 1467 1468 const avg = (times.map!(a => a.compileTime) 1469 .sum 1470 .total!"msecs" + times.map!(a => a.testTime) 1471 .sum 1472 .total!"msecs") / times.length; 1473 return avg.dur!"msecs".AverageTimePerMutant; 1474 }