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