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