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