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