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