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