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;
18 import std.conv : to;
19 import std.exception : collectException;
20 import std.format : format;
21 import std.range : take, retro, only;
22 import std.typecons : Flag, Yes, No, Tuple, Nullable, tuple;
23 
24 import dextool.plugin.mutate.backend.database : Database, spinSql, MutationId, MarkedMutant;
25 import dextool.plugin.mutate.backend.diff_parser : Diff;
26 import dextool.plugin.mutate.backend.generate_mutant : MakeMutationTextResult,
27     makeMutationText, makeMutation;
28 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
29 import dextool.plugin.mutate.backend.report.utility : window, windowSize,
30     statusToString, kindToString;
31 import dextool.plugin.mutate.backend.type : Mutation, Offset, TestCase, TestGroup;
32 import dextool.plugin.mutate.backend.utility : Profile;
33 import dextool.plugin.mutate.type : ReportKillSortOrder, ReportLevel, ReportSection;
34 import dextool.type;
35 
36 public import dextool.plugin.mutate.backend.report.utility : Table;
37 
38 @safe:
39 
40 void reportMutationSubtypeStats(ref const long[MakeMutationTextResult] mut_stat, ref Table!4 tbl) @safe nothrow {
41     auto profile = Profile(ReportSection.mut_stat);
42 
43     long total = mut_stat.byValue.sum;
44 
45     foreach (v; mut_stat.byKeyValue.array.sort!((a, b) => a.value > b.value).take(20)) {
46         try {
47             auto percentage = (cast(double) v.value / cast(double) total) * 100.0;
48 
49             // dfmt off
50             typeof(tbl).Row r = [
51                 percentage.to!string,
52                 v.value.to!string,
53                 format("`%s`", window(v.key.original, windowSize)),
54                 format("`%s`", window(v.key.mutation, windowSize)),
55             ];
56             // dfmt on
57             tbl.put(r);
58         } catch (Exception e) {
59             logger.warning(e.msg).collectException;
60         }
61     }
62 }
63 
64 /** Test case score based on how many mutants they killed.
65  */
66 struct TestCaseStat {
67     import dextool.plugin.mutate.backend.database.type : TestCaseInfo;
68 
69     struct Info {
70         double ratio;
71         TestCase tc;
72         TestCaseInfo info;
73         alias info this;
74     }
75 
76     Info[TestCase] testCases;
77 
78     /// Returns: the test cases sorted from most kills to least kills.
79     auto toSortedRange() {
80         static bool cmp(T)(ref T a, ref T b) {
81             if (a.killedMutants > b.killedMutants)
82                 return true;
83             else if (a.killedMutants < b.killedMutants)
84                 return false;
85             else if (a.tc.name > b.tc.name)
86                 return true;
87             else if (a.tc.name < b.tc.name)
88                 return false;
89             return false;
90         }
91 
92         return testCases.byValue.array.sort!cmp;
93     }
94 }
95 
96 /** Update the table with the score of test cases and how many mutants they killed.
97  *
98  * Params:
99  *  take_ = how many from the top should be moved to the table
100  *  sort_order = ctrl if the top or bottom of the test cases should be reported
101  *  tbl = table to write the data to
102  */
103 void toTable(ref TestCaseStat st, const long take_,
104         const ReportKillSortOrder sort_order, ref Table!3 tbl) @safe nothrow {
105     auto takeOrder(RangeT)(RangeT range) {
106         final switch (sort_order) {
107         case ReportKillSortOrder.top:
108             return range.take(take_).array;
109         case ReportKillSortOrder.bottom:
110             return range.retro.take(take_).array;
111         }
112     }
113 
114     foreach (v; takeOrder(st.toSortedRange)) {
115         try {
116             typeof(tbl).Row r = [
117                 (100.0 * v.ratio).to!string, v.info.killedMutants.to!string,
118                 v.tc.name
119             ];
120             tbl.put(r);
121         } catch (Exception e) {
122             logger.warning(e.msg).collectException;
123         }
124     }
125 }
126 
127 /** Extract the number of source code mutants that a test case has killed and
128  * how much the kills contributed to the total.
129  */
130 TestCaseStat reportTestCaseStats(ref Database db, const Mutation.Kind[] kinds) @safe nothrow {
131     import dextool.plugin.mutate.backend.database.type : TestCaseInfo;
132 
133     auto profile = Profile(ReportSection.tc_stat);
134 
135     const total = spinSql!(() { return db.totalSrcMutants(kinds).count; });
136     // nothing to do. this also ensure that we do not divide by zero.
137     if (total == 0)
138         return TestCaseStat.init;
139 
140     alias TcInfo = Tuple!(TestCase, "tc", TestCaseInfo, "info");
141     TestCaseStat rval;
142 
143     foreach (v; spinSql!(() { return db.getDetectedTestCases; }).map!(a => TcInfo(a, spinSql!(() {
144                 return db.getTestCaseInfo(a, kinds);
145             })))) {
146         try {
147             const ratio = cast(double) v.info.killedMutants / cast(double) total;
148             rval.testCases[v.tc] = TestCaseStat.Info(ratio, v.tc, v.info);
149         } catch (Exception e) {
150             logger.warning(e.msg).collectException;
151         }
152     }
153 
154     return rval;
155 }
156 
157 /** The result of analysing the test cases to see how similare they are to each
158  * other.
159  */
160 class TestCaseSimilarityAnalyse {
161     import dextool.plugin.mutate.backend.type : TestCase;
162 
163     static struct Similarity {
164         TestCase testCase;
165         double similarity;
166         /// Mutants that are similare between `testCase` and the parent.
167         MutationId[] intersection;
168         /// Unique mutants that are NOT verified by `testCase`.
169         MutationId[] difference;
170     }
171 
172     Similarity[][TestCase] similarities;
173 }
174 
175 /// The result of the similarity analyse
176 private struct Similarity {
177     /// The quota |A intersect B| / |A|. Thus it is how similare A is to B. If
178     /// B ever fully encloses A then the score is 1.0.
179     double similarity;
180     MutationId[] intersection;
181     MutationId[] difference;
182 }
183 
184 // The set similairty measures how much of lhs is in rhs. This is a
185 // directional metric.
186 private Similarity setSimilarity(MutationId[] lhs_, MutationId[] rhs_) {
187     import my.set;
188 
189     auto lhs = lhs_.toSet;
190     auto rhs = rhs_.toSet;
191     auto intersect = lhs.intersect(rhs);
192     auto diff = lhs.setDifference(rhs);
193     return Similarity(cast(double) intersect.length / cast(double) lhs.length,
194             intersect.toArray, diff.toArray);
195 }
196 
197 /** Analyse the similarity between test cases.
198  *
199  * TODO: the algorithm used is slow. Maybe matrix representation and sorted is better?
200  *
201  * Params:
202  *  db = ?
203  *  kinds = mutation kinds to use in the distance analyze
204  *  limit = limit the number of test cases to the top `limit`.
205  */
206 TestCaseSimilarityAnalyse reportTestCaseSimilarityAnalyse(ref Database db,
207         const Mutation.Kind[] kinds, ulong limit) @safe {
208     import std.container.binaryheap;
209     import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId;
210 
211     auto profile = Profile(ReportSection.tc_similarity);
212 
213     // TODO: reduce the code duplication of the caches.
214     // The DB lookups must be cached or otherwise the algorithm becomes too
215     // slow for practical use.
216 
217     MutationId[][TestCaseId] kill_cache2;
218     MutationId[] getKills(TestCaseId id) @trusted {
219         return kill_cache2.require(id, spinSql!(() {
220                 return db.getTestCaseMutantKills(id, kinds);
221             }));
222     }
223 
224     TestCase[TestCaseId] tc_cache2;
225     TestCase getTestCase(TestCaseId id) @trusted {
226         return tc_cache2.require(id, spinSql!(() {
227                 // assuming it can never be null
228                 return db.getTestCase(id).get;
229             }));
230     }
231 
232     alias TcKills = Tuple!(TestCaseId, "id", MutationId[], "kills");
233 
234     const test_cases = spinSql!(() { return db.getDetectedTestCaseIds; });
235 
236     auto rval = new typeof(return);
237 
238     foreach (tc_kill; test_cases.map!(a => TcKills(a, getKills(a)))
239             .filter!(a => a.kills.length != 0)) {
240         auto app = appender!(TestCaseSimilarityAnalyse.Similarity[])();
241         foreach (tc; test_cases.filter!(a => a != tc_kill.id)
242                 .map!(a => TcKills(a, getKills(a)))
243                 .filter!(a => a.kills.length != 0)) {
244             auto distance = setSimilarity(tc_kill.kills, tc.kills);
245             if (distance.similarity > 0)
246                 app.put(TestCaseSimilarityAnalyse.Similarity(getTestCase(tc.id),
247                         distance.similarity, distance.intersection, distance.difference));
248         }
249         if (app.data.length != 0) {
250             () @trusted {
251                 rval.similarities[getTestCase(tc_kill.id)] = heapify!((a,
252                         b) => a.similarity < b.similarity)(app.data).take(limit).array;
253             }();
254         }
255     }
256 
257     return rval;
258 }
259 
260 /// Statistics about dead test cases.
261 struct TestCaseDeadStat {
262     import std.range : isOutputRange;
263 
264     /// The ratio of dead TC of the total.
265     double ratio;
266     TestCase[] testCases;
267     long total;
268 
269     long numDeadTC() @safe pure nothrow const @nogc scope {
270         return testCases.length;
271     }
272 
273     string toString() @safe const {
274         auto buf = appender!string;
275         toString(buf);
276         return buf.data;
277     }
278 
279     void toString(Writer)(ref Writer w) @safe const 
280             if (isOutputRange!(Writer, char)) {
281         import std.ascii : newline;
282         import std.format : formattedWrite;
283         import std.range : put;
284 
285         if (total > 0)
286             formattedWrite(w, "%s/%s = %s of all test cases\n", numDeadTC, total, ratio);
287         foreach (tc; testCases) {
288             put(w, tc.name);
289             if (tc.location.length > 0) {
290                 put(w, " | ");
291                 put(w, tc.location);
292             }
293             put(w, newline);
294         }
295     }
296 }
297 
298 void toTable(ref TestCaseDeadStat st, ref Table!2 tbl) @safe pure nothrow {
299     foreach (tc; st.testCases) {
300         typeof(tbl).Row r = [tc.name, tc.location];
301         tbl.put(r);
302     }
303 }
304 
305 /** Returns: report of test cases that has killed zero mutants.
306  */
307 TestCaseDeadStat reportDeadTestCases(ref Database db) @safe {
308     auto profile = Profile(ReportSection.tc_killed_no_mutants);
309 
310     TestCaseDeadStat r;
311     r.total = db.getNumOfTestCases;
312     r.testCases = db.getTestCasesWithZeroKills;
313     if (r.total > 0)
314         r.ratio = cast(double) r.numDeadTC / cast(double) r.total;
315     return r;
316 }
317 
318 /// Information needed to present the mutant to an user.
319 struct MutationRepr {
320     import dextool.type : Path;
321     import dextool.plugin.mutate.backend.type : SourceLoc;
322 
323     SourceLoc sloc;
324     Path file;
325     MakeMutationTextResult mutation;
326 }
327 
328 alias Mutations = bool[MutationId];
329 alias MutationsMap = Mutations[TestCase];
330 alias MutationReprMap = MutationRepr[MutationId];
331 
332 void reportTestCaseKillMap(WriterTextT, WriterT)(ref const MutationsMap mut_stat,
333         ref const MutationReprMap mutrepr, WriterTextT writer_txt, WriterT writer) @safe {
334     import std.range : put;
335 
336     auto profile = Profile(ReportSection.tc_map);
337 
338     alias MutTable = Table!4;
339     alias Row = MutTable.Row;
340 
341     foreach (tc_muts; mut_stat.byKeyValue) {
342         put(writer_txt, tc_muts.key.toString);
343 
344         MutTable tbl;
345         tbl.heading = ["ID", "File Line:Column", "From", "To"];
346 
347         foreach (mut; tc_muts.value.byKey) {
348             Row row;
349 
350             if (auto v = mut in mutrepr) {
351                 row[1] = format("%s %s:%s", v.file, v.sloc.line, v.sloc.column);
352                 row[2] = format("`%s`", window(v.mutation.original, windowSize));
353                 row[3] = format("`%s`", window(v.mutation.mutation, windowSize));
354             }
355 
356             row[0] = mut.to!string;
357             tbl.put(row);
358         }
359 
360         put(writer, tbl);
361     }
362 }
363 
364 void reportMutationTestCaseSuggestion(WriterT)(ref Database db,
365         const MutationId[] tc_sugg, WriterT writer) @safe {
366     import std.range : put;
367 
368     auto profile = Profile(ReportSection.tc_suggestion);
369 
370     alias MutTable = Table!1;
371     alias Row = MutTable.Row;
372 
373     foreach (mut_id; tc_sugg) {
374         MutTable tbl;
375         tbl.heading = [mut_id.to!string];
376 
377         try {
378             auto suggestions = db.getSurroundingTestCases(mut_id);
379             if (suggestions.length == 0)
380                 continue;
381 
382             foreach (tc; suggestions) {
383                 Row row;
384                 row[0] = format("`%s`", tc);
385                 tbl.put(row);
386             }
387             put(writer, tbl);
388         } catch (Exception e) {
389             logger.warning(e.msg);
390         }
391     }
392 }
393 
394 /// Statistics for a group of mutants.
395 struct MutationStat {
396     import core.time : Duration;
397     import std.range : isOutputRange;
398 
399     long alive;
400     // Nr of mutants that are alive but tagged with nomut.
401     long aliveNoMut;
402     long killed;
403     long timeout;
404     long untested;
405     long killedByCompiler;
406     long total;
407 
408     Duration totalTime;
409     Duration killedByCompilerTime;
410     Duration predictedDone;
411 
412     /// Adjust the score with the alive mutants that are suppressed.
413     double score() @safe pure nothrow const @nogc {
414         if (total > 0)
415             return cast(double)(killed + timeout) / cast(double)(total - aliveNoMut);
416         if (untested > 0)
417             return 0.0;
418         return 1.0;
419     }
420 
421     /// Suppressed mutants of the total mutants.
422     double suppressedOfTotal() @safe pure nothrow const @nogc {
423         if (total > 0)
424             return (cast(double)(aliveNoMut) / cast(double) total);
425         return 0.0;
426     }
427 
428     string toString() @safe const {
429         auto buf = appender!string;
430         toString(buf);
431         return buf.data;
432     }
433 
434     void toString(Writer)(ref Writer w) const if (isOutputRange!(Writer, char)) {
435         import core.time : dur;
436         import std.ascii : newline;
437         import std.datetime : Clock;
438         import std.format : formattedWrite;
439         import std.range : put;
440         import dextool.plugin.mutate.backend.utility;
441 
442         immutable align_ = 12;
443 
444         formattedWrite(w, "%-*s %s\n", align_, "Time spent:", totalTime);
445         if (untested > 0 && predictedDone > 0.dur!"msecs") {
446             const pred = Clock.currTime + predictedDone;
447             formattedWrite(w, "Remaining: %s (%s)\n", predictedDone, pred.toISOExtString);
448         }
449         if (killedByCompiler > 0) {
450             formattedWrite(w, "%-*s %s\n", align_ * 3,
451                     "Time spent on mutants killed by compiler:", killedByCompilerTime);
452         }
453 
454         put(w, newline);
455 
456         // mutation score and details
457         formattedWrite(w, "%-*s %.3s\n", align_, "Score:", score);
458         formattedWrite(w, "%-*s %s\n", align_, "Total:", total);
459         if (untested > 0) {
460             formattedWrite(w, "%-*s %s\n", align_, "Untested:", untested);
461         }
462         formattedWrite(w, "%-*s %s\n", align_, "Alive:", alive);
463         formattedWrite(w, "%-*s %s\n", align_, "Killed:", killed);
464         formattedWrite(w, "%-*s %s\n", align_, "Timeout:", timeout);
465         formattedWrite(w, "%-*s %s\n", align_, "Killed by compiler:", killedByCompiler);
466 
467         if (aliveNoMut != 0)
468             formattedWrite(w, "%-*s %s (%.3s)\n", align_,
469                     "Suppressed (nomut):", aliveNoMut, suppressedOfTotal);
470     }
471 }
472 
473 MutationStat reportStatistics(ref Database db, const Mutation.Kind[] kinds, string file = null) @safe nothrow {
474     import core.time : dur;
475     import dextool.plugin.mutate.backend.utility;
476 
477     auto profile = Profile(ReportSection.summary);
478 
479     const alive = spinSql!(() { return db.aliveSrcMutants(kinds, file); });
480     const alive_nomut = spinSql!(() {
481         return db.aliveNoMutSrcMutants(kinds, file);
482     });
483     const killed = spinSql!(() { return db.killedSrcMutants(kinds, file); });
484     const timeout = spinSql!(() { return db.timeoutSrcMutants(kinds, file); });
485     const untested = spinSql!(() { return db.unknownSrcMutants(kinds, file); });
486     const killed_by_compiler = spinSql!(() {
487         return db.killedByCompilerSrcMutants(kinds, file);
488     });
489     const total = spinSql!(() { return db.totalSrcMutants(kinds, file); });
490 
491     MutationStat st;
492     st.alive = alive.count;
493     st.aliveNoMut = alive_nomut.count;
494     st.killed = killed.count;
495     st.timeout = timeout.count;
496     st.untested = untested.count;
497     st.total = total.count;
498     st.killedByCompiler = killed_by_compiler.count;
499 
500     st.totalTime = total.time;
501     st.predictedDone = st.total > 0 ? (st.untested * (st.totalTime / st.total)) : 0.dur!"msecs";
502     st.killedByCompilerTime = killed_by_compiler.time;
503 
504     return st;
505 }
506 
507 struct MarkedMutantsStat {
508     Table!6 tbl;
509 }
510 
511 MarkedMutantsStat reportMarkedMutants(ref Database db, const Mutation.Kind[] kinds,
512         string file = null) @safe {
513     MarkedMutantsStat st;
514     st.tbl.heading = [
515         "File", "Line", "Column", "Mutation", "Status", "Rationale"
516     ];
517 
518     foreach (m; db.getMarkedMutants()) {
519         typeof(st.tbl).Row r = [
520             m.path, m.sloc.line.to!string, m.sloc.column.to!string,
521             m.mutText, statusToString(m.toStatus), m.rationale
522         ];
523         st.tbl.put(r);
524     }
525     return st;
526 }
527 
528 struct TestCaseOverlapStat {
529     import std.format : formattedWrite;
530     import std.range : put;
531     import my.hash;
532     import dextool.plugin.mutate.backend.database.type : TestCaseId;
533 
534     long overlap;
535     long total;
536     double ratio;
537 
538     // map between test cases and the mutants they have killed.
539     TestCaseId[][Murmur3] tc_mut;
540     // map between mutation IDs and the test cases that killed them.
541     long[][Murmur3] mutid_mut;
542     string[TestCaseId] name_tc;
543 
544     string sumToString() @safe const {
545         return format("%s/%s = %s test cases", overlap, total, ratio);
546     }
547 
548     void sumToString(Writer)(ref Writer w) @trusted const {
549         formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio);
550     }
551 
552     string toString() @safe const {
553         auto buf = appender!string;
554         toString(buf);
555         return buf.data;
556     }
557 
558     void toString(Writer)(ref Writer w) @safe const {
559         sumToString(w);
560 
561         foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
562             bool first = true;
563             // TODO this is a bit slow. use a DB row iterator instead.
564             foreach (name; tcs.value.map!(id => name_tc[id])) {
565                 if (first) {
566                     () @trusted {
567                         formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length);
568                     }();
569                     first = false;
570                 } else {
571                     () @trusted { formattedWrite(w, "%s\n", name); }();
572                 }
573             }
574             put(w, "\n");
575         }
576     }
577 }
578 
579 /** Report test cases that completly overlap each other.
580  *
581  * Returns: a string with statistics.
582  */
583 template toTable(Flag!"colWithMutants" colMutants) {
584     static if (colMutants) {
585         alias TableT = Table!3;
586     } else {
587         alias TableT = Table!2;
588     }
589     alias RowT = TableT.Row;
590 
591     void toTable(ref TestCaseOverlapStat st, ref TableT tbl) {
592         foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
593             bool first = true;
594             // TODO this is a bit slow. use a DB row iterator instead.
595             foreach (name; tcs.value.map!(id => st.name_tc[id])) {
596                 RowT r;
597                 r[0] = name;
598                 if (first) {
599                     auto muts = st.mutid_mut[tcs.key];
600                     r[1] = muts.length.to!string;
601                     static if (colMutants) {
602                         r[2] = format("%-(%s,%)", muts);
603                     }
604                     first = false;
605                 }
606 
607                 tbl.put(r);
608             }
609             static if (colMutants)
610                 RowT r = ["", "", ""];
611             else
612                 RowT r = ["", ""];
613             tbl.put(r);
614         }
615     }
616 }
617 
618 /// Test cases that kill exactly the same mutants.
619 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe {
620     import my.hash;
621     import dextool.plugin.mutate.backend.database.type : TestCaseId;
622 
623     auto profile = Profile(ReportSection.tc_full_overlap);
624 
625     TestCaseOverlapStat st;
626     st.total = db.getNumOfTestCases;
627 
628     foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) {
629         auto muts = db.getTestCaseMutantKills(tc_id, kinds).sort.map!(a => cast(long) a).array;
630         auto m3 = makeMurmur3(cast(ubyte[]) muts);
631         if (auto v = m3 in st.tc_mut)
632             (*v) ~= tc_id;
633         else {
634             st.tc_mut[m3] = [tc_id];
635             st.mutid_mut[m3] = muts;
636         }
637         st.name_tc[tc_id] = db.getTestCaseName(tc_id);
638     }
639 
640     foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
641         st.overlap += tcs.value.count;
642     }
643 
644     if (st.total > 0)
645         st.ratio = cast(double) st.overlap / cast(double) st.total;
646 
647     return st;
648 }
649 
650 class TestGroupSimilarity {
651     static struct TestGroup {
652         string description;
653         string name;
654 
655         /// What the user configured as regex. Useful when e.g. generating reports
656         /// for a user.
657         string userInput;
658 
659         int opCmp(ref const TestGroup s) const {
660             return cmp(name, s.name);
661         }
662     }
663 
664     static struct Similarity {
665         /// The test group that the `key` is compared to.
666         TestGroup comparedTo;
667         /// How similare the `key` is to `comparedTo`.
668         double similarity;
669         /// Mutants that are similare between `testCase` and the parent.
670         MutationId[] intersection;
671         /// Unique mutants that are NOT verified by `testCase`.
672         MutationId[] difference;
673     }
674 
675     Similarity[][TestGroup] similarities;
676 }
677 
678 /** Analyze the similarity between the test groups.
679  *
680  * Assuming that a limit on how many test groups to report isn't interesting
681  * because they are few so it is never a problem.
682  *
683  */
684 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db,
685         const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe {
686     import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId;
687 
688     auto profile = Profile(ReportSection.tc_groups_similarity);
689 
690     alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", MutationId[], "kills");
691 
692     const test_cases = spinSql!(() { return db.getDetectedTestCaseIds; }).map!(
693             a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() {
694                 return db.getTestCase(a);
695             }))).array;
696 
697     MutationId[] gatherKilledMutants(const(TestGroup) tg) {
698         auto kills = appender!(MutationId[])();
699         foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) {
700             kills.put(spinSql!(() {
701                     return db.getTestCaseMutantKills(tc.id, kinds);
702                 }));
703         }
704         return kills.data;
705     }
706 
707     TgKills[] test_group_kills;
708     foreach (const tg; test_groups) {
709         auto kills = gatherKilledMutants(tg);
710         if (kills.length != 0)
711             test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description,
712                     tg.name, tg.userInput), kills);
713     }
714 
715     // calculate similarity between all test groups.
716     auto rval = new typeof(return);
717 
718     foreach (tg_parent; test_group_kills) {
719         auto app = appender!(TestGroupSimilarity.Similarity[])();
720         foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) {
721             auto similarity = setSimilarity(tg_parent.kills, tg_other.kills);
722             if (similarity.similarity > 0)
723                 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup,
724                         similarity.similarity, similarity.intersection, similarity.difference));
725             if (app.data.length != 0)
726                 rval.similarities[tg_parent.testGroup] = app.data;
727         }
728     }
729 
730     return rval;
731 }
732 
733 class TestGroupStat {
734     import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo;
735 
736     /// Human readable description for the test group.
737     string description;
738     /// Statistics for a test group.
739     MutationStat stats;
740     /// Map between test cases and their test group.
741     TestCase[] testCases;
742     /// Lookup for converting a id to a filename
743     Path[FileId] files;
744     /// Mutants alive in a file.
745     MutantInfo[][FileId] alive;
746     /// Mutants killed in a file.
747     MutantInfo[][FileId] killed;
748 }
749 
750 import std.regex : Regex;
751 
752 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) {
753     import std.regex : matchFirst;
754 
755     auto m = matchFirst(tc.name, tg);
756     // the regex must match the full test case thus checking that
757     // nothing is left before or after
758     if (!m.empty && m.pre.length == 0 && m.post.length == 0) {
759         return true;
760     }
761     return false;
762 }
763 
764 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds,
765         const(TestGroup) test_g) @safe {
766     import dextool.plugin.mutate.backend.database : MutationStatusId;
767     import my.set;
768 
769     auto profile = Profile(ReportSection.tc_groups);
770 
771     static struct TcStat {
772         Set!MutationStatusId alive;
773         Set!MutationStatusId killed;
774         Set!MutationStatusId timeout;
775         Set!MutationStatusId total;
776 
777         // killed by the specific test case
778         Set!MutationStatusId tcKilled;
779     }
780 
781     auto r = new TestGroupStat;
782     r.description = test_g.description;
783     TcStat tc_stat;
784 
785     // map test cases to this test group
786     foreach (tc; db.getDetectedTestCases) {
787         if (tc.isTestCaseInTestGroup(test_g.re))
788             r.testCases ~= tc;
789     }
790 
791     // collect mutation statistics for each test case group
792     foreach (const tc; r.testCases) {
793         foreach (const id; db.testCaseMutationPointAliveSrcMutants(kinds, tc))
794             tc_stat.alive.add(id);
795         foreach (const id; db.testCaseMutationPointKilledSrcMutants(kinds, tc))
796             tc_stat.killed.add(id);
797         foreach (const id; db.testCaseMutationPointTimeoutSrcMutants(kinds, tc))
798             tc_stat.timeout.add(id);
799         foreach (const id; db.testCaseMutationPointTotalSrcMutants(kinds, tc))
800             tc_stat.total.add(id);
801         foreach (const id; db.testCaseKilledSrcMutants(kinds, tc))
802             tc_stat.tcKilled.add(id);
803     }
804 
805     // update the mutation stat for the test group
806     r.stats.alive = tc_stat.alive.length;
807     r.stats.killed = tc_stat.killed.length;
808     r.stats.timeout = tc_stat.timeout.length;
809     r.stats.total = tc_stat.total.length;
810 
811     // associate mutants with their file
812     foreach (const m; db.getMutantsInfo(kinds, tc_stat.tcKilled.toArray)) {
813         auto fid = db.getFileId(m.id);
814         r.killed[fid.get] ~= m;
815 
816         if (fid.get !in r.files) {
817             r.files[fid.get] = Path.init;
818             r.files[fid.get] = db.getFile(fid.get);
819         }
820     }
821 
822     foreach (const m; db.getMutantsInfo(kinds, tc_stat.alive.toArray)) {
823         auto fid = db.getFileId(m.id);
824         r.alive[fid.get] ~= m;
825 
826         if (fid.get !in r.files) {
827             r.files[fid.get] = Path.init;
828             r.files[fid.get] = db.getFile(fid.get);
829         }
830     }
831 
832     return r;
833 }
834 
835 /// High interest mutants.
836 class MutantSample {
837     import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo,
838         MutationStatus, MutationStatusId, MutationEntry, MutationStatusTime;
839 
840     MutationEntry[MutationStatusId] mutants;
841 
842     /// The mutant that had its status updated the furthest back in time.
843     MutationStatusTime[] oldest;
844 
845     /// The mutant that has survived the longest in the system.
846     MutationStatus[] hardestToKill;
847 
848     /// The latest mutants that where added and survived.
849     MutationStatusTime[] latest;
850 }
851 
852 /// Returns: samples of mutants that are of high interest to the user.
853 MutantSample reportSelectedAliveMutants(ref Database db,
854         const(Mutation.Kind)[] kinds, long history_nr) {
855     auto profile = Profile(ReportSection.mut_recommend_kill);
856 
857     auto rval = new typeof(return);
858 
859     rval.hardestToKill = db.getHardestToKillMutant(kinds, Mutation.Status.alive, history_nr);
860     foreach (const mutst; rval.hardestToKill) {
861         auto ids = db.getMutationIds(kinds, [mutst.statusId]);
862         if (ids.length != 0)
863             rval.mutants[mutst.statusId] = db.getMutation(ids[0]);
864     }
865 
866     rval.oldest = db.getOldestMutants(kinds, history_nr);
867     foreach (const mutst; rval.oldest) {
868         auto ids = db.getMutationIds(kinds, [mutst.id]);
869         if (ids.length != 0)
870             rval.mutants[mutst.id] = db.getMutation(ids[0]);
871     }
872 
873     return rval;
874 }
875 
876 class DiffReport {
877     import dextool.plugin.mutate.backend.database : FileId, MutantInfo;
878     import dextool.plugin.mutate.backend.diff_parser : Diff;
879 
880     /// The mutation score.
881     double score;
882 
883     /// The raw diff for a file
884     Diff.Line[][FileId] rawDiff;
885 
886     /// Lookup for converting a id to a filename
887     Path[FileId] files;
888     /// Mutants alive in a file.
889     MutantInfo[][FileId] alive;
890     /// Mutants killed in a file.
891     MutantInfo[][FileId] killed;
892     /// Test cases that killed mutants.
893     TestCase[] testCases;
894 
895     override string toString() @safe const {
896         import std.format : formattedWrite;
897         import std.range : put;
898 
899         auto w = appender!string;
900 
901         foreach (file; files.byKeyValue) {
902             put(w, file.value.toString);
903             foreach (mut; alive[file.key])
904                 formattedWrite(w, "  %s\n", mut);
905             foreach (mut; killed[file.key])
906                 formattedWrite(w, "  %s\n", mut);
907         }
908 
909         formattedWrite(w, "Test Cases killing mutants");
910         foreach (tc; testCases)
911             formattedWrite(w, "  %s", tc);
912 
913         return w.data;
914     }
915 }
916 
917 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds,
918         ref Diff diff, AbsolutePath workdir) {
919     import dextool.plugin.mutate.backend.database : MutationId, MutationStatusId;
920     import dextool.plugin.mutate.backend.type : SourceLoc;
921     import my.set;
922 
923     auto profile = Profile(ReportSection.diff);
924 
925     auto rval = new DiffReport;
926 
927     Set!MutationStatusId total;
928     Set!MutationId alive;
929     Set!MutationId killed;
930 
931     foreach (kv; diff.toRange(workdir)) {
932         auto fid = db.getFileId(kv.key);
933         if (fid.isNull) {
934             logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key);
935             continue;
936         }
937 
938         bool hasMutants;
939         foreach (id; kv.value
940                 .toRange
941                 .map!(line => spinSql!(() => db.getMutationsOnLine(kinds,
942                     fid.get, SourceLoc(line))))
943                 .joiner
944                 .filter!(a => a !in total)) {
945             hasMutants = true;
946             total.add(id);
947 
948             const info = db.getMutantsInfo(kinds, [id])[0];
949             if (info.status == Mutation.Status.alive) {
950                 rval.alive[fid.get] ~= info;
951                 alive.add(info.id);
952             } else if (info.status.among(Mutation.Status.killed, Mutation.Status.timeout)) {
953                 rval.killed[fid.get] ~= info;
954                 killed.add(info.id);
955             }
956         }
957 
958         if (hasMutants) {
959             rval.files[fid.get] = kv.key;
960             rval.rawDiff[fid.get] = diff.rawDiff[kv.key];
961         } else {
962             logger.info("This file in the diff has no mutants on changed lines: ", kv.key);
963         }
964     }
965 
966     Set!TestCase test_cases;
967     foreach (tc; killed.toRange.map!(a => db.getTestCases(a)).joiner) {
968         test_cases.add(tc);
969     }
970 
971     rval.testCases = test_cases.toArray.sort.array;
972 
973     if (total.length == 0) {
974         rval.score = 1.0;
975     } else {
976         // TODO: use total to compute e.g. a standard deviation or some other
977         // useful statistical metric to convey a "confidence" of the value.
978         rval.score = cast(double) killed.length / cast(double)(killed.length + alive.length);
979     }
980 
981     return rval;
982 }
983 
984 struct MinimalTestSet {
985     import dextool.plugin.mutate.backend.database.type : TestCaseInfo;
986 
987     long total;
988 
989     /// Minimal set that achieve the mutation test score.
990     TestCase[] minimalSet;
991     /// Test cases that do not contribute to the mutation test score.
992     TestCase[] redundant;
993     /// Map between test case name and sum of all the test time of the mutants it killed.
994     TestCaseInfo[string] testCaseTime;
995 }
996 
997 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) {
998     import dextool.plugin.mutate.backend.database : TestCaseId, TestCaseInfo;
999     import my.set;
1000 
1001     auto profile = Profile(ReportSection.tc_min_set);
1002 
1003     alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info");
1004 
1005     MinimalTestSet rval;
1006 
1007     Set!MutationId killedMutants;
1008 
1009     // start by picking test cases that have the fewest kills.
1010     foreach (const val; db.getDetectedTestCases
1011             .map!(a => tuple(a, db.getTestCaseId(a)))
1012             .filter!(a => !a[1].isNull)
1013             .map!(a => TcIdInfo(a[0], a[1], db.getTestCaseInfo(a[0], kinds)))
1014             .filter!(a => a.info.killedMutants != 0)
1015             .array
1016             .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) {
1017         rval.testCaseTime[val.tc.name] = val.info;
1018 
1019         const killed = killedMutants.length;
1020         foreach (const id; db.getTestCaseMutantKills(val.id, kinds)) {
1021             killedMutants.add(id);
1022         }
1023 
1024         if (killedMutants.length > killed)
1025             rval.minimalSet ~= val.tc;
1026         else
1027             rval.redundant ~= val.tc;
1028     }
1029 
1030     rval.total = rval.minimalSet.length + rval.redundant.length;
1031 
1032     return rval;
1033 }
1034 
1035 struct TestCaseUniqueness {
1036     MutationId[][TestCase] uniqueKills;
1037 
1038     // test cases that have no unique kills. These are candidates for being
1039     // refactored/removed.
1040     TestCase[] noUniqueKills;
1041 }
1042 
1043 /// Returns: a report of the mutants that a test case is the only one that kills.
1044 TestCaseUniqueness reportTestCaseUniqueness(ref Database db, const Mutation.Kind[] kinds) {
1045     import dextool.plugin.mutate.backend.database.type : TestCaseId;
1046     import my.set;
1047 
1048     auto profile = Profile(ReportSection.tc_unique);
1049 
1050     /// any time a mutant is killed by more than one test case it is removed.
1051     TestCaseId[MutationId] killedBy;
1052     Set!MutationId blacklist;
1053 
1054     foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) {
1055         auto muts = db.getTestCaseMutantKills(tc_id, kinds);
1056         foreach (m; muts.filter!(a => !blacklist.contains(a))) {
1057             if (m in killedBy) {
1058                 killedBy.remove(m);
1059                 blacklist.add(m);
1060             } else {
1061                 killedBy[m] = tc_id;
1062             }
1063         }
1064     }
1065 
1066     // use a cache to reduce the database access
1067     TestCase[TestCaseId] idToTc;
1068     TestCase getTestCase(TestCaseId id) @trusted {
1069         return idToTc.require(id, spinSql!(() { return db.getTestCase(id).get; }));
1070     }
1071 
1072     typeof(return) rval;
1073     Set!TestCaseId uniqueTc;
1074     foreach (kv; killedBy.byKeyValue) {
1075         rval.uniqueKills[getTestCase(kv.value)] ~= kv.key;
1076         uniqueTc.add(kv.value);
1077     }
1078     foreach (tc_id; db.getDetectedTestCaseIds.filter!(a => !uniqueTc.contains(a))) {
1079         rval.noUniqueKills ~= getTestCase(tc_id);
1080     }
1081 
1082     return rval;
1083 }
1084 
1085 private: