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 }
480 
481 MarkedMutantsStat reportMarkedMutants(ref Database db, const Mutation.Kind[] kinds,
482         string file = null) @safe {
483     MarkedMutantsStat st;
484     st.tbl.heading = [
485         "File", "Line", "Column", "Mutation", "Status", "Rationale"
486     ];
487 
488     foreach (m; db.getMarkedMutants()) {
489         typeof(st.tbl).Row r = [
490             m.path, m.sloc.line.to!string, m.sloc.column.to!string,
491             m.mutText, statusToString(m.toStatus), m.rationale
492         ];
493         st.tbl.put(r);
494     }
495     return st;
496 }
497 
498 struct TestCaseOverlapStat {
499     import std.format : formattedWrite;
500     import std.range : put;
501     import dextool.hash;
502     import dextool.plugin.mutate.backend.database.type : TestCaseId;
503 
504     long overlap;
505     long total;
506     double ratio;
507 
508     // map between test cases and the mutants they have killed.
509     TestCaseId[][Murmur3] tc_mut;
510     // map between mutation IDs and the test cases that killed them.
511     long[][Murmur3] mutid_mut;
512     string[TestCaseId] name_tc;
513 
514     string sumToString() @safe const {
515         return format("%s/%s = %s test cases", overlap, total, ratio);
516     }
517 
518     void sumToString(Writer)(ref Writer w) @trusted const {
519         formattedWrite(w, "%s/%s = %s test cases\n", overlap, total, ratio);
520     }
521 
522     string toString() @safe const {
523         auto buf = appender!string;
524         toString(buf);
525         return buf.data;
526     }
527 
528     void toString(Writer)(ref Writer w) @safe const {
529         sumToString(w);
530 
531         foreach (tcs; tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
532             bool first = true;
533             // TODO this is a bit slow. use a DB row iterator instead.
534             foreach (name; tcs.value.map!(id => name_tc[id])) {
535                 if (first) {
536                     () @trusted {
537                         formattedWrite(w, "%s %s\n", name, mutid_mut[tcs.key].length);
538                     }();
539                     first = false;
540                 } else {
541                     () @trusted { formattedWrite(w, "%s\n", name); }();
542                 }
543             }
544             put(w, "\n");
545         }
546     }
547 }
548 
549 /** Report test cases that completly overlap each other.
550  *
551  * Returns: a string with statistics.
552  */
553 template toTable(Flag!"colWithMutants" colMutants) {
554     static if (colMutants) {
555         alias TableT = Table!3;
556     } else {
557         alias TableT = Table!2;
558     }
559     alias RowT = TableT.Row;
560 
561     void toTable(ref TestCaseOverlapStat st, ref TableT tbl) {
562         foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
563             bool first = true;
564             // TODO this is a bit slow. use a DB row iterator instead.
565             foreach (name; tcs.value.map!(id => st.name_tc[id])) {
566                 RowT r;
567                 r[0] = name;
568                 if (first) {
569                     auto muts = st.mutid_mut[tcs.key];
570                     r[1] = muts.length.to!string;
571                     static if (colMutants) {
572                         r[2] = format("%-(%s,%)", muts);
573                     }
574                     first = false;
575                 }
576 
577                 tbl.put(r);
578             }
579             static if (colMutants)
580                 RowT r = ["", "", ""];
581             else
582                 RowT r = ["", ""];
583             tbl.put(r);
584         }
585     }
586 }
587 
588 /// Test cases that kill exactly the same mutants.
589 TestCaseOverlapStat reportTestCaseFullOverlap(ref Database db, const Mutation.Kind[] kinds) @safe {
590     import dextool.hash;
591     import dextool.plugin.mutate.backend.database.type : TestCaseId;
592 
593     auto profile = Profile(ReportSection.tc_full_overlap);
594 
595     TestCaseOverlapStat st;
596     st.total = db.getNumOfTestCases;
597 
598     foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) {
599         auto muts = db.getTestCaseMutantKills(tc_id, kinds).sort.map!(a => cast(long) a).array;
600         auto m3 = makeMurmur3(cast(ubyte[]) muts);
601         if (auto v = m3 in st.tc_mut)
602             (*v) ~= tc_id;
603         else {
604             st.tc_mut[m3] = [tc_id];
605             st.mutid_mut[m3] = muts;
606         }
607         st.name_tc[tc_id] = db.getTestCaseName(tc_id);
608     }
609 
610     foreach (tcs; st.tc_mut.byKeyValue.filter!(a => a.value.length > 1)) {
611         st.overlap += tcs.value.count;
612     }
613 
614     if (st.total > 0)
615         st.ratio = cast(double) st.overlap / cast(double) st.total;
616 
617     return st;
618 }
619 
620 class TestGroupSimilarity {
621     static struct TestGroup {
622         string description;
623         string name;
624 
625         /// What the user configured as regex. Useful when e.g. generating reports
626         /// for a user.
627         string userInput;
628 
629         int opCmp(ref const TestGroup s) const {
630             return cmp(name, s.name);
631         }
632     }
633 
634     static struct Similarity {
635         /// The test group that the `key` is compared to.
636         TestGroup comparedTo;
637         /// How similare the `key` is to `comparedTo`.
638         double similarity;
639         /// Mutants that are similare between `testCase` and the parent.
640         MutationId[] intersection;
641         /// Unique mutants that are NOT verified by `testCase`.
642         MutationId[] difference;
643     }
644 
645     Similarity[][TestGroup] similarities;
646 }
647 
648 /** Analyze the similarity between the test groups.
649  *
650  * Assuming that a limit on how many test groups to report isn't interesting
651  * because they are few so it is never a problem.
652  *
653  */
654 TestGroupSimilarity reportTestGroupsSimilarity(ref Database db,
655         const(Mutation.Kind)[] kinds, const(TestGroup)[] test_groups) @safe {
656     import dextool.plugin.mutate.backend.database.type : TestCaseInfo, TestCaseId;
657 
658     auto profile = Profile(ReportSection.tc_groups_similarity);
659 
660     alias TgKills = Tuple!(TestGroupSimilarity.TestGroup, "testGroup", MutationId[], "kills");
661 
662     const test_cases = spinSql!(() { return db.getDetectedTestCaseIds; }).map!(
663             a => Tuple!(TestCaseId, "id", TestCase, "tc")(a, spinSql!(() {
664                 return db.getTestCase(a);
665             }))).array;
666 
667     MutationId[] gatherKilledMutants(const(TestGroup) tg) {
668         auto kills = appender!(MutationId[])();
669         foreach (tc; test_cases.filter!(a => a.tc.isTestCaseInTestGroup(tg.re))) {
670             kills.put(spinSql!(() {
671                     return db.getTestCaseMutantKills(tc.id, kinds);
672                 }));
673         }
674         return kills.data;
675     }
676 
677     TgKills[] test_group_kills;
678     foreach (const tg; test_groups) {
679         auto kills = gatherKilledMutants(tg);
680         if (kills.length != 0)
681             test_group_kills ~= TgKills(TestGroupSimilarity.TestGroup(tg.description,
682                     tg.name, tg.userInput), kills);
683     }
684 
685     // calculate similarity between all test groups.
686     auto rval = new typeof(return);
687 
688     foreach (tg_parent; test_group_kills) {
689         auto app = appender!(TestGroupSimilarity.Similarity[])();
690         foreach (tg_other; test_group_kills.filter!(a => a.testGroup != tg_parent.testGroup)) {
691             auto similarity = setSimilarity(tg_parent.kills, tg_other.kills);
692             if (similarity.similarity > 0)
693                 app.put(TestGroupSimilarity.Similarity(tg_other.testGroup,
694                         similarity.similarity, similarity.intersection, similarity.difference));
695             if (app.data.length != 0)
696                 rval.similarities[tg_parent.testGroup] = app.data;
697         }
698     }
699 
700     return rval;
701 }
702 
703 class TestGroupStat {
704     import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo;
705 
706     /// Human readable description for the test group.
707     string description;
708     /// Statistics for a test group.
709     MutationStat stats;
710     /// Map between test cases and their test group.
711     TestCase[] testCases;
712     /// Lookup for converting a id to a filename
713     Path[FileId] files;
714     /// Mutants alive in a file.
715     MutantInfo[][FileId] alive;
716     /// Mutants killed in a file.
717     MutantInfo[][FileId] killed;
718 }
719 
720 import std.regex : Regex;
721 
722 private bool isTestCaseInTestGroup(const TestCase tc, const Regex!char tg) {
723     import std.regex : matchFirst;
724 
725     auto m = matchFirst(tc.name, tg);
726     // the regex must match the full test case thus checking that
727     // nothing is left before or after
728     if (!m.empty && m.pre.length == 0 && m.post.length == 0) {
729         return true;
730     }
731     return false;
732 }
733 
734 TestGroupStat reportTestGroups(ref Database db, const(Mutation.Kind)[] kinds,
735         const(TestGroup) test_g) @safe {
736     import dextool.plugin.mutate.backend.database : MutationStatusId;
737     import dextool.set;
738 
739     auto profile = Profile(ReportSection.tc_groups);
740 
741     static struct TcStat {
742         Set!MutationStatusId alive;
743         Set!MutationStatusId killed;
744         Set!MutationStatusId timeout;
745         Set!MutationStatusId total;
746 
747         // killed by the specific test case
748         Set!MutationStatusId tcKilled;
749     }
750 
751     auto r = new TestGroupStat;
752     r.description = test_g.description;
753     TcStat tc_stat;
754 
755     // map test cases to this test group
756     foreach (tc; db.getDetectedTestCases) {
757         if (tc.isTestCaseInTestGroup(test_g.re))
758             r.testCases ~= tc;
759     }
760 
761     // collect mutation statistics for each test case group
762     foreach (const tc; r.testCases) {
763         foreach (const id; db.testCaseMutationPointAliveSrcMutants(kinds, tc))
764             tc_stat.alive.add(id);
765         foreach (const id; db.testCaseMutationPointKilledSrcMutants(kinds, tc))
766             tc_stat.killed.add(id);
767         foreach (const id; db.testCaseMutationPointTimeoutSrcMutants(kinds, tc))
768             tc_stat.timeout.add(id);
769         foreach (const id; db.testCaseMutationPointTotalSrcMutants(kinds, tc))
770             tc_stat.total.add(id);
771         foreach (const id; db.testCaseKilledSrcMutants(kinds, tc))
772             tc_stat.tcKilled.add(id);
773     }
774 
775     // update the mutation stat for the test group
776     r.stats.alive = tc_stat.alive.length;
777     r.stats.killed = tc_stat.killed.length;
778     r.stats.timeout = tc_stat.timeout.length;
779     r.stats.total = tc_stat.total.length;
780 
781     // associate mutants with their file
782     foreach (const m; db.getMutantsInfo(kinds, tc_stat.tcKilled.toArray)) {
783         auto fid = db.getFileId(m.id);
784         r.killed[fid.get] ~= m;
785 
786         if (fid.get !in r.files) {
787             r.files[fid.get] = Path.init;
788             r.files[fid.get] = db.getFile(fid.get);
789         }
790     }
791 
792     foreach (const m; db.getMutantsInfo(kinds, tc_stat.alive.toArray)) {
793         auto fid = db.getFileId(m.id);
794         r.alive[fid.get] ~= m;
795 
796         if (fid.get !in r.files) {
797             r.files[fid.get] = Path.init;
798             r.files[fid.get] = db.getFile(fid.get);
799         }
800     }
801 
802     return r;
803 }
804 
805 /// High interest mutants.
806 class MutantSample {
807     import dextool.plugin.mutate.backend.database : MutationId, FileId, MutantInfo,
808         MutationStatus, MutationStatusId, MutationEntry, MutationStatusTime;
809 
810     MutationEntry[MutationStatusId] mutants;
811 
812     /// The mutant that had its status updated the furthest back in time.
813     MutationStatusTime[] oldest;
814 
815     /// The mutant that has survived the longest in the system.
816     MutationStatus[] hardestToKill;
817 
818     /// The latest mutants that where added and survived.
819     MutationStatusTime[] latest;
820 }
821 
822 /// Returns: samples of mutants that are of high interest to the user.
823 MutantSample reportSelectedAliveMutants(ref Database db,
824         const(Mutation.Kind)[] kinds, long history_nr) {
825     auto profile = Profile(ReportSection.mut_recommend_kill);
826 
827     auto rval = new typeof(return);
828 
829     rval.hardestToKill = db.getHardestToKillMutant(kinds, Mutation.Status.alive, history_nr);
830     foreach (const mutst; rval.hardestToKill) {
831         auto ids = db.getMutationIds(kinds, [mutst.statusId]);
832         if (ids.length != 0)
833             rval.mutants[mutst.statusId] = db.getMutation(ids[0]);
834     }
835 
836     rval.oldest = db.getOldestMutants(kinds, history_nr);
837     foreach (const mutst; rval.oldest) {
838         auto ids = db.getMutationIds(kinds, [mutst.id]);
839         if (ids.length != 0)
840             rval.mutants[mutst.id] = db.getMutation(ids[0]);
841     }
842 
843     return rval;
844 }
845 
846 class DiffReport {
847     import dextool.plugin.mutate.backend.database : FileId, MutantInfo;
848     import dextool.plugin.mutate.backend.diff_parser : Diff;
849 
850     /// The mutation score.
851     double score;
852 
853     /// The raw diff for a file
854     Diff.Line[][FileId] rawDiff;
855 
856     /// Lookup for converting a id to a filename
857     Path[FileId] files;
858     /// Mutants alive in a file.
859     MutantInfo[][FileId] alive;
860     /// Mutants killed in a file.
861     MutantInfo[][FileId] killed;
862     /// Test cases that killed mutants.
863     TestCase[] testCases;
864 
865     override string toString() @safe const {
866         import std.format : formattedWrite;
867         import std.range : put;
868 
869         auto w = appender!string;
870 
871         foreach (file; files.byKeyValue) {
872             put(w, file.value);
873             foreach (mut; alive[file.key])
874                 formattedWrite(w, "  %s\n", mut);
875             foreach (mut; killed[file.key])
876                 formattedWrite(w, "  %s\n", mut);
877         }
878 
879         formattedWrite(w, "Test Cases killing mutants");
880         foreach (tc; testCases)
881             formattedWrite(w, "  %s", tc);
882 
883         return w.data;
884     }
885 }
886 
887 DiffReport reportDiff(ref Database db, const(Mutation.Kind)[] kinds,
888         ref Diff diff, AbsolutePath workdir) {
889     import dextool.plugin.mutate.backend.database : MutationId, MutationStatusId;
890     import dextool.plugin.mutate.backend.type : SourceLoc;
891     import dextool.set;
892 
893     auto profile = Profile(ReportSection.diff);
894 
895     auto rval = new DiffReport;
896 
897     Set!MutationStatusId total;
898     // used for deriving what test cases killed mutants in the diff.
899     Set!MutationId killing_mutants;
900 
901     foreach (kv; diff.toRange(workdir)) {
902         auto fid = db.getFileId(kv.key);
903         if (fid.isNull) {
904             logger.warning("This file in the diff has not been tested thus skipping it: ", kv.key);
905             continue;
906         }
907 
908         bool has_mutants;
909         foreach (id; kv.value
910                 .toRange
911                 .map!(line => spinSql!(() => db.getMutationsOnLine(kinds,
912                     fid.get, SourceLoc(line))))
913                 .joiner
914                 .filter!(id => id !in total)) {
915             has_mutants = true;
916             total.add(id);
917 
918             const info = db.getMutantsInfo(kinds, [id])[0];
919             if (info.status == Mutation.Status.alive) {
920                 rval.alive[fid.get] ~= info;
921             } else {
922                 rval.killed[fid.get] ~= info;
923                 killing_mutants.add(info.id);
924             }
925         }
926 
927         if (has_mutants) {
928             rval.files[fid.get] = kv.key;
929             rval.rawDiff[fid.get] = diff.rawDiff[kv.key];
930         } else {
931             logger.info("This file in the diff has no mutants on changed lines: ", kv.key);
932         }
933     }
934 
935     Set!TestCase test_cases;
936     foreach (tc; killing_mutants.toRange.map!(a => db.getTestCases(a)).joiner)
937         test_cases.add(tc);
938 
939     rval.testCases = test_cases.toArray.sort.array;
940 
941     if (total.length == 0) {
942         rval.score = 1.0;
943     } else {
944         rval.score = cast(double) killing_mutants.length / cast(double) total.length;
945     }
946 
947     return rval;
948 }
949 
950 struct MinimalTestSet {
951     import dextool.plugin.mutate.backend.database.type : TestCaseInfo;
952 
953     long total;
954 
955     /// Minimal set that achieve the mutation test score.
956     TestCase[] minimalSet;
957     /// Test cases that do not contribute to the mutation test score.
958     TestCase[] redundant;
959     /// Map between test case name and sum of all the test time of the mutants it killed.
960     TestCaseInfo[string] testCaseTime;
961 }
962 
963 MinimalTestSet reportMinimalSet(ref Database db, const Mutation.Kind[] kinds) {
964     import dextool.plugin.mutate.backend.database : TestCaseId, TestCaseInfo;
965     import dextool.set;
966 
967     auto profile = Profile(ReportSection.tc_min_set);
968 
969     alias TcIdInfo = Tuple!(TestCase, "tc", TestCaseId, "id", TestCaseInfo, "info");
970 
971     MinimalTestSet rval;
972 
973     Set!MutationId killedMutants;
974 
975     // start by picking test cases that have the fewest kills.
976     foreach (const val; db.getDetectedTestCases
977             .map!(a => tuple(a, db.getTestCaseId(a)))
978             .filter!(a => !a[1].isNull)
979             .map!(a => TcIdInfo(a[0], a[1], db.getTestCaseInfo(a[0], kinds)))
980             .filter!(a => a.info.killedMutants != 0)
981             .array
982             .sort!((a, b) => a.info.killedMutants < b.info.killedMutants)) {
983         rval.testCaseTime[val.tc.name] = val.info;
984 
985         const killed = killedMutants.length;
986         foreach (const id; db.getTestCaseMutantKills(val.id, kinds)) {
987             killedMutants.add(id);
988         }
989 
990         if (killedMutants.length > killed)
991             rval.minimalSet ~= val.tc;
992         else
993             rval.redundant ~= val.tc;
994     }
995 
996     rval.total = rval.minimalSet.length + rval.redundant.length;
997 
998     return rval;
999 }
1000 
1001 struct TestCaseUniqueness {
1002     MutationId[][TestCase] uniqueKills;
1003 
1004     // test cases that have no unique kills. These are candidates for being
1005     // refactored/removed.
1006     TestCase[] noUniqueKills;
1007 }
1008 
1009 /// Returns: a report of the mutants that a test case is the only one that kills.
1010 TestCaseUniqueness reportTestCaseUniqueness(ref Database db, const Mutation.Kind[] kinds) {
1011     import dextool.plugin.mutate.backend.database.type : TestCaseId;
1012     import dextool.set;
1013 
1014     auto profile = Profile(ReportSection.tc_unique);
1015 
1016     /// any time a mutant is killed by more than one test case it is removed.
1017     TestCaseId[MutationId] killedBy;
1018     Set!MutationId blacklist;
1019 
1020     foreach (tc_id; db.getTestCasesWithAtLeastOneKill(kinds)) {
1021         auto muts = db.getTestCaseMutantKills(tc_id, kinds);
1022         foreach (m; muts.filter!(a => !blacklist.contains(a))) {
1023             if (m in killedBy) {
1024                 killedBy.remove(m);
1025                 blacklist.add(m);
1026             } else {
1027                 killedBy[m] = tc_id;
1028             }
1029         }
1030     }
1031 
1032     // use a cache to reduce the database access
1033     TestCase[TestCaseId] idToTc;
1034     TestCase getTestCase(TestCaseId id) @trusted {
1035         return idToTc.require(id, spinSql!(() { return db.getTestCase(id).get; }));
1036     }
1037 
1038     typeof(return) rval;
1039     Set!TestCaseId uniqueTc;
1040     foreach (kv; killedBy.byKeyValue) {
1041         rval.uniqueKills[getTestCase(kv.value)] ~= kv.key;
1042         uniqueTc.add(kv.value);
1043     }
1044     foreach (tc_id; db.getDetectedTestCaseIds.filter!(a => !uniqueTc.contains(a))) {
1045         rval.noUniqueKills ~= getTestCase(tc_id);
1046     }
1047 
1048     return rval;
1049 }
1050 
1051 private:
1052 
1053 /** Measure how long a report takes to generate and print it as trace data.
1054  *
1055  * This is an example from clang-tidy for how it could be reported to the user.
1056  * For now it is *just* reported as it is running.
1057  *
1058  * ===-------------------------------------------------------------------------===
1059  *                           clang-tidy checks profiling
1060  * ===-------------------------------------------------------------------------===
1061  *   Total Execution Time: 0.0021 seconds (0.0021 wall clock)
1062  *
1063  *    ---User Time---   --System Time--   --User+System--   ---Wall Time---  --- Name ---
1064  *    0.0000 (  0.1%)   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0000 (  0.1%)  readability-misplaced-array-index
1065  *    0.0000 (  0.2%)   0.0000 (  0.0%)   0.0000 (  0.1%)   0.0000 (  0.1%)  abseil-duration-division
1066  *    0.0012 (100.0%)   0.0009 (100.0%)   0.0021 (100.0%)   0.0021 (100.0%)  Total
1067  */
1068 struct Profile {
1069     import std.datetime.stopwatch : StopWatch;
1070 
1071     ReportSection kind;
1072     StopWatch sw;
1073 
1074     this(ReportSection kind) @safe nothrow @nogc {
1075         this.kind = kind;
1076         sw.start;
1077     }
1078 
1079     ~this() @safe nothrow {
1080         try {
1081             sw.stop;
1082             logger.tracef("profiling:%s wall time:%s", kind, sw.peek);
1083         } catch (Exception e) {
1084         }
1085     }
1086 }