1 /**
2 Copyright: Copyright (c) 2017, 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 is responsible for converting the users CLI arguments to
11 configuration of how the mutation plugin should behave.
12 */
13 module dextool.plugin.mutate.frontend.argparser;
14 
15 import core.time : dur;
16 import logger = std.experimental.logger;
17 import std.algorithm : joiner, sort, map, filter;
18 import std.array : empty, array, appender;
19 import std.exception : collectException;
20 import std.parallelism : totalCPUs;
21 import std.path : buildPath;
22 import std.traits : EnumMembers;
23 
24 import my.filter : GlobFilter;
25 import my.optional;
26 import my.path;
27 
28 import toml : TOMLDocument;
29 
30 public import dextool.plugin.mutate.backend : Mutation, Language;
31 public import dextool.plugin.mutate.type;
32 import dextool.plugin.mutate.config;
33 import dextool.type : ExitStatusType;
34 
35 version (unittest) {
36     import unit_threaded.assertions;
37 }
38 
39 @safe:
40 
41 // by default use the recommended operators. These are a good default that test
42 // those with relatively few equivalent mutants thus most that survive are
43 // relevant.
44 private immutable MutationKind[] defaultMutants = [
45     MutationKind.lcr, MutationKind.lcrb, MutationKind.sdl, MutationKind.uoi,
46     MutationKind.dcr
47 ];
48 
49 /// Extract and cleanup user input from the command line.
50 struct ArgParser {
51     import std.typecons : Nullable;
52     import std.conv : ConvException;
53     import std.getopt : GetoptResult, getopt, defaultGetoptPrinter;
54 
55     /// Minimal data needed to bootstrap the configuration.
56     MiniConfig miniConf;
57 
58     ConfigAdmin admin;
59     ConfigAnalyze analyze;
60     ConfigCompileDb compileDb;
61     ConfigCompiler compiler;
62     ConfigMutationTest mutationTest;
63     ConfigReport report;
64     ConfigWorkArea workArea;
65     ConfigGenerate generate;
66     ConfigCoverage coverage;
67     ConfigSchema schema;
68 
69     struct Data {
70         AbsolutePath db;
71         ExitStatusType exitStatus = ExitStatusType.Ok;
72         MutationKind[] mutation = defaultMutants;
73         ToolMode toolMode;
74         bool help;
75         string[] inFiles;
76     }
77 
78     Data data;
79     alias data this;
80 
81     private GetoptResult help_info;
82 
83     alias GroupF = void delegate(string[]) @system;
84     GroupF[string] groups;
85 
86     /// Returns: a config object with default values.
87     static ArgParser make() @safe {
88         import dextool.compilation_db : defaultCompilerFlagFilter, CompileCommandFilter;
89 
90         ArgParser r;
91         r.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
92         return r;
93     }
94 
95     /// Convert the configuration to a TOML file.
96     string toTOML() @trusted {
97         import std.ascii : newline;
98         import std.conv : to;
99         import std.format : format;
100         import std.utf : toUTF8;
101 
102         auto app = appender!(string[])();
103 
104         app.put("[workarea]");
105         app.put(null);
106         app.put("# base path (absolute or relative) to look for C/C++ files to mutate.");
107         app.put(`root = "."`);
108         app.put(null);
109         app.put("# only those files that fully match the glob filter will be mutated.");
110         app.put("# glob filter are relative to root.");
111         app.put(`include = ["*"]`);
112         app.put("exclude = []");
113         app.put(null);
114 
115         app.put("[generic]");
116         app.put(null);
117         app.put("# default mutants to test if none is specified by --mutant");
118         app.put(
119                 "# note that this affects the analyze phase thus the --mutant argument must be one of those specified here");
120         app.put(format!"# available options are: [%(%s, %)]"(
121                 [EnumMembers!MutationKind].map!(a => a.to!string)));
122         app.put(format("mutants = [%(%s, %)]", defaultMutants.map!(a => a.to!string)));
123         app.put(null);
124 
125         app.put("[analyze]");
126         app.put(null);
127         app.put(
128                 "# glob filter which matched files (relative to root) to be used for static analysis information.");
129         app.put(`include = ["*"]`);
130         app.put("exclude = []");
131         app.put(null);
132         app.put("# number of threads to be used for analysis (default is the number of cores).");
133         app.put(format!"# threads = %s"(totalCPUs));
134         app.put(null);
135         app.put("# remove files from the database that are no longer found during analysis.");
136         app.put(`prune = true`);
137         app.put(null);
138         app.put("# checksum the files in this directories and warn if a mutation status is older");
139         app.put("# than the newest file. The path can be a file or a directory. Directories");
140         app.put("# are traveresed. All paths are relative to root.");
141         app.put(`# test_paths = ["test/suite1", "test/mytest.cpp"]`);
142         app.put(null);
143         app.put(
144                 "# glob filter which include/exclude matched test files (relative to root) from analysis.");
145         app.put(`# test_include = ["*/*.ext"]`);
146         app.put("# test_exclude = []");
147         app.put(null);
148 
149         app.put("[schema]");
150         app.put(null);
151         app.put("# Use scheman to reduce the compile+link time");
152         app.put("use = true");
153         app.put(null);
154         app.put("# how to add the schema runtime to the SUT");
155         app.put(format!"# available options are: %(%s, %)"(
156                 [EnumMembers!SchemaRuntime].map!(a => a.to!string)));
157         app.put("# runtime = inject");
158         app.put(null);
159         app.put(
160                 "# Default is to inject the runtime in all roots. A root is a file either provided by --in");
161         app.put("# or a file in compile_commands.json.");
162         app.put(
163                 "# If specified then the coverage and schemata runtime is only injected in these files.");
164         app.put("# paths are relative to root.");
165         app.put(`# inject_runtime_impl = [["file1.c", "c"], ["file2.c", "cpp"]]`);
166         app.put(null);
167         app.put("# minimum number of mutants per schema.");
168         app.put(format!"# min_mutants_per_schema = %s"(schema.minMutantsPerSchema.get));
169         app.put(null);
170         app.put("# maximum number of mutants per schema (zero means no limit).");
171         app.put(format!"# mutants_per_schema = %s"(schema.mutantsPerSchema.get));
172         app.put(null);
173         app.put("# sanity check the schemata before it is used by executing the test cases");
174         app.put("# it is a slowdown but nice robustness that is usually worth having");
175         app.put("check_schemata = true");
176         app.put(null);
177 
178         app.put("[coverage]");
179         app.put(null);
180         app.put("# Use coverage to reduce the tested mutants");
181         app.put("use = true");
182         app.put(null);
183         app.put("# how to add the coverage runtime to the SUT");
184         app.put(format!"# available options are: %(%s, %)"(
185                 [EnumMembers!CoverageRuntime].map!(a => a.to!string)));
186         app.put("# runtime = inject");
187         app.put(null);
188         app.put(
189                 "# Default is to inject the runtime in all roots. A root is a file either provided by --in");
190         app.put("# or a file in compile_commands.json.");
191         app.put(
192                 "# If specified then the coverage and schemata runtime is only injected in these files.");
193         app.put("# paths are relative to root.");
194         app.put(`# inject_runtime_impl = [["file1.c", "c"], ["file2.c", "cpp"]]`);
195         app.put(null);
196 
197         app.put("[database]");
198         app.put(null);
199         app.put("# path (absolute or relative) where mutation statistics will be stored.");
200         app.put(`db = "dextool_mutate.sqlite3"`);
201         app.put(null);
202 
203         app.put("[compiler]");
204         app.put(null);
205         app.put("# extra flags to pass on to the compiler such as the C++ standard.");
206         app.put(format(`# extra_flags = [%(%s, %)]`, compiler.extraFlags));
207         app.put(null);
208         app.put("# force system includes to use -I instead of -isystem");
209         app.put(format!"# force_system_includes = %s"(compiler.forceSystemIncludes));
210         app.put(null);
211         app.put("# system include paths to use instead of the ones in compile_commands.json");
212         app.put(format(`# use_compiler_system_includes = "%s"`, compiler.useCompilerSystemIncludes.length == 0
213                 ? "/path/to/c++" : compiler.useCompilerSystemIncludes.value));
214         app.put(null);
215         app.put("# allow compilation errors.");
216         app.put("# This is useful to active when clang complain about e.g. gcc specific builtins");
217         app.put("# allow_errors = true");
218         app.put(null);
219 
220         app.put("[compile_commands]");
221         app.put(null);
222         app.put("# files and/or directories to look for compile_commands.json.");
223         if (compileDb.dbs.length == 0)
224             app.put(`search_paths = ["./compile_commands.json"]`);
225         else
226             app.put(format("search_paths = %s", compileDb.rawDbs));
227         app.put(null);
228         app.put("# compile flags to remove when analyzing a file.");
229         app.put(format("# filter = [%(%s, %)]", compileDb.flagFilter.filter));
230         app.put(null);
231         app.put("# number of compiler arguments to skip from the beginning (needed when the first argument is NOT a compiler but rather a wrapper).");
232         app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs));
233         app.put(null);
234 
235         app.put("[mutant_test]");
236         app.put(null);
237         app.put("# command to build the program **and** test suite.");
238         app.put(format!`build_cmd = ["cd build && make -j%s"]`(totalCPUs));
239         app.put(null);
240         app.put("# at least one of test_cmd_dir (recommended) or test_cmd needs to be specified.");
241         app.put(null);
242         app.put(`# path(s) to recursively look for test binaries to execute.`);
243         app.put(`test_cmd_dir = ["./build/test"]`);
244         app.put(null);
245         app.put(`# flags to add to all executables found in test_cmd_dir.`);
246         app.put(`# test_cmd_dir_flag = ["--gtest_filter", "-foo*"]`);
247         app.put(null);
248         app.put("# command(s) to test the program.");
249         app.put("# the arguments for test_cmd can be an array of multiple test commands");
250         app.put(`# 1. ["test1.sh", "test2.sh"]`);
251         app.put(`# 2. [["test1.sh", "-x"], "test2.sh"]`);
252         app.put(`# 3. [["/bin/make", "test"]]`);
253         app.put(`# test_cmd = ["./test.sh"]`);
254         app.put(null);
255         app.put(
256                 "# timeout to use for the test suite (by default a measurement-based heuristic will be used).");
257         app.put(`# test_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`);
258         app.put(null);
259         app.put("# timeout to use when compiling the program and test suite (default: 30 minutes)");
260         app.put(`# build_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`);
261         app.put(null);
262         app.put(
263                 "# program used to analyze the output from the test suite for test cases that killed the mutant");
264         app.put(`# analyze_cmd = "analyze.sh"`);
265         app.put(null);
266         app.put("# built-in analyzer of output from testing frameworks to find failing test cases");
267         app.put(format("# available options are: [%(%s, %)]",
268                 [EnumMembers!TestCaseAnalyzeBuiltin].map!(a => a.to!string)));
269         app.put(`analyze_using_builtin = ["test_cmd"]`);
270         app.put(null);
271         app.put("# determine in what order mutants are chosen");
272         app.put(format("# available options are: %(%s %)",
273                 [EnumMembers!MutationOrder].map!(a => a.to!string)));
274         app.put(format(`# order = "%s"`, MutationOrder.bySize));
275         app.put(null);
276         app.put("# how to behave when new test cases are found");
277         app.put(format("# available options are: %(%s %)",
278                 [EnumMembers!(ConfigMutationTest.NewTestCases)].map!(a => a.to!string)));
279         app.put(`detected_new_test_case = "resetAlive"`);
280         app.put(null);
281         app.put("# how to behave when test cases are detected as having been removed");
282         app.put("# should the test and the gathered statistics be removed too?");
283         app.put(format("# available options are: %(%s %)",
284                 [EnumMembers!(ConfigMutationTest.RemovedTestCases)].map!(a => a.to!string)));
285         app.put(`detected_dropped_test_case = "remove"`);
286         app.put(null);
287         app.put("# how the oldest mutants should be treated.");
288         app.put("# It is recommended to test them again.");
289         app.put("# Because you may have changed the test suite so mutants that where previously killed by the test suite now survive.");
290         app.put(format!"# available options are: %(%s %)"(
291                 [EnumMembers!(ConfigMutationTest.OldMutant)].map!(a => a.to!string)));
292         app.put(`oldest_mutants = "test"`);
293         app.put(null);
294         app.put("# how many of the oldest mutants to do the above with");
295         app.put("# oldest_mutants_nr = 10");
296         app.put("# how many of the oldest mutants to do the above with");
297         app.put("oldest_mutants_percentage = 1.0");
298         app.put(null);
299         app.put(
300                 "# number of threads to be used when running tests in parallel (default is the number of cores).");
301         app.put(format!"# parallel_test = %s"(totalCPUs));
302         app.put(null);
303         app.put("# stop executing tests as soon as a test command fails.");
304         app.put(
305                 "# This speed up the test phase but the report of test cases killing mutants is less accurate");
306         app.put("use_early_stop = true");
307         app.put(null);
308         app.put(`# Enable continues sanity check of the build environment and test suite.`);
309         app.put(
310                 `# Run the test suite every 100 mutant to see that the test suite is OK when no mutants are injected.`);
311         app.put(
312                 `# If the test suite fails the previous 100 mutants will be reverted and mutation testing stops.`);
313         app.put(`continues_check_test_suite = true`);
314         app.put(null);
315         app.put("# How often the test suite check is performed");
316         app.put(format!`continues_check_test_suite_period = %s`(
317                 mutationTest.contCheckTestSuitePeriod.get));
318         app.put(null);
319         app.put("# Compare the checksum of the test binaries with and without a mutant injected to determine which test binaries need to be executed.");
320         app.put(
321                 "# Requires that all `test_cmd`s are binaries, it can not be e.g. make test or scripts.");
322         app.put("# test_cmd_checksum = true");
323         app.put(null);
324         app.put("# Max output to capture from a test case. Useful when e.g. a test case goes into an infinite loop and spew out Gbyte of text.");
325         app.put("# Unit is Mbyte");
326         app.put("# max_test_cmd_output = 10");
327         app.put(null);
328         app.put("# Kill test_cmd's if the total used memory goes above this limit.");
329         app.put("# This avoids the host from running out of memory.");
330         app.put("# Killed tests are tagged as timeout thus they will be re-tested");
331         app.put("max_mem_usage_percentage = 90.0");
332         app.put(null);
333 
334         app.put("[report]");
335         app.put(null);
336         app.put("# default style to use");
337         app.put(format("# available options are: %(%s %)",
338                 [EnumMembers!ReportKind].map!(a => a.to!string)));
339         app.put(format!`style = "%s"`(report.reportKind));
340         app.put(null);
341         app.put("# default report sections when no --section is specified");
342         app.put(format!"# available options are: [%(%s, %)]"(
343                 [EnumMembers!ReportSection].map!(a => a.to!string)));
344         app.put(format!"sections = [%(%s, %)]"(report.reportSection.map!(a => a.to!string)));
345         app.put(null);
346         app.put("# how many mutants to show in the high interest section");
347         app.put("# high_interest_mutants_nr = 5");
348         app.put(null);
349 
350         app.put("[test_group]");
351         app.put(null);
352         app.put("# subgroups with a description and pattern. Example:");
353         app.put("# [test_group.uc1]");
354         app.put(`# description = "use case 1"`);
355         app.put(`# pattern = "uc_1.*"`);
356         app.put(`# see for regex syntax: http://dlang.org/phobos/std_regex.html`);
357         app.put(null);
358 
359         app.put("[test]");
360         app.put(null);
361         app.put("# Path to a JSON file containing metadata about tests.");
362         app.put(`# metadata = "test_data.json"`);
363 
364         return app.data.joiner(newline).toUTF8;
365     }
366 
367     void parse(string[] args) {
368         import std.format : format;
369 
370         static import std.getopt;
371 
372         const db_help = "sqlite3 database to use (default: dextool_mutate.sqlite3)";
373         const include_help = "only mutate the files matching at least one of the patterns (default: *)";
374         const exclude_help = "do not mutate the files matching any the patterns (default: <empty>)";
375         const out_help = "path used as the root for mutation/reporting of files (default: .)";
376         const conf_help = "load configuration (default: .dextool_mutate.toml)";
377 
378         // specified by command line. if set it overwride the one from the config.
379         MutationKind[] mutants;
380 
381         // not used but need to be here. The one used is in MiniConfig.
382         string conf_file;
383         string db = data.db;
384 
385         void analyzerG(string[] args) {
386             bool noPrune;
387             string[] compileDbs;
388 
389             data.toolMode = ToolMode.analyzer;
390             // dfmt off
391             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
392                    "allow-errors", "allow compilation errors during analysis (default: false)", compiler.allowErrors.getPtr,
393                    "compile-db", "Retrieve compilation parameters from the file", &compileDbs,
394                    "c|config", conf_help, &conf_file,
395                    "db", db_help, &db,
396                    "diff-from-stdin", "restrict testing to the mutants in the diff", &analyze.unifiedDiffFromStdin,
397                    "exclude", exclude_help, &workArea.rawExclude,
398                    "fast-db-store", "improve the write speed of the analyze result (may corrupt the database)", &analyze.fastDbStore,
399                    "file-exclude", "glob filter which exclude matched files (relative to root) from analysis (default: <empty>)", &analyze.rawExclude,
400                    "file-include", "glob filter which include matched files (relative to root) for analysis (default: *)", &analyze.rawInclude,
401                    "force-save", "force the result from the analyze to be saved", &analyze.forceSaveAnalyze,
402                    "in", "Input file to parse (default: all files in the compilation database)", &data.inFiles,
403                    "include", include_help, &workArea.rawInclude,
404                    "m|mutant", "kind of mutation save in the database " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants,
405                    "no-prune", "do not prune the database of files that aren't found during the analyze", &noPrune,
406                    "out", out_help, &workArea.rawRoot,
407                    "profile", "print performance profile for the analyzers that are part of the report", &analyze.profile,
408                    "schema-min-mutants", "mini number of mutants per schema", schema.minMutantsPerSchema.getPtr,
409                    "schema-mutants", "number of mutants per schema (soft upper limit)", schema.mutantsPerSchema.getPtr,
410                    "threads", "number of threads to use for analysing files (default: CPU cores available)", &analyze.poolSize,
411                    );
412             // dfmt on
413 
414             analyze.prune = !noPrune;
415             updateCompileDb(compileDb, compileDbs);
416         }
417 
418         void generateMutantG(string[] args) {
419             data.toolMode = ToolMode.generate_mutant;
420             // dfmt off
421             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
422                    "c|config", conf_help, &conf_file,
423                    "db", db_help, &db,
424                    "exclude", exclude_help, &workArea.rawExclude,
425                    "include", include_help, &workArea.rawInclude,
426                    "out", out_help, &workArea.rawRoot,
427                    std.getopt.config.required, "id", "mutate the source code as mutant ID", &generate.mutationId,
428                    );
429             // dfmt on
430         }
431 
432         void testMutantsG(string[] args) {
433             import std.datetime : Clock;
434 
435             bool noSkip;
436             int maxAlive = -1;
437             long mutationTesterRuntime;
438             string maxRuntime;
439             string mutationCompile;
440             string[] mutationTestCaseAnalyze;
441             string[] mutationTester;
442             string[] testConstraint;
443 
444             mutationTest.loadThreshold.get = totalCPUs + 1;
445 
446             data.toolMode = ToolMode.test_mutants;
447             // dfmt off
448             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
449                    "L", "restrict testing to the requested files and lines (<file>:<start>-<end>)", &testConstraint,
450                    "build-cmd", "program used to build the application", &mutationCompile,
451                    "cont-test-suite", "enable continues check of the test suite", mutationTest.contCheckTestSuite.getPtr,
452                    "cont-test-suite-period", "how often to check the test suite", mutationTest.contCheckTestSuitePeriod.getPtr,
453                    "c|config", conf_help, &conf_file,
454                    "db", db_help, &db,
455                    "diff-from-stdin", "restrict testing to the mutants in the diff", &mutationTest.unifiedDiffFromStdin,
456                    "dry-run", "do not write data to the filesystem", &mutationTest.dryRun,
457                    "exclude", exclude_help, &workArea.rawExclude,
458                    "include", include_help, &workArea.rawInclude,
459                    "load-behavior", "how to behave when the threshold is hit " ~ format("[%(%s|%)]", [EnumMembers!(ConfigMutationTest.LoadBehavior)]), &mutationTest.loadBehavior,
460                    "load-threshold", format!"the 15min loadavg threshold (default: %s)"(mutationTest.loadThreshold.get), mutationTest.loadThreshold.getPtr,
461                    "log-coverage", "write the instrumented coverage files to a separate file", &coverage.log,
462                    "max-alive", "stop after NR alive mutants is found (only effective with -L or --diff-from-stdin)", &maxAlive,
463                    "max-runtime", format("max time to run the mutation testing for (default: %s)", mutationTest.maxRuntime), &maxRuntime,
464                    "m|mutant", "kind of mutation to test " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants,
465                    "no-skipped", "do not skip mutants that are covered by others", &noSkip,
466                    "order", "determine in what order mutants are chosen " ~ format("[%(%s|%)]", [EnumMembers!MutationOrder]), &mutationTest.mutationOrder,
467                    "out", out_help, &workArea.rawRoot,
468                    "schema-check", "sanity check a schemata before it is used", &schema.sanityCheckSchemata,
469                    "schema-log", "write mutant schematan to a separate file for later inspection", &schema.log,
470                    "schema-min-mutants", "mini number of mutants per schema", schema.minMutantsPerSchema.getPtr,
471                    "schema-only", "stop testing after the last schema has been executed", &schema.stopAfterLastSchema,
472                    "schema-train", "train the schema generator by only compiling the scheman", schema.onlyCompile.getPtr,
473                    "schema-use", "use schematas to speed-up testing", &schema.use,
474                    "test-case-analyze-builtin", "builtin analyzer of output from testing frameworks to find failing test cases", &mutationTest.mutationTestCaseBuiltin,
475                    "test-case-analyze-cmd", "program used to find what test cases killed the mutant", &mutationTestCaseAnalyze,
476                    "test-cmd", "program used to run the test suite", &mutationTester,
477                    "test-cmd-checksum", "use checksums of the original to avoid running unmodified tests", mutationTest.testCmdChecksum.getPtr,
478                    "test-timeout", "timeout to use for the test suite (msecs)", &mutationTesterRuntime,
479                    "use-early-stop", "stop executing tests for a mutant as soon as one kill a mutant to speed-up testing", &mutationTest.useEarlyTestCmdStop,
480                    );
481             // dfmt on
482 
483             if (maxAlive > 0)
484                 mutationTest.maxAlive = maxAlive;
485             if (mutationTester.length != 0)
486                 mutationTest.mutationTester = mutationTester.map!(a => ShellCommand([
487                             a
488                         ])).array;
489             if (mutationCompile.length != 0)
490                 mutationTest.mutationCompile = ShellCommand([mutationCompile]);
491             if (mutationTestCaseAnalyze.length != 0)
492                 mutationTest.mutationTestCaseAnalyze = mutationTestCaseAnalyze.map!(
493                         a => ShellCommand([a])).array;
494             if (mutationTesterRuntime != 0)
495                 mutationTest.mutationTesterRuntime = mutationTesterRuntime.dur!"msecs";
496             if (!maxRuntime.empty)
497                 mutationTest.maxRuntime = parseDuration(maxRuntime);
498             mutationTest.constraint = parseUserTestConstraint(testConstraint);
499             mutationTest.useSkipMutant.get = !noSkip;
500         }
501 
502         void reportG(string[] args) {
503             string[] compileDbs;
504             string logDir;
505 
506             data.toolMode = ToolMode.report;
507             ReportSection[] sections;
508             string metadataPath;
509 
510             // dfmt off
511             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
512                    "compile-db", "Retrieve compilation parameters from the file", &compileDbs,
513                    "c|config", conf_help, &conf_file,
514                    "db", db_help, &db,
515                    "diff-from-stdin", "report alive mutants in the areas indicated as changed in the diff", &report.unifiedDiff,
516                    "exclude", exclude_help, &workArea.rawExclude,
517                    "high-interest-mutants-nr", "nr of mutants to show in the section", report.highInterestMutantsNr.getPtr,
518                    "include", include_help, &workArea.rawInclude,
519                    "logdir", "Directory to write log files to (default: .)", &logDir,
520                    "m|mutant", "kind of mutation to report " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants,
521                    "out", out_help, &workArea.rawRoot,
522                    "profile", "print performance profile for the analyzers that are part of the report", &report.profile,
523                    "section", "sections to include in the report " ~ format("[%-(%s|%)]", [EnumMembers!ReportSection]), &sections,
524                    "section-tc_stat-num", "number of test cases to report", &report.tcKillSortNum,
525                    "section-tc_stat-sort", "sort order when reporting test case kill stat " ~ format("[%(%s|%)]", [EnumMembers!ReportKillSortOrder]), &report.tcKillSortOrder,
526                    "style", "kind of report to generate " ~ format("[%(%s|%)]", [EnumMembers!ReportKind]), &report.reportKind,
527                    "test-metadata", "path to a JSON file containing metadata about tests", &metadataPath,
528                    );
529             // dfmt on
530 
531             if (logDir.empty)
532                 logDir = ".";
533             report.logDir = logDir.Path.AbsolutePath;
534             if (!sections.empty)
535                 report.reportSection = sections;
536             if (!metadataPath.empty)
537                 report.testMetadata = some(ConfigReport.TestMetaData(AbsolutePath(metadataPath)));
538 
539             updateCompileDb(compileDb, compileDbs);
540         }
541 
542         void adminG(string[] args) {
543             bool dump_conf;
544             bool init_conf;
545             data.toolMode = ToolMode.admin;
546             // dfmt off
547             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
548                 "c|config", conf_help, &conf_file,
549                 "db", db_help, &db,
550                 "dump-config", "dump the detailed configuration used", &dump_conf,
551                 "init", "create an initial config to use", &init_conf,
552                 "m|mutant", "mutants to operate on " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants,
553                 "mutant-sub-kind", "kind of mutant " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Kind)]), &admin.subKind,
554                 "operation", "administrative operation to perform " ~ format("[%(%s|%)]", [EnumMembers!AdminOperation]), &admin.adminOp,
555                 "test-case-regex", "regex to use when removing test cases", &admin.testCaseRegex,
556                 "status", "change mutants with this state to the value specified by --to-status " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantStatus,
557                 "to-status", "reset mutants to state (default: unknown) " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantToStatus,
558                 "id", "specify mutant by Id", &admin.mutationId,
559                 "rationale", "rationale for marking mutant", &admin.mutantRationale,
560                 "out", out_help, &workArea.rawRoot,
561                 );
562             // dfmt on
563 
564             if (dump_conf)
565                 data.toolMode = ToolMode.dumpConfig;
566             else if (init_conf)
567                 data.toolMode = ToolMode.initConfig;
568         }
569 
570         groups["analyze"] = &analyzerG;
571         groups["generate"] = &generateMutantG;
572         groups["test"] = &testMutantsG;
573         groups["report"] = &reportG;
574         groups["admin"] = &adminG;
575 
576         if (args.length < 2) {
577             logger.error("Missing command");
578             help = true;
579             exitStatus = ExitStatusType.Errors;
580             return;
581         }
582 
583         const string cg = args[1];
584         string[] subargs = args[0 .. 1];
585         if (args.length > 2)
586             subargs ~= args[2 .. $];
587 
588         if (auto f = cg in groups) {
589             try {
590                 // trusted: not any external input.
591                 () @trusted { (*f)(subargs); }();
592                 help = help_info.helpWanted;
593             } catch (std.getopt.GetOptException ex) {
594                 logger.error(ex.msg);
595                 help = true;
596                 exitStatus = ExitStatusType.Errors;
597             } catch (Exception ex) {
598                 logger.error(ex.msg);
599                 help = true;
600                 exitStatus = ExitStatusType.Errors;
601             }
602         } else {
603             logger.error("Unknown command: ", cg);
604             help = true;
605             exitStatus = ExitStatusType.Errors;
606             return;
607         }
608 
609         import std.algorithm : find;
610         import std.range : drop;
611 
612         if (db.empty) {
613             db = "dextool_mutate.sqlite3";
614         }
615         data.db = AbsolutePath(Path(db));
616 
617         if (workArea.rawRoot.empty) {
618             workArea.rawRoot = ".";
619         }
620         workArea.root = workArea.rawRoot.Path.AbsolutePath;
621 
622         if (workArea.rawInclude.empty) {
623             workArea.rawInclude = ["*"];
624         }
625         workArea.mutantMatcher = GlobFilter(workArea.rawInclude.map!(a => buildPath(workArea.root,
626                 a)).array, workArea.rawExclude.map!(a => buildPath(workArea.root, a)).array,);
627 
628         if (analyze.rawInclude.empty) {
629             analyze.rawInclude = ["*"];
630         }
631         analyze.fileMatcher = GlobFilter(analyze.rawInclude.map!(a => buildPath(workArea.root,
632                 a)).array, analyze.rawExclude.map!(a => buildPath(workArea.root, a)).array);
633 
634         analyze.testPaths = analyze.rawTestPaths.map!(
635                 a => AbsolutePath(buildPath(workArea.root, a))).array;
636         if (analyze.rawTestInclude.empty) {
637             analyze.rawTestInclude = ["*"];
638         }
639         analyze.testFileMatcher = GlobFilter(analyze.rawTestInclude.map!(
640                 a => buildPath(workArea.root, a)).array,
641                 analyze.rawTestExclude.map!(a => buildPath(workArea.root, a)).array);
642 
643         if (!mutants.empty) {
644             data.mutation = mutants;
645         }
646 
647         compiler.extraFlags = compiler.extraFlags ~ args.find("--").drop(1).array();
648     }
649 
650     /**
651      * Trusted:
652      * The only input is a static string and data derived from getopt itselt.
653      * Assuming that getopt in phobos behave well.
654      */
655     void printHelp() @trusted {
656         import std.ascii : newline;
657         import std.stdio : writeln;
658 
659         string base_help = "Usage: dextool mutate COMMAND [options]";
660 
661         switch (toolMode) with (ToolMode) {
662         case none:
663             writeln("commands: ", newline,
664                     groups.byKey.array.sort.map!(a => "  " ~ a).joiner(newline));
665             break;
666         case analyzer:
667             base_help = "Usage: dextool mutate analyze [options] [-- CFLAGS...]";
668             break;
669         case generate_mutant:
670             break;
671         case test_mutants:
672             logger.infof("--test-case-analyze-builtin possible values: %(%s|%)",
673                     [EnumMembers!TestCaseAnalyzeBuiltin]);
674             logger.infof(
675                     "--max-runtime supported units are [weeks, days, hours, minutes, seconds, msecs]");
676             logger.infof(`example: --max-runtime "1 hours 30 minutes"`);
677             break;
678         case report:
679             break;
680         case admin:
681             break;
682         default:
683             break;
684         }
685 
686         defaultGetoptPrinter(base_help, help_info.options);
687     }
688 }
689 
690 /// Replace the config from the users input.
691 void updateCompileDb(ref ConfigCompileDb db, string[] compileDbs) {
692     if (compileDbs.length != 0)
693         db.rawDbs = compileDbs;
694 
695     db.dbs = db.rawDbs
696         .filter!(a => a.length != 0)
697         .map!(a => Path(a).AbsolutePath)
698         .array;
699 }
700 
701 /** Print a help message conveying how files in the compilation database will
702  * be analyzed.
703  *
704  * It must be enough information that the user can adjust `--out` and `--include`.
705  */
706 void printFileAnalyzeHelp(ref ArgParser ap) @safe {
707     static void printPath(string user, string real_) {
708         logger.info("  User: ", user);
709         logger.info("  Real: ", real_);
710     }
711 
712     logger.infof("Reading compilation database:\n%-(%s\n%)", ap.compileDb.dbs);
713 
714     logger.info(
715             "Analyze and mutation of files will only be done on those inside this directory root");
716     printPath(ap.workArea.rawRoot, ap.workArea.root);
717 
718     logger.info(!ap.workArea.rawInclude.empty,
719             "Only mutating files matching any of the following glob patterns:");
720     foreach (idx; 0 .. ap.workArea.rawInclude.length) {
721         printPath(ap.workArea.rawInclude[idx], ap.workArea.mutantMatcher.include[idx]);
722     }
723     logger.info(!ap.workArea.rawExclude.empty,
724             "Excluding mutation of files matching any of the following glob patterns:");
725     foreach (idx; 0 .. ap.workArea.rawExclude.length) {
726         printPath(ap.workArea.rawExclude[idx], ap.workArea.mutantMatcher.exclude[idx]);
727     }
728 
729     logger.info(!ap.analyze.fileMatcher.include.empty,
730             "Only analyzing files matching any of the following glob patterns");
731     foreach (idx; 0 .. ap.analyze.rawInclude.length) {
732         printPath(ap.analyze.rawInclude[idx], ap.analyze.fileMatcher.include[idx]);
733     }
734 
735     logger.info(!ap.analyze.rawExclude.empty,
736             "Excluding files matching any of the following glob patterns from analysis");
737     foreach (idx; 0 .. ap.analyze.rawExclude.length) {
738         printPath(ap.analyze.rawExclude[idx], ap.analyze.fileMatcher.exclude[idx]);
739     }
740 }
741 
742 /** Load the configuration from file.
743  *
744  * Example of a TOML configuration
745  * ---
746  * [defaults]
747  * check_name_standard = true
748  * ---
749  */
750 void loadConfig(ref ArgParser rval) @trusted {
751     import std.file : exists, readText;
752     import toml;
753 
754     if (!exists(rval.miniConf.confFile))
755         return;
756 
757     static auto tryLoading(string configFile) {
758         auto txt = readText(configFile);
759         auto doc = parseTOML(txt);
760         return doc;
761     }
762 
763     TOMLDocument doc;
764     try {
765         doc = tryLoading(rval.miniConf.confFile);
766     } catch (Exception e) {
767         logger.warning("Unable to read the configuration from ", rval.miniConf.confFile);
768         logger.warning(e.msg);
769         rval.data.exitStatus = ExitStatusType.Errors;
770         return;
771     }
772 
773     rval = loadConfig(rval, doc);
774 }
775 
776 ArgParser loadConfig(ArgParser rval, ref TOMLDocument doc) @trusted {
777     import std.conv : to;
778     import std.path : dirName, buildPath;
779     import toml;
780 
781     alias Fn = void delegate(ref ArgParser c, ref TOMLValue v);
782     Fn[string] callbacks;
783 
784     static ShellCommand toShellCommand(ref TOMLValue v, string errorMsg) {
785         if (v.type == TOML_TYPE.STRING) {
786             return ShellCommand([v.str]);
787         } else if (v.type == TOML_TYPE.ARRAY) {
788             return ShellCommand(v.array.map!(a => a.str).array);
789         }
790         logger.warning(errorMsg);
791         return ShellCommand.init;
792     }
793 
794     static ShellCommand[] toShellCommands(ref TOMLValue v, string errorMsg) {
795         import std.format : format;
796 
797         if (v.type == TOML_TYPE.STRING) {
798             return [ShellCommand([v.str])];
799         } else if (v.type == TOML_TYPE.ARRAY) {
800             return v.array.map!(a => toShellCommand(a,
801                     format!"%s: failed to parse as an array"(errorMsg))).array;
802         }
803         logger.warning(errorMsg);
804         return ShellCommand[].init;
805     }
806 
807     static UserRuntime toUserRuntime(ref TOMLValue v) {
808         if (v.type != TOML_TYPE.ARRAY)
809             throw new Exception("the data must be an array of arrays");
810         auto tmp = v.array;
811         if (tmp.length != 2)
812             throw new Exception("the inner array must be size 2");
813         try {
814             return UserRuntime(Path(tmp[0].str), tmp[1].str.to!Language);
815         } catch (Exception e) {
816             logger.warningf("Available options for language are %-(%s, %)",
817                     [EnumMembers!Language]);
818             throw e;
819         }
820     }
821 
822     callbacks["analyze.include"] = (ref ArgParser c, ref TOMLValue v) {
823         c.analyze.rawInclude = v.array.map!(a => a.str).array;
824     };
825     callbacks["analyze.exclude"] = (ref ArgParser c, ref TOMLValue v) {
826         c.analyze.rawExclude = v.array.map!(a => a.str).array;
827     };
828     callbacks["analyze.threads"] = (ref ArgParser c, ref TOMLValue v) {
829         c.analyze.poolSize = cast(int) v.integer;
830     };
831     callbacks["analyze.prune"] = (ref ArgParser c, ref TOMLValue v) {
832         c.analyze.prune = v == true;
833     };
834     callbacks["analyze.mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) {
835         logger.warning("analyze.mutants_per_schema deprecated. Use schema.mutants_per_schema");
836         c.schema.mutantsPerSchema.get = cast(int) v.integer;
837     };
838     callbacks["analyze.min_mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) {
839         logger.warning(
840                 "analyze.min_mutants_per_schema deprecated. Use schema.min_mutants_per_schema");
841         c.schema.minMutantsPerSchema.get = cast(int) v.integer;
842     };
843     callbacks["analyze.test_paths"] = (ref ArgParser c, ref TOMLValue v) {
844         try {
845             c.analyze.rawTestPaths = v.array.map!(a => a.str).array;
846         } catch (Exception e) {
847             logger.error(e.msg);
848         }
849     };
850     callbacks["analyze.test_include"] = (ref ArgParser c, ref TOMLValue v) {
851         c.analyze.rawTestInclude = v.array.map!(a => a.str).array;
852     };
853     callbacks["analyze.test_exclude"] = (ref ArgParser c, ref TOMLValue v) {
854         c.analyze.rawTestExclude = v.array.map!(a => a.str).array;
855     };
856 
857     callbacks["workarea.root"] = (ref ArgParser c, ref TOMLValue v) {
858         c.workArea.rawRoot = v.str;
859     };
860     callbacks["workarea.restrict"] = (ref ArgParser c, ref TOMLValue v) {
861         throw new Exception(
862                 "workarea.restrict is deprecated. Use workarea.exclude instead as glob patterns");
863     };
864     callbacks["workarea.include"] = (ref ArgParser c, ref TOMLValue v) {
865         c.workArea.rawInclude = v.array.map!(a => a.str).array;
866     };
867     callbacks["workarea.exclude"] = (ref ArgParser c, ref TOMLValue v) {
868         c.workArea.rawExclude = v.array.map!(a => a.str).array;
869     };
870 
871     callbacks["generic.mutants"] = (ref ArgParser c, ref TOMLValue v) {
872         try {
873             c.mutation = v.array.map!(a => a.str.to!MutationKind).array;
874         } catch (Exception e) {
875             logger.info("Available mutation kinds ", [EnumMembers!MutationKind]);
876             logger.warning(e.msg);
877         }
878     };
879     callbacks["generic.use_coverage"] = (ref ArgParser c, ref TOMLValue v) {
880         logger.warning("generic.use_coverage is deprecated. Use coverage.use");
881         c.coverage.use = v == true;
882     };
883     callbacks["generic.inject_runtime_impl"] = (ref ArgParser c, ref TOMLValue v) {
884         logger.warning("generic.use_coverage is deprecated. Use coverage.inject_runtime_impl");
885         try {
886             c.coverage.userRuntimeCtrl = v.array.map!(a => toUserRuntime(a)).array;
887         } catch (Exception e) {
888             logger.error("generic.inject_runtime_impl: failed parsing");
889             logger.error(e.msg);
890         }
891     };
892 
893     callbacks["coverage.use"] = (ref ArgParser c, ref TOMLValue v) {
894         c.coverage.use = v == true;
895     };
896     callbacks["coverage.runtime"] = (ref ArgParser c, ref TOMLValue v) {
897         try {
898             c.coverage.runtime = v.str.to!CoverageRuntime;
899         } catch (Exception e) {
900             logger.info("Available options for coverage.runtime ", [
901                     EnumMembers!CoverageRuntime
902                     ]);
903             logger.warning(e.msg);
904         }
905     };
906     callbacks["coverage.inject_runtime_impl"] = (ref ArgParser c, ref TOMLValue v) {
907         try {
908             c.coverage.userRuntimeCtrl = v.array.map!(a => toUserRuntime(a)).array;
909         } catch (Exception e) {
910             logger.error("coverage.inject_runtime_impl: failed parsing");
911             logger.error(e.msg);
912         }
913     };
914 
915     callbacks["schema.use"] = (ref ArgParser c, ref TOMLValue v) {
916         c.schema.use = v == true;
917     };
918     callbacks["schema.runtime"] = (ref ArgParser c, ref TOMLValue v) {
919         try {
920             c.schema.runtime = v.str.to!SchemaRuntime;
921         } catch (Exception e) {
922             logger.info("Available options for schema.runtime ", [
923                     EnumMembers!SchemaRuntime
924                     ]);
925             logger.warning(e.msg);
926         }
927     };
928     callbacks["schema.inject_runtime_impl"] = (ref ArgParser c, ref TOMLValue v) {
929         try {
930             c.schema.userRuntimeCtrl = v.array.map!(a => toUserRuntime(a)).array;
931         } catch (Exception e) {
932             logger.error("coverage.inject_runtime_impl: failed parsing");
933             logger.error(e.msg);
934         }
935     };
936     callbacks["schema.mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) {
937         c.schema.mutantsPerSchema.get = cast(int) v.integer;
938     };
939     callbacks["schema.min_mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) {
940         c.schema.minMutantsPerSchema.get = cast(int) v.integer;
941     };
942     callbacks["schema.check_schemata"] = (ref ArgParser c, ref TOMLValue v) {
943         c.schema.sanityCheckSchemata = v == true;
944     };
945 
946     callbacks["database.db"] = (ref ArgParser c, ref TOMLValue v) {
947         c.db = v.str.Path.AbsolutePath;
948     };
949 
950     callbacks["compile_commands.search_paths"] = (ref ArgParser c, ref TOMLValue v) {
951         c.compileDb.rawDbs = v.array.map!"a.str".array;
952     };
953     callbacks["compile_commands.filter"] = (ref ArgParser c, ref TOMLValue v) {
954         import dextool.type : FilterClangFlag;
955 
956         c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array;
957     };
958     callbacks["compile_commands.skip_compiler_args"] = (ref ArgParser c, ref TOMLValue v) {
959         c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer;
960     };
961 
962     callbacks["compiler.extra_flags"] = (ref ArgParser c, ref TOMLValue v) {
963         c.compiler.extraFlags = v.array.map!(a => a.str).array;
964     };
965     callbacks["compiler.force_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
966         c.compiler.forceSystemIncludes = v == true;
967     };
968     callbacks["compiler.use_compiler_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
969         c.compiler.useCompilerSystemIncludes = v.str;
970     };
971 
972     callbacks["compiler.allow_errors"] = (ref ArgParser c, ref TOMLValue v) {
973         c.compiler.allowErrors.get = v == true;
974     };
975 
976     callbacks["mutant_test.test_cmd"] = (ref ArgParser c, ref TOMLValue v) {
977         c.mutationTest.mutationTester = toShellCommands(v,
978                 "config: failed to parse mutant_test.test_cmd");
979     };
980     callbacks["mutant_test.test_cmd_dir"] = (ref ArgParser c, ref TOMLValue v) {
981         c.mutationTest.testCommandDir = v.array.map!(a => Path(a.str)).array;
982     };
983     callbacks["mutant_test.test_cmd_dir_flag"] = (ref ArgParser c, ref TOMLValue v) {
984         c.mutationTest.testCommandDirFlag = v.array.map!(a => a.str).array;
985     };
986     callbacks["mutant_test.test_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) {
987         c.mutationTest.mutationTesterRuntime = v.str.parseDuration;
988     };
989     callbacks["mutant_test.build_cmd"] = (ref ArgParser c, ref TOMLValue v) {
990         c.mutationTest.mutationCompile = toShellCommand(v,
991                 "config: failed to parse mutant_test.build_cmd");
992     };
993     callbacks["mutant_test.build_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) {
994         c.mutationTest.buildCmdTimeout = v.str.parseDuration;
995     };
996     callbacks["mutant_test.analyze_cmd"] = (ref ArgParser c, ref TOMLValue v) {
997         c.mutationTest.mutationTestCaseAnalyze = toShellCommands(v,
998                 "config: failed to parse mutant_test.analyze_cmd");
999     };
1000     callbacks["mutant_test.analyze_using_builtin"] = (ref ArgParser c, ref TOMLValue v) {
1001         c.mutationTest.mutationTestCaseBuiltin = v.array.map!(
1002                 a => a.str.to!TestCaseAnalyzeBuiltin).array;
1003     };
1004     callbacks["mutant_test.order"] = (ref ArgParser c, ref TOMLValue v) {
1005         c.mutationTest.mutationOrder = v.str.to!MutationOrder;
1006     };
1007     callbacks["mutant_test.detected_new_test_case"] = (ref ArgParser c, ref TOMLValue v) {
1008         try {
1009             c.mutationTest.onNewTestCases = v.str.to!(ConfigMutationTest.NewTestCases);
1010         } catch (Exception e) {
1011             logger.info("Available alternatives: ",
1012                     [EnumMembers!(ConfigMutationTest.NewTestCases)]);
1013             logger.error(e.msg);
1014         }
1015     };
1016     callbacks["mutant_test.detected_dropped_test_case"] = (ref ArgParser c, ref TOMLValue v) {
1017         try {
1018             c.mutationTest.onRemovedTestCases = v.str.to!(ConfigMutationTest.RemovedTestCases);
1019         } catch (Exception e) {
1020             logger.info("Available alternatives: ",
1021                     [EnumMembers!(ConfigMutationTest.RemovedTestCases)]);
1022             logger.error(e.msg);
1023         }
1024     };
1025     callbacks["mutant_test.oldest_mutants"] = (ref ArgParser c, ref TOMLValue v) {
1026         try {
1027             c.mutationTest.onOldMutants = v.str.to!(ConfigMutationTest.OldMutant);
1028         } catch (Exception e) {
1029             logger.info("Available alternatives: ", [
1030                     EnumMembers!(ConfigMutationTest.OldMutant)
1031                     ]);
1032             logger.error(e.msg);
1033         }
1034     };
1035     callbacks["mutant_test.oldest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) {
1036         c.mutationTest.oldMutantsNr = v.integer;
1037     };
1038     callbacks["mutant_test.oldest_mutants_percentage"] = (ref ArgParser c, ref TOMLValue v) {
1039         c.mutationTest.oldMutantPercentage.get = v.floating;
1040     };
1041     callbacks["mutant_test.parallel_test"] = (ref ArgParser c, ref TOMLValue v) {
1042         c.mutationTest.testPoolSize = cast(int) v.integer;
1043     };
1044     callbacks["mutant_test.use_early_stop"] = (ref ArgParser c, ref TOMLValue v) {
1045         c.mutationTest.useEarlyTestCmdStop = v == true;
1046     };
1047     callbacks["mutant_test.use_schemata"] = (ref ArgParser c, ref TOMLValue v) {
1048         logger.warning("mutant_test.use_schemata is deprecated. Use schema.use");
1049         c.schema.use = v == true;
1050     };
1051     callbacks["mutant_test.check_schemata"] = (ref ArgParser c, ref TOMLValue v) {
1052         logger.warning("mutant_test.check_schemata is deprecated. Use schema.check_schema");
1053         c.schema.sanityCheckSchemata = v == true;
1054     };
1055     callbacks["mutant_test.continues_check_test_suite"] = (ref ArgParser c, ref TOMLValue v) {
1056         c.mutationTest.contCheckTestSuite.get = v == true;
1057     };
1058     callbacks["mutant_test.continues_check_test_suite_period"] = (ref ArgParser c, ref TOMLValue v) {
1059         c.mutationTest.contCheckTestSuitePeriod.get = cast(int) v.integer;
1060     };
1061     callbacks["mutant_test.test_cmd_checksum"] = (ref ArgParser c, ref TOMLValue v) {
1062         c.mutationTest.testCmdChecksum.get = v == true;
1063     };
1064     callbacks["mutant_test.max_test_cmd_output"] = (ref ArgParser c, ref TOMLValue v) {
1065         c.mutationTest.maxTestCaseOutput.get = v.integer;
1066     };
1067     callbacks["mutant_test.max_mem_usage_percentage"] = (ref ArgParser c, ref TOMLValue v) {
1068         c.mutationTest.maxMemUsage.get = v.floating;
1069     };
1070 
1071     callbacks["report.style"] = (ref ArgParser c, ref TOMLValue v) {
1072         c.report.reportKind = v.str.to!ReportKind;
1073     };
1074     callbacks["report.sections"] = (ref ArgParser c, ref TOMLValue v) {
1075         try {
1076             c.report.reportSection = v.array.map!(a => a.str.to!ReportSection).array;
1077         } catch (Exception e) {
1078             logger.info("Available mutation kinds ", [
1079                     EnumMembers!ReportSection
1080                     ]);
1081             logger.error(e.msg);
1082         }
1083     };
1084     callbacks["report.high_interest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) {
1085         c.report.highInterestMutantsNr.get = cast(uint) v.integer;
1086     };
1087 
1088     callbacks["test.metadata"] = (ref ArgParser c, ref TOMLValue v) {
1089         c.report.testMetadata = some(ConfigReport.TestMetaData(AbsolutePath(v.str)));
1090     };
1091 
1092     void iterSection(ref ArgParser c, string sectionName) {
1093         if (auto section = sectionName in doc) {
1094             // specific configuration from section members
1095             foreach (k, v; *section) {
1096                 const key = sectionName ~ "." ~ k;
1097                 if (auto cb = key in callbacks) {
1098                     try {
1099                         (*cb)(c, v);
1100                     } catch (Exception e) {
1101                         logger.error(e.msg).collectException;
1102                         logger.error("section ", key).collectException;
1103                         logger.error("value ", v).collectException;
1104                     }
1105                 } else {
1106                     logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName);
1107                 }
1108             }
1109         }
1110     }
1111 
1112     iterSection(rval, "generic");
1113     iterSection(rval, "analyze");
1114     iterSection(rval, "workarea");
1115     iterSection(rval, "database");
1116     iterSection(rval, "compiler");
1117     iterSection(rval, "compile_commands");
1118     iterSection(rval, "mutant_test");
1119     iterSection(rval, "report");
1120     iterSection(rval, "schema");
1121     iterSection(rval, "coverage");
1122     iterSection(rval, "test");
1123 
1124     parseTestGroups(rval, doc);
1125 
1126     return rval;
1127 }
1128 
1129 void parseTestGroups(ref ArgParser c, ref TOMLDocument doc) @trusted {
1130     import toml;
1131 
1132     if ("test_group" !in doc)
1133         return;
1134 
1135     foreach (k, s; *("test_group" in doc)) {
1136         if (s.type != TOML_TYPE.TABLE)
1137             continue;
1138 
1139         string desc;
1140         if (auto v = "description" in s)
1141             desc = v.str;
1142         if (auto v = "pattern" in s) {
1143             string re = v.str;
1144             c.report.testGroups ~= TestGroup(k, desc, re);
1145         }
1146     }
1147 }
1148 
1149 @("shall populate the test, build and analyze command of an ArgParser from a TOML document")
1150 @system unittest {
1151     import std.format : format;
1152     import toml : parseTOML;
1153 
1154     immutable txt = `
1155 [mutant_test]
1156 test_cmd = %s
1157 build_cmd = %s
1158 analyze_cmd = %s
1159 `;
1160 
1161     {
1162         auto doc = parseTOML(format!txt(`"test.sh"`, `"build.sh"`, `"analyze.sh"`));
1163         auto ap = loadConfig(ArgParser.init, doc);
1164         ap.mutationTest.mutationTester.shouldEqual([ShellCommand(["test.sh"])]);
1165         ap.mutationTest.mutationCompile.shouldEqual(ShellCommand(["build.sh"]));
1166         ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([
1167                 ShellCommand(["analyze.sh"])
1168                 ]);
1169     }
1170 
1171     {
1172         auto doc = parseTOML(format!txt(`[["test1.sh"], ["test2.sh"]]`,
1173                 `["build.sh", "-y"]`, `[["analyze.sh", "-z"]]`));
1174         auto ap = loadConfig(ArgParser.init, doc);
1175         ap.mutationTest.mutationTester.shouldEqual([
1176                 ShellCommand(["test1.sh"]), ShellCommand(["test2.sh"])
1177                 ]);
1178         ap.mutationTest.mutationCompile.shouldEqual(ShellCommand([
1179                     "build.sh", "-y"
1180                 ]));
1181         ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([
1182                 ShellCommand(["analyze.sh", "-z"])
1183                 ]);
1184     }
1185 
1186     {
1187         auto doc = parseTOML(format!txt(`[["test1.sh", "-x"], ["test2.sh", "-y"]]`,
1188                 `"build.sh"`, `"analyze.sh"`));
1189         auto ap = loadConfig(ArgParser.init, doc);
1190         ap.mutationTest.mutationTester.shouldEqual([
1191                 ShellCommand(["test1.sh", "-x"]), ShellCommand([
1192                         "test2.sh", "-y"
1193                     ])
1194                 ]);
1195     }
1196 }
1197 
1198 @("shall set the thread analyze limitation from the configuration")
1199 @system unittest {
1200     import toml : parseTOML;
1201 
1202     immutable txt = `
1203 [analyze]
1204 threads = 42
1205 `;
1206     auto doc = parseTOML(txt);
1207     auto ap = loadConfig(ArgParser.init, doc);
1208     ap.analyze.poolSize.shouldEqual(42);
1209 }
1210 
1211 @("shall set how many tests are executed in parallel from the configuration")
1212 @system unittest {
1213     import toml : parseTOML;
1214 
1215     immutable txt = `
1216 [mutant_test]
1217 parallel_test = 42
1218 `;
1219     auto doc = parseTOML(txt);
1220     auto ap = loadConfig(ArgParser.init, doc);
1221     ap.mutationTest.testPoolSize.shouldEqual(42);
1222 }
1223 
1224 @("shall deactivate prune of old files when analyze")
1225 @system unittest {
1226     import toml : parseTOML;
1227 
1228     immutable txt = `
1229 [analyze]
1230 prune = false
1231 `;
1232     auto doc = parseTOML(txt);
1233     auto ap = loadConfig(ArgParser.init, doc);
1234     ap.analyze.prune.shouldBeFalse;
1235 }
1236 
1237 @("shall activate early stop of test commands")
1238 @system unittest {
1239     import toml : parseTOML;
1240 
1241     immutable txt = `
1242 [mutant_test]
1243 use_early_stop = true
1244 `;
1245     auto doc = parseTOML(txt);
1246     auto ap = loadConfig(ArgParser.init, doc);
1247     ap.mutationTest.useEarlyTestCmdStop.shouldBeTrue;
1248 }
1249 
1250 @("shall activate schematas and sanity check of schematas")
1251 @system unittest {
1252     import toml : parseTOML;
1253 
1254     immutable txt = `
1255 [schema]
1256 use = true
1257 check_schemata = true
1258 `;
1259     auto doc = parseTOML(txt);
1260     auto ap = loadConfig(ArgParser.init, doc);
1261     ap.schema.use.shouldBeTrue;
1262     ap.schema.sanityCheckSchemata.shouldBeTrue;
1263 }
1264 
1265 @("shall set the number of mutants per schema")
1266 @system unittest {
1267     import toml : parseTOML;
1268 
1269     immutable txt = `
1270 [schema]
1271 mutants_per_schema = 200
1272 `;
1273     auto doc = parseTOML(txt);
1274     auto ap = loadConfig(ArgParser.init, doc);
1275     ap.schema.mutantsPerSchema.get.shouldEqual(200);
1276 }
1277 
1278 @("shall parse if compilation errors are allowed")
1279 @system unittest {
1280     import toml : parseTOML;
1281 
1282     immutable txt = `
1283 [compiler]
1284 allow_errors = true
1285 `;
1286     auto doc = parseTOML(txt);
1287     auto ap = loadConfig(ArgParser.init, doc);
1288     ap.compiler.allowErrors.get.shouldBeTrue;
1289 }
1290 
1291 @("shall parse the build command timeout")
1292 @system unittest {
1293     import toml : parseTOML;
1294 
1295     immutable txt = `
1296 [mutant_test]
1297 build_cmd_timeout = "1 hours"
1298 `;
1299     auto doc = parseTOML(txt);
1300     auto ap = loadConfig(ArgParser.init, doc);
1301     ap.mutationTest.buildCmdTimeout.shouldEqual(1.dur!"hours");
1302 }
1303 
1304 @("shall parse the continues test suite test")
1305 @system unittest {
1306     import toml : parseTOML;
1307 
1308     immutable txt = `
1309 [mutant_test]
1310 continues_check_test_suite = true
1311 continues_check_test_suite_period = 3
1312 `;
1313     auto doc = parseTOML(txt);
1314     auto ap = loadConfig(ArgParser.init, doc);
1315     ap.mutationTest.contCheckTestSuite.get.shouldBeTrue;
1316     ap.mutationTest.contCheckTestSuitePeriod.get.shouldEqual(3);
1317 }
1318 
1319 @("shall parse the mutants to test")
1320 @system unittest {
1321     import toml : parseTOML;
1322 
1323     immutable txt = `
1324 [generic]
1325 mutants = ["lcr"]
1326 `;
1327     auto doc = parseTOML(txt);
1328     auto ap = loadConfig(ArgParser.init, doc);
1329     ap.data.mutation.shouldEqual([MutationKind.lcr]);
1330 }
1331 
1332 @("shall parse the files to inject the schema runtime to")
1333 @system unittest {
1334     import toml : parseTOML;
1335 
1336     immutable txt = `
1337 [schema]
1338 inject_runtime_impl = [["foo", "cpp"]]
1339 `;
1340     auto doc = parseTOML(txt);
1341     auto ap = loadConfig(ArgParser.init, doc);
1342     ap.schema.userRuntimeCtrl.shouldEqual([
1343             UserRuntime(Path("foo"), Language.cpp)
1344             ]);
1345 }
1346 
1347 @("shall parse the files to inject the coverage runtime to")
1348 @system unittest {
1349     import toml : parseTOML;
1350 
1351     immutable txt = `
1352 [coverage]
1353 inject_runtime_impl = [["foo", "cpp"]]
1354 `;
1355     auto doc = parseTOML(txt);
1356     auto ap = loadConfig(ArgParser.init, doc);
1357     ap.coverage.userRuntimeCtrl.shouldEqual([
1358             UserRuntime(Path("foo"), Language.cpp)
1359             ]);
1360 }
1361 
1362 @("shall parse the report sections")
1363 @system unittest {
1364     import toml : parseTOML;
1365 
1366     immutable txt = `
1367 [report]
1368 sections = ["all_mut", "summary"]
1369 `;
1370     auto doc = parseTOML(txt);
1371     auto ap = loadConfig(ArgParser.init, doc);
1372     ap.report.reportSection.shouldEqual([
1373             ReportSection.all_mut, ReportSection.summary
1374             ]);
1375 }
1376 
1377 @("shall parse the number of high interest mutants")
1378 @system unittest {
1379     import toml : parseTOML;
1380 
1381     immutable txt = `
1382 [report]
1383 high_interest_mutants_nr = 10
1384 `;
1385     auto doc = parseTOML(txt);
1386     auto ap = loadConfig(ArgParser.init, doc);
1387     ap.report.highInterestMutantsNr.get.shouldEqual(10);
1388 }
1389 
1390 @("shall activate test binary checksum")
1391 @system unittest {
1392     import toml : parseTOML;
1393 
1394     immutable txt = `
1395 [mutant_test]
1396 test_cmd_checksum = true
1397 `;
1398     auto doc = parseTOML(txt);
1399     auto ap = loadConfig(ArgParser.init, doc);
1400     ap.mutationTest.testCmdChecksum.get.shouldBeTrue;
1401 }
1402 
1403 @("shall parse the max Mb to capture")
1404 @system unittest {
1405     import toml : parseTOML;
1406 
1407     immutable txt = `
1408 [mutant_test]
1409 max_test_cmd_output = 42
1410 `;
1411     auto doc = parseTOML(txt);
1412     auto ap = loadConfig(ArgParser.init, doc);
1413     ap.mutationTest.maxTestCaseOutput.get.shouldEqual(42);
1414 }
1415 
1416 @("shall parse the test metadata")
1417 @system unittest {
1418     import toml : parseTOML;
1419 
1420     immutable txt = `[test]
1421 metadata = "foo.json"`;
1422     auto doc = parseTOML(txt);
1423     auto ap = loadConfig(ArgParser.init, doc);
1424     ap.report.testMetadata.orElse(ConfigReport.TestMetaData(AbsolutePath.init))
1425         .get.toString.shouldEqual(AbsolutePath("foo.json").toString);
1426 }
1427 
1428 @("shall parse the max memory usage")
1429 @system unittest {
1430     import toml : parseTOML;
1431 
1432     immutable txt = `[mutant_test]
1433 max_mem_usage_percentage = 2.0`;
1434     auto doc = parseTOML(txt);
1435     auto ap = loadConfig(ArgParser.init, doc);
1436     (cast(long) ap.mutationTest.maxMemUsage.get).shouldEqual(2);
1437 }
1438 
1439 /// Minimal config to setup path to config file.
1440 struct MiniConfig {
1441     /// Value from the user via CLI, unmodified.
1442     string rawConfFile;
1443 
1444     /// The configuration file that has been loaded
1445     AbsolutePath confFile;
1446 
1447     bool shortPluginHelp;
1448 }
1449 
1450 /// Returns: minimal config to load settings and setup working directory.
1451 MiniConfig cliToMiniConfig(string[] args) @trusted nothrow {
1452     import std.file : exists;
1453     static import std.getopt;
1454 
1455     immutable default_conf = ".dextool_mutate.toml";
1456 
1457     MiniConfig conf;
1458 
1459     try {
1460         std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough,
1461                 "c|config", "none not visible to the user", &conf.rawConfFile,
1462                 "short-plugin-help", "not visible to the user", &conf.shortPluginHelp);
1463         if (conf.rawConfFile.length == 0)
1464             conf.rawConfFile = default_conf;
1465         conf.confFile = Path(conf.rawConfFile).AbsolutePath;
1466     } catch (Exception e) {
1467         logger.trace(conf).collectException;
1468         logger.error(e.msg).collectException;
1469     }
1470 
1471     return conf;
1472 }
1473 
1474 auto parseDuration(string timeSpec) {
1475     import std.conv : to;
1476     import std..string : split;
1477     import std.datetime : Duration, dur;
1478     import std.range : chunks;
1479 
1480     Duration d;
1481     const parts = timeSpec.split;
1482 
1483     if (parts.length % 2 != 0) {
1484         logger.warning("Invalid time specification because either the number or unit is missing");
1485         return d;
1486     }
1487 
1488     foreach (const p; parts.chunks(2)) {
1489         const nr = p[0].to!long;
1490         bool validUnit;
1491         immutable Units = [
1492             "msecs", "seconds", "minutes", "hours", "days", "weeks"
1493         ];
1494         static foreach (Unit; Units) {
1495             if (p[1] == Unit) {
1496                 d += nr.dur!Unit;
1497                 validUnit = true;
1498             }
1499         }
1500         if (!validUnit) {
1501             logger.warningf("Invalid unit '%s'. Valid are %-(%s, %).", p[1], Units);
1502             return d;
1503         }
1504     }
1505 
1506     return d;
1507 }
1508 
1509 @("shall parse a string to a duration")
1510 unittest {
1511     const expected = 1.dur!"weeks" + 1.dur!"days" + 3.dur!"hours"
1512         + 2.dur!"minutes" + 5.dur!"seconds" + 9.dur!"msecs";
1513     const d = parseDuration("1 weeks 1 days 3 hours 2 minutes 5 seconds 9 msecs");
1514     d.should == expected;
1515 }
1516 
1517 TestConstraint parseUserTestConstraint(string[] raw) {
1518     import std.conv : to;
1519     import std.regex : regex, matchFirst;
1520     import std.typecons : tuple;
1521     import dextool.plugin.mutate.type : TestConstraint;
1522 
1523     TestConstraint rval;
1524     const re = regex(`(?P<file>.*):(?P<start>\d*)-(?P<end>\d*)`);
1525 
1526     foreach (r; raw.map!(a => tuple!("user", "match")(a, matchFirst(a, re)))) {
1527         if (r.match.empty) {
1528             logger.warning("Unable to parse ", r.user);
1529             continue;
1530         }
1531 
1532         const start = r.match["start"].to!uint;
1533         const end = r.match["end"].to!uint + 1;
1534 
1535         if (start > end) {
1536             logger.warningf("Unable to parse %s because start (%s) must be less than end (%s)",
1537                     r.user, r.match["start"], r.match["end"]);
1538             continue;
1539         }
1540 
1541         foreach (const l; start .. end)
1542             rval.value[Path(r.match["file"])] ~= Line(l);
1543     }
1544 
1545     return rval;
1546 }
1547 
1548 @("shall parse a test restriction")
1549 unittest {
1550     const r = parseUserTestConstraint([
1551             "foo/bar:1-10", "smurf bar/i oknen:ea,ting:33-45"
1552             ]);
1553 
1554     Path("foo/bar").shouldBeIn(r.value);
1555     r.value[Path("foo/bar")][0].should == Line(1);
1556     r.value[Path("foo/bar")][9].should == Line(10);
1557 
1558     Path("smurf bar/i oknen:ea,ting").shouldBeIn(r.value);
1559     r.value[Path("smurf bar/i oknen:ea,ting")][0].should == Line(33);
1560     r.value[Path("smurf bar/i oknen:ea,ting")][12].should == Line(45);
1561 }