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 }