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