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