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