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