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 std.array : empty;
17 import logger = std.experimental.logger;
18 import std.exception : collectException;
19 import std.traits : EnumMembers;
20 
21 import toml : TOMLDocument;
22 
23 public import dextool.plugin.mutate.backend : Mutation;
24 public import dextool.plugin.mutate.type;
25 import dextool.plugin.mutate.config;
26 import dextool.utility : asAbsNormPath;
27 import dextool.type : AbsolutePath, Path, ExitStatusType, ShellCommand;
28 
29 @safe:
30 
31 /// Extract and cleanup user input from the command line.
32 struct ArgParser {
33     import std.typecons : Nullable;
34     import std.conv : ConvException;
35     import std.getopt : GetoptResult, getopt, defaultGetoptPrinter;
36     import dextool.type : FileName;
37 
38     /// Minimal data needed to bootstrap the configuration.
39     MiniConfig miniConf;
40 
41     ConfigCompileDb compileDb;
42     ConfigCompiler compiler;
43     ConfigMutationTest mutationTest;
44     ConfigAdmin admin;
45     ConfigWorkArea workArea;
46     ConfigReport report;
47 
48     struct Data {
49         string[] inFiles;
50 
51         AbsolutePath db;
52 
53         bool help;
54         ExitStatusType exitStatus = ExitStatusType.Ok;
55 
56         MutationKind[] mutation;
57 
58         Nullable!long mutationId;
59 
60         ToolMode toolMode;
61     }
62 
63     Data data;
64     alias data this;
65 
66     private GetoptResult help_info;
67 
68     alias GroupF = void delegate(string[]) @system;
69     GroupF[string] groups;
70 
71     /// Returns: a config object with default values.
72     static ArgParser make() @safe {
73         import dextool.compilation_db : defaultCompilerFlagFilter, CompileCommandFilter;
74 
75         ArgParser r;
76         r.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
77         return r;
78     }
79 
80     /// Convert the configuration to a TOML file.
81     string toTOML() @trusted {
82         import std.algorithm : joiner, map;
83         import std.array : appender, array;
84         import std.ascii : newline;
85         import std.conv : to;
86         import std.format : format;
87         import std.traits : EnumMembers;
88         import std.utf : toUTF8;
89 
90         auto app = appender!(string[])();
91 
92         app.put("[workarea]");
93         app.put("# path used as the root for accessing files");
94         app.put(
95                 "# dextool will not modify files with a path outside the root when it perform mutation testing");
96         app.put(`# root = "."`);
97         app.put("# restrict analysis to files in this directory tree");
98         app.put("# this make it possible to only mutate certain parts of an application");
99         app.put("# use relative paths that are inside the root");
100         app.put("# restrict = []");
101         app.put(null);
102 
103         app.put("[database]");
104         app.put("# path to where to store the sqlite3 database");
105         app.put(`# db = "dextool_mutate.sqlite3"`);
106         app.put(null);
107 
108         app.put("[compiler]");
109         app.put("# extra flags to pass on to the compiler such as the C++ standard");
110         app.put(format(`# extra_flags = [%(%s, %)]`, compiler.extraFlags));
111         app.put("# toggle this to force system include paths to use -I instead of -isystem");
112         app.put("# force_system_includes = true");
113         app.put(
114                 "# use this compilers system includes instead of the one used in the compile_commands.json");
115         app.put(format(`# use_compiler_system_includes = "%s"`, compiler.useCompilerSystemIncludes.length == 0
116                 ? "/path/to/c++" : compiler.useCompilerSystemIncludes.value));
117         app.put(null);
118 
119         app.put("[compile_commands]");
120         app.put("# search for compile_commands.json in this paths");
121         if (compileDb.dbs.length == 0)
122             app.put(`# search_paths = ["./compile_commands.json"]`);
123         else
124             app.put(format("search_paths = %s", compileDb.rawDbs));
125         app.put("# flags to remove when analyzing a file in the DB");
126         app.put(format("# filter = [%(%s, %)]", compileDb.flagFilter.filter));
127         app.put("# compiler arguments to skip from the beginning. Needed when the first argument is NOT a compiler but rather a wrapper");
128         app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs));
129         app.put(null);
130 
131         app.put("[mutant_test]");
132         app.put("# (required) program used to run the test suite");
133         app.put(`test_cmd = "test.sh"`);
134         app.put("# timeout to use for the test suite (msecs)");
135         app.put("# test_cmd_timeout = 1000");
136         app.put("# (required) program used to build the application");
137         app.put(`build_cmd = "build.sh"`);
138         app.put(
139                 "# program used to analyze the output from the test suite for test cases that killed the mutant");
140         app.put(`# analyze_cmd = "analyze.sh"`);
141         app.put("# builtin analyzer of output from testing frameworks to find failing test cases");
142         app.put(format("# analyze_using_builtin = [%(%s, %)]",
143                 [EnumMembers!TestCaseAnalyzeBuiltin].map!(a => a.to!string)));
144         app.put("# determine in what order mutations are chosen");
145         app.put(format("# order = %(%s|%)", [EnumMembers!MutationOrder].map!(a => a.to!string)));
146         app.put("# how to behave when new test cases are found");
147         app.put(format("# detected_new_test_case = %(%s|%)",
148                 [EnumMembers!(ConfigMutationTest.NewTestCases)].map!(a => a.to!string)));
149         app.put("# how to behave when test cases are detected as having been removed");
150         app.put("# should the test and the gathered statistics be remove too?");
151         app.put(format("# detected_dropped_test_case = %(%s|%)",
152                 [EnumMembers!(ConfigMutationTest.RemovedTestCases)].map!(a => a.to!string)));
153         app.put("# how the oldest mutants should be treated.");
154         app.put("# It is recommended to test them again.");
155         app.put("# Because you may have changed the test suite so mutants that where previously killed by the test suite now survive.");
156         app.put(format("# oldest_mutants = %(%s|%)",
157                 [EnumMembers!(ConfigMutationTest.OldMutant)].map!(a => a.to!string)));
158         app.put("# How many of the oldest mutants to do the above with");
159         app.put("# oldest_mutants_nr = 10");
160         app.put(null);
161 
162         app.put("[report]");
163         app.put("# default style to use");
164         app.put(format("# style = %(%s|%)", [EnumMembers!ReportKind].map!(a => a.to!string)));
165         app.put(null);
166 
167         app.put("[test_group]");
168         app.put("# subgroups with a description and pattern. Example:");
169         app.put("# [test_group.uc1]");
170         app.put(`# description = "use case 1"`);
171         app.put(`# pattern = "uc_1.*"`);
172         app.put(`# see for regex syntax: http://dlang.org/phobos/std_regex.html`);
173         app.put(null);
174 
175         return app.data.joiner(newline).toUTF8;
176     }
177 
178     void parse(string[] args) {
179         import std.algorithm : filter, map;
180         import std.array : array;
181         import std.format : format;
182 
183         static import std.getopt;
184 
185         const db_help = "sqlite3 database to use (default: dextool_mutate.sqlite3)";
186         const restrict_help = "restrict analysis to files in this directory tree (default: .)";
187         const out_help = "path used as the root for mutation/reporting of files (default: .)";
188         const conf_help = "load configuration (default: .dextool_mutate.toml)";
189 
190         // not used but need to be here. The one used is in MiniConfig.
191         string conf_file;
192 
193         string db;
194 
195         void analyzerG(string[] args) {
196             string[] compile_dbs;
197             data.toolMode = ToolMode.analyzer;
198             // dfmt off
199             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
200                    "compile-db", "Retrieve compilation parameters from the file", &compile_dbs,
201                    "c|config", conf_help, &conf_file,
202                    "db", db_help, &db,
203                    "in", "Input file to parse (default: all files in the compilation database)", &data.inFiles,
204                    "out", out_help, &workArea.rawRoot,
205                    "restrict", restrict_help, &workArea.rawRestrict,
206                    );
207             // dfmt on
208 
209             updateCompileDb(compileDb, compile_dbs);
210         }
211 
212         void generateMutantG(string[] args) {
213             data.toolMode = ToolMode.generate_mutant;
214             string cli_mutation_id;
215             // dfmt off
216             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
217                    "c|config", conf_help, &conf_file,
218                    "db", db_help, &db,
219                    "out", out_help, &workArea.rawRoot,
220                    "restrict", restrict_help, &workArea.rawRestrict,
221                    "id", "mutate the source code as mutant ID", &cli_mutation_id,
222                    );
223             // dfmt on
224 
225             try {
226                 import std.conv : to;
227 
228                 if (cli_mutation_id.length != 0)
229                     data.mutationId = cli_mutation_id.to!long;
230             } catch (ConvException e) {
231                 logger.infof("Invalid mutation point '%s'. It must be in the range [0, %s]",
232                         cli_mutation_id, long.max);
233             }
234         }
235 
236         void testMutantsG(string[] args) {
237             string mutationTester;
238             string mutationCompile;
239             string mutationTestCaseAnalyze;
240             long mutationTesterRuntime;
241 
242             data.toolMode = ToolMode.test_mutants;
243             // dfmt off
244             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
245                    "build-cmd", "program used to build the application", &mutationCompile,
246                    "c|config", conf_help, &conf_file,
247                    "db", db_help, &db,
248                    "dry-run", "do not write data to the filesystem", &mutationTest.dryRun,
249                    "mutant", "kind of mutation to test " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
250                    "order", "determine in what order mutations are chosen " ~ format("[%(%s|%)]", [EnumMembers!MutationOrder]), &mutationTest.mutationOrder,
251                    "out", out_help, &workArea.rawRoot,
252                    "restrict", restrict_help, &workArea.rawRestrict,
253                    "test-cmd", "program used to run the test suite", &mutationTester,
254                    "test-case-analyze-builtin", "builtin analyzer of output from testing frameworks to find failing test cases", &mutationTest.mutationTestCaseBuiltin,
255                    "test-case-analyze-cmd", "program used to find what test cases killed the mutant", &mutationTestCaseAnalyze,
256                    "test-timeout", "timeout to use for the test suite (msecs)", &mutationTesterRuntime,
257                    );
258             // dfmt on
259 
260             if (mutationTester.length != 0)
261                 mutationTest.mutationTester = ShellCommand(mutationTester);
262             if (mutationCompile.length != 0)
263                 mutationTest.mutationCompile = ShellCommand(mutationCompile);
264             if (mutationTestCaseAnalyze.length != 0)
265                 mutationTest.mutationTestCaseAnalyze = Path(mutationTestCaseAnalyze).AbsolutePath;
266             if (mutationTesterRuntime != 0)
267                 mutationTest.mutationTesterRuntime = mutationTesterRuntime.dur!"msecs";
268         }
269 
270         void reportG(string[] args) {
271             string[] compile_dbs;
272             string logDir;
273 
274             data.toolMode = ToolMode.report;
275             // dfmt off
276             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
277                    "compile-db", "Retrieve compilation parameters from the file", &compile_dbs,
278                    "c|config", conf_help, &conf_file,
279                    "db", db_help, &db,
280                    "diff-from-stdin", "report alive mutants in the areas indicated as changed in the diff", &report.unifiedDiff,
281                    "level", "the report level of the mutation data " ~ format("[%(%s|%)]", [EnumMembers!ReportLevel]), &report.reportLevel,
282                    "logdir", "Directory to write log files to (default: .)", &logDir,
283                    "mutant", "kind of mutation to report " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
284                    "out", out_help, &workArea.rawRoot,
285                    "restrict", restrict_help, &workArea.rawRestrict,
286                    "section", "sections to include in the report " ~ format("[%(%s|%)]", [EnumMembers!ReportSection]), &report.reportSection,
287                    "section-tc_stat-num", "number of test cases to report", &report.tcKillSortNum,
288                    "section-tc_stat-sort", "sort order when reporting test case kill stat " ~ format("[%(%s|%)]", [EnumMembers!ReportKillSortOrder]), &report.tcKillSortOrder,
289                    "style", "kind of report to generate " ~ format("[%(%s|%)]", [EnumMembers!ReportKind]), &report.reportKind,
290                    );
291             // dfmt on
292 
293             if (report.reportSection.length != 0 && report.reportLevel != ReportLevel.summary) {
294                 logger.error("Combining --section and --level is not supported");
295                 help_info.helpWanted = true;
296             }
297 
298             if (logDir.empty)
299                 logDir = ".";
300             report.logDir = logDir.Path.AbsolutePath;
301 
302             updateCompileDb(compileDb, compile_dbs);
303         }
304 
305         void adminG(string[] args) {
306             bool dump_conf;
307             bool init_conf;
308             data.toolMode = ToolMode.admin;
309             // dfmt off
310             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
311                 "c|config", conf_help, &conf_file,
312                 "db", db_help, &db,
313                 "dump-config", "dump the detailed configuration used", &dump_conf,
314                 "init", "create an initial config to use", &init_conf,
315                 "mutant", "mutants to operate on " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
316                 "operation", "administrative operation to perform " ~ format("[%(%s|%)]", [EnumMembers!AdminOperation]), &admin.adminOp,
317                 "test-case-regex", "regex to use when removing test cases", &admin.testCaseRegex,
318                 "status", "change mutants with this state to the value specified by --to-status " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantStatus,
319                 "to-status", "reset mutants to state (default: unknown) " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantToStatus,
320                 );
321             // dfmt on
322 
323             if (dump_conf)
324                 data.toolMode = ToolMode.dumpConfig;
325             else if (init_conf)
326                 data.toolMode = ToolMode.initConfig;
327         }
328 
329         groups["analyze"] = &analyzerG;
330         groups["generate"] = &generateMutantG;
331         groups["test"] = &testMutantsG;
332         groups["report"] = &reportG;
333         groups["admin"] = &adminG;
334 
335         if (args.length < 2) {
336             logger.error("Missing command");
337             help = true;
338             exitStatus = ExitStatusType.Errors;
339             return;
340         }
341 
342         const string cg = args[1];
343         string[] subargs = args[0 .. 1];
344         if (args.length > 2)
345             subargs ~= args[2 .. $];
346 
347         if (auto f = cg in groups) {
348             try {
349                 // trusted: not any external input.
350                 () @trusted { (*f)(subargs); }();
351                 help = help_info.helpWanted;
352             } catch (std.getopt.GetOptException ex) {
353                 logger.error(ex.msg);
354                 help = true;
355                 exitStatus = ExitStatusType.Errors;
356             } catch (Exception ex) {
357                 logger.error(ex.msg);
358                 help = true;
359                 exitStatus = ExitStatusType.Errors;
360             }
361         } else {
362             logger.error("Unknown command: ", cg);
363             help = true;
364             exitStatus = ExitStatusType.Errors;
365             return;
366         }
367 
368         import std.algorithm : find;
369         import std.array : array;
370         import std.range : drop;
371 
372         if (db.length != 0)
373             data.db = AbsolutePath(FileName(db));
374         else if (data.db.length == 0)
375             data.db = "dextool_mutate.sqlite3".Path.AbsolutePath;
376 
377         if (workArea.rawRoot.length != 0)
378             workArea.outputDirectory = AbsolutePath(Path(workArea.rawRoot.asAbsNormPath));
379         else if (workArea.outputDirectory.length == 0) {
380             workArea.rawRoot = ".";
381             workArea.outputDirectory = workArea.rawRoot.Path.AbsolutePath;
382         }
383 
384         if (workArea.rawRestrict.length != 0)
385             workArea.restrictDir = workArea.rawRestrict.map!(a => AbsolutePath(FileName(a))).array;
386         else if (workArea.restrictDir.length == 0) {
387             workArea.rawRestrict = [workArea.rawRoot];
388             workArea.restrictDir = [workArea.outputDirectory];
389         }
390 
391         compiler.extraFlags = compiler.extraFlags ~ args.find("--").drop(1).array();
392     }
393 
394     /**
395      * Trusted:
396      * The only input is a static string and data derived from getopt itselt.
397      * Assuming that getopt in phobos behave well.
398      */
399     void printHelp() @trusted {
400         import std.array : array;
401         import std.algorithm : joiner, sort, map;
402         import std.ascii : newline;
403         import std.stdio : writeln;
404 
405         string base_help = "Usage: dextool mutate COMMAND [options]";
406 
407         switch (toolMode) with (ToolMode) {
408         case none:
409             writeln("commands: ", newline,
410                     groups.byKey.array.sort.map!(a => "  " ~ a).joiner(newline));
411             break;
412         case analyzer:
413             base_help = "Usage: dextool mutate analyze [options] [-- CFLAGS...]";
414             break;
415         case generate_mutant:
416             break;
417         case test_mutants:
418             logger.infof("--test-case-analyze-builtin possible values: %(%s|%)",
419                     [EnumMembers!TestCaseAnalyzeBuiltin]);
420             break;
421         case report:
422             break;
423         case admin:
424             break;
425         default:
426             break;
427         }
428 
429         defaultGetoptPrinter(base_help, help_info.options);
430     }
431 }
432 
433 /// Update the config from the users input.
434 void updateCompileDb(ref ConfigCompileDb db, string[] compile_dbs) {
435     import std.array : array;
436     import std.algorithm : filter, map;
437 
438     if (compile_dbs.length != 0)
439         db.rawDbs = compile_dbs;
440     db.dbs = db.rawDbs
441         .filter!(a => a.length != 0)
442         .map!(a => Path(a).AbsolutePath)
443         .array;
444 }
445 
446 /** Print a help message conveying how files in the compilation database will
447  * be analyzed.
448  *
449  * It must be enough information that the user can adjust `--out` and `--restrict`.
450  */
451 void printFileAnalyzeHelp(ref ArgParser ap) @safe {
452     logger.infof("Reading compilation database:\n%-(%s\n%)", ap.compileDb.dbs);
453 
454     logger.info(
455             "Analyze and mutation of files will only be done on those inside this directory root");
456     logger.info("  User input: ", ap.workArea.rawRoot);
457     logger.info("  Real path: ", ap.workArea.outputDirectory);
458     logger.info(ap.workArea.rawRestrict.length != 0,
459             "Further restricted to those inside these paths");
460 
461     assert(ap.workArea.rawRestrict.length == ap.workArea.restrictDir.length);
462     foreach (idx; 0 .. ap.workArea.rawRestrict.length) {
463         if (ap.workArea.rawRestrict[idx] == ap.workArea.rawRoot)
464             continue;
465         logger.info("  User input: ", ap.workArea.rawRestrict[idx]);
466         logger.info("  Real path: ", ap.workArea.restrictDir[idx]);
467     }
468 }
469 
470 /** Load the configuration from file.
471  *
472  * Example of a TOML configuration
473  * ---
474  * [defaults]
475  * check_name_standard = true
476  * ---
477  */
478 void loadConfig(ref ArgParser rval) @trusted {
479     import std.algorithm : filter, map;
480     import std.array : array;
481     import std.conv : to;
482     import std.file : exists, readText;
483     import std.path : dirName, buildPath;
484     import toml;
485 
486     if (!exists(rval.miniConf.confFile))
487         return;
488 
489     static auto tryLoading(string configFile) {
490         auto txt = readText(configFile);
491         auto doc = parseTOML(txt);
492         return doc;
493     }
494 
495     TOMLDocument doc;
496     try {
497         doc = tryLoading(rval.miniConf.confFile);
498     } catch (Exception e) {
499         logger.warning("Unable to read the configuration from ", rval.miniConf.confFile);
500         logger.warning(e.msg);
501         rval.data.exitStatus = ExitStatusType.Errors;
502         return;
503     }
504 
505     alias Fn = void delegate(ref ArgParser c, ref TOMLValue v);
506     Fn[string] callbacks;
507 
508     callbacks["workarea.root"] = (ref ArgParser c, ref TOMLValue v) {
509         c.workArea.rawRoot = v.str;
510     };
511     callbacks["workarea.restrict"] = (ref ArgParser c, ref TOMLValue v) {
512         c.workArea.rawRestrict = v.array.map!(a => a.str).array;
513     };
514     callbacks["database.db"] = (ref ArgParser c, ref TOMLValue v) {
515         c.db = v.str.Path.AbsolutePath;
516     };
517     callbacks["compile_commands.search_paths"] = (ref ArgParser c, ref TOMLValue v) {
518         c.compileDb.rawDbs = v.array.map!"a.str".array;
519     };
520     callbacks["compile_commands.filter"] = (ref ArgParser c, ref TOMLValue v) {
521         import dextool.type : FilterClangFlag;
522 
523         c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array;
524     };
525     callbacks["compile_commands.skip_compiler_args"] = (ref ArgParser c, ref TOMLValue v) {
526         c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer;
527     };
528 
529     callbacks["compiler.extra_flags"] = (ref ArgParser c, ref TOMLValue v) {
530         c.compiler.extraFlags = v.array.map!(a => a.str).array;
531     };
532     callbacks["compiler.force_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
533         c.compiler.forceSystemIncludes = v == true;
534     };
535     callbacks["compiler.use_compiler_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
536         c.compiler.useCompilerSystemIncludes = v.str;
537     };
538 
539     callbacks["mutant_test.test_cmd"] = (ref ArgParser c, ref TOMLValue v) {
540         c.mutationTest.mutationTester = ShellCommand(v.str);
541     };
542     callbacks["mutant_test.test_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) {
543         c.mutationTest.mutationTesterRuntime = v.integer.dur!"msecs";
544     };
545     callbacks["mutant_test.build_cmd"] = (ref ArgParser c, ref TOMLValue v) {
546         c.mutationTest.mutationCompile = ShellCommand(v.str);
547     };
548     callbacks["mutant_test.analyze_cmd"] = (ref ArgParser c, ref TOMLValue v) {
549         c.mutationTest.mutationTestCaseAnalyze = Path(v.str).AbsolutePath;
550     };
551     callbacks["mutant_test.analyze_using_builtin"] = (ref ArgParser c, ref TOMLValue v) {
552         c.mutationTest.mutationTestCaseBuiltin = v.array.map!(
553                 a => a.str.to!TestCaseAnalyzeBuiltin).array;
554     };
555     callbacks["mutant_test.order"] = (ref ArgParser c, ref TOMLValue v) {
556         c.mutationTest.mutationOrder = v.str.to!MutationOrder;
557     };
558     callbacks["mutant_test.detected_new_test_case"] = (ref ArgParser c, ref TOMLValue v) {
559         try {
560             c.mutationTest.onNewTestCases = v.str.to!(ConfigMutationTest.NewTestCases);
561         } catch (Exception e) {
562             logger.info("Available alternatives: ",
563                     [EnumMembers!(ConfigMutationTest.NewTestCases)]);
564         }
565     };
566     callbacks["mutant_test.detected_dropped_test_case"] = (ref ArgParser c, ref TOMLValue v) {
567         try {
568             c.mutationTest.onRemovedTestCases = v.str.to!(ConfigMutationTest.RemovedTestCases);
569         } catch (Exception e) {
570             logger.info("Available alternatives: ",
571                     [EnumMembers!(ConfigMutationTest.RemovedTestCases)]);
572         }
573     };
574     callbacks["mutant_test.oldest_mutants"] = (ref ArgParser c, ref TOMLValue v) {
575         try {
576             c.mutationTest.onOldMutants = v.str.to!(ConfigMutationTest.OldMutant);
577         } catch (Exception e) {
578             logger.info("Available alternatives: ", [EnumMembers!(ConfigMutationTest.OldMutant)]);
579         }
580     };
581     callbacks["mutant_test.oldest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) {
582         c.mutationTest.oldMutantsNr = v.integer;
583     };
584     callbacks["report.style"] = (ref ArgParser c, ref TOMLValue v) {
585         c.report.reportKind = v.str.to!ReportKind;
586     };
587 
588     void iterSection(ref ArgParser c, string sectionName) {
589         if (auto section = sectionName in doc) {
590             // specific configuration from section members
591             foreach (k, v; *section) {
592                 if (auto cb = (sectionName ~ "." ~ k) in callbacks) {
593                     try {
594                         (*cb)(c, v);
595                     } catch (Exception e) {
596                         logger.error(e.msg).collectException;
597                     }
598                 } else {
599                     logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName);
600                 }
601             }
602         }
603     }
604 
605     iterSection(rval, "workarea");
606     iterSection(rval, "database");
607     iterSection(rval, "compiler");
608     iterSection(rval, "compile_commands");
609     iterSection(rval, "mutant_test");
610     iterSection(rval, "report");
611 
612     parseTestGroups(rval, doc);
613 }
614 
615 void parseTestGroups(ref ArgParser c, ref TOMLDocument doc) @trusted {
616     import toml;
617 
618     if ("test_group" !in doc)
619         return;
620 
621     foreach (k, s; *("test_group" in doc)) {
622         if (s.type != TOML_TYPE.TABLE)
623             continue;
624 
625         string desc;
626         if (auto v = "description" in s)
627             desc = v.str;
628         if (auto v = "pattern" in s) {
629             string re = v.str;
630             c.report.testGroups ~= TestGroup(k, desc, re);
631         }
632     }
633 }
634 
635 /// Minimal config to setup path to config file.
636 struct MiniConfig {
637     /// Value from the user via CLI, unmodified.
638     string rawConfFile;
639 
640     /// The configuration file that has been loaded
641     AbsolutePath confFile;
642 
643     bool shortPluginHelp;
644 }
645 
646 /// Returns: minimal config to load settings and setup working directory.
647 MiniConfig cliToMiniConfig(string[] args) @trusted nothrow {
648     import std.file : exists;
649     static import std.getopt;
650 
651     immutable default_conf = ".dextool_mutate.toml";
652 
653     MiniConfig conf;
654 
655     try {
656         std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough,
657                 "c|config", "none not visible to the user", &conf.rawConfFile,
658                 "short-plugin-help", "not visible to the user", &conf.shortPluginHelp);
659         if (conf.rawConfFile.length == 0)
660             conf.rawConfFile = default_conf;
661         conf.confFile = Path(conf.rawConfFile).AbsolutePath;
662     } catch (Exception e) {
663         logger.trace(conf).collectException;
664         logger.error(e.msg).collectException;
665     }
666 
667     return conf;
668 }