1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 This module is responsible for converting the users CLI arguments to
11 configuration of how the mutation plugin should behave.
12 */
13 module dextool.plugin.mutate.frontend.argparser;
14 
15 import core.time : dur;
16 import logger = std.experimental.logger;
17 import std.algorithm : joiner, sort, map, filter;
18 import std.array : empty, array, appender;
19 import std.exception : collectException;
20 import std.path : buildPath;
21 import std.traits : EnumMembers;
22 
23 import toml : TOMLDocument;
24 
25 public import dextool.plugin.mutate.backend : Mutation;
26 public import dextool.plugin.mutate.type;
27 import dextool.plugin.mutate.config;
28 import dextool.type : AbsolutePath, Path, ExitStatusType;
29 
30 version (unittest) {
31     import unit_threaded.assertions;
32 }
33 
34 @safe:
35 
36 /// Extract and cleanup user input from the command line.
37 struct ArgParser {
38     import std.typecons : Nullable;
39     import std.conv : ConvException;
40     import std.getopt : GetoptResult, getopt, defaultGetoptPrinter;
41 
42     /// Minimal data needed to bootstrap the configuration.
43     MiniConfig miniConf;
44 
45     ConfigAdmin admin;
46     ConfigAnalyze analyze;
47     ConfigCompileDb compileDb;
48     ConfigCompiler compiler;
49     ConfigMutationTest mutationTest;
50     ConfigReport report;
51     ConfigWorkArea workArea;
52     ConfigGenerate generate;
53 
54     struct Data {
55         AbsolutePath db;
56         ExitStatusType exitStatus = ExitStatusType.Ok;
57         Mutation.Status to_status;
58         MutationKind[] mutation;
59         ToolMode toolMode;
60         bool help;
61         string[] inFiles;
62     }
63 
64     Data data;
65     alias data this;
66 
67     private GetoptResult help_info;
68 
69     alias GroupF = void delegate(string[]) @system;
70     GroupF[string] groups;
71 
72     /// Returns: a config object with default values.
73     static ArgParser make() @safe {
74         import dextool.compilation_db : defaultCompilerFlagFilter, CompileCommandFilter;
75 
76         ArgParser r;
77         r.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
78         return r;
79     }
80 
81     /// Convert the configuration to a TOML file.
82     string toTOML() @trusted {
83         import std.ascii : newline;
84         import std.conv : to;
85         import std.format : format;
86         import std.utf : toUTF8;
87 
88         auto app = appender!(string[])();
89 
90         app.put("[workarea]");
91         app.put(null);
92         app.put("# base path (absolute or relative) to look for C/C++ files to mutate.");
93         app.put(`# root = "."`);
94         app.put(null);
95         app.put(
96                 "# files and/or directories (relative to root) to be the **only** sources to mutate.");
97         app.put("# restrict = []");
98         app.put(null);
99 
100         app.put("[analyze]");
101         app.put(null);
102         app.put("# files and/or directories (relative to root) to be excluded from analysis.");
103         app.put("# exclude = []");
104         app.put(null);
105         app.put("# number of threads to be used for analysis (default is the number of cores).");
106         app.put("# threads = 1");
107         app.put(null);
108         app.put("# remove files from the database that are no longer found during analysis.");
109         app.put(`# prune = true`);
110         app.put(null);
111         app.put("# maximum number of mutants per schema (zero means no limit).");
112         app.put("# mutants_per_schema = 100");
113         app.put(null);
114 
115         app.put("[database]");
116         app.put(null);
117         app.put("# path (absolute or relative) where mutation statistics will be stored.");
118         app.put(`# db = "dextool_mutate.sqlite3"`);
119         app.put(null);
120 
121         app.put("[compiler]");
122         app.put(null);
123         app.put("# extra flags to pass on to the compiler such as the C++ standard.");
124         app.put(format(`# extra_flags = [%(%s, %)]`, compiler.extraFlags));
125         app.put(null);
126         app.put("# force system includes to use -I instead of -isystem");
127         app.put("# force_system_includes = true");
128         app.put(null);
129         app.put("# system include paths to use instead of the ones in compile_commands.json");
130         app.put(format(`# use_compiler_system_includes = "%s"`, compiler.useCompilerSystemIncludes.length == 0
131                 ? "/path/to/c++" : compiler.useCompilerSystemIncludes.value));
132         app.put(null);
133 
134         app.put("[compile_commands]");
135         app.put(null);
136         app.put("# files and/or directories to look for compile_commands.json.");
137         if (compileDb.dbs.length == 0)
138             app.put(`search_paths = ["./compile_commands.json"]`);
139         else
140             app.put(format("search_paths = %s", compileDb.rawDbs));
141         app.put(null);
142         app.put("# compile flags to remove when analyzing a file.");
143         app.put(format("# filter = [%(%s, %)]", compileDb.flagFilter.filter));
144         app.put(null);
145         app.put("# number of compiler arguments to skip from the beginning (needed when the first argument is NOT a compiler but rather a wrapper).");
146         app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs));
147         app.put(null);
148 
149         app.put("[mutant_test]");
150         app.put(null);
151         app.put("# command to build the program **and** test suite.");
152         app.put(`build_cmd = ["./build.sh"]`);
153         app.put(null);
154         app.put("# at least one of test_cmd_dir (recommended) or test_cmd needs to be specified.");
155         app.put(null);
156         app.put(`# path(s) to recursively look for test binaries to execute.`);
157         app.put(`test_cmd_dir = ["./build/test"]`);
158         app.put(null);
159         app.put(`# flags to add to all executables found in test_cmd_dir.`);
160         app.put(`# test_cmd_dir_flag = ["--gtest_filter", "-*foo"]`);
161         app.put(null);
162         app.put("# command(s) to test the program.");
163         app.put(`# test_cmd = ["./test.sh"]`);
164         app.put(null);
165         app.put(
166                 "# timeout to use for the test suite (by default a measurement-based heuristic will be used).");
167         app.put(`# test_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`);
168         app.put(null);
169         app.put("# timeout to use when compiling the program and test suite (default: 30 minutes)");
170         app.put(`# build_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`);
171         app.put(null);
172         app.put(
173                 "# program used to analyze the output from the test suite for test cases that killed the mutant");
174         app.put(`# analyze_cmd = "analyze.sh"`);
175         app.put(null);
176         app.put("# built-in analyzer of output from testing frameworks to find failing test cases");
177         app.put(format("# analyze_using_builtin = [%(%s, %)]",
178                 [EnumMembers!TestCaseAnalyzeBuiltin].map!(a => a.to!string)));
179         app.put(null);
180         app.put("# determine in what order mutations are chosen");
181         app.put(format("# order = %(%s|%)", [EnumMembers!MutationOrder].map!(a => a.to!string)));
182         app.put(null);
183         app.put("# how to behave when new test cases are found");
184         app.put(format("# detected_new_test_case = %(%s|%)",
185                 [EnumMembers!(ConfigMutationTest.NewTestCases)].map!(a => a.to!string)));
186         app.put(null);
187         app.put("# how to behave when test cases are detected as having been removed");
188         app.put("# should the test and the gathered statistics be removed too?");
189         app.put(format("# detected_dropped_test_case = %(%s|%)",
190                 [EnumMembers!(ConfigMutationTest.RemovedTestCases)].map!(a => a.to!string)));
191         app.put(null);
192         app.put("# how the oldest mutants should be treated.");
193         app.put("# It is recommended to test them again.");
194         app.put("# Because you may have changed the test suite so mutants that where previously killed by the test suite now survive.");
195         app.put(format("# oldest_mutants = %(%s|%)",
196                 [EnumMembers!(ConfigMutationTest.OldMutant)].map!(a => a.to!string)));
197         app.put(null);
198         app.put("# how many of the oldest mutants to do the above with");
199         app.put("# oldest_mutants_nr = 10");
200         app.put(null);
201         app.put(
202                 "# number of threads to be used when running tests in parallel (default is the number of cores).");
203         app.put("# parallel_test = 1");
204         app.put(null);
205         app.put("# stop executing tests as soon as a test command fails.");
206         app.put(
207                 "# This speed up the test phase but the report of test cases killing mutants is less accurate");
208         app.put("use_early_stop = true");
209         app.put(null);
210         app.put("# reduce the compile and link time when testing mutants");
211         app.put("use_schemata = true");
212         app.put(null);
213         app.put("# sanity check the schemata before it is used by executing the test cases");
214         app.put("# it is a slowdown but nice robustness that is usually worth having");
215         app.put("check_schemata = true");
216         app.put(null);
217 
218         app.put("[report]");
219         app.put(null);
220         app.put("# default style to use");
221         app.put(format("# style = %(%s|%)", [EnumMembers!ReportKind].map!(a => a.to!string)));
222         app.put(null);
223 
224         app.put("[test_group]");
225         app.put(null);
226         app.put("# subgroups with a description and pattern. Example:");
227         app.put("# [test_group.uc1]");
228         app.put(`# description = "use case 1"`);
229         app.put(`# pattern = "uc_1.*"`);
230         app.put(`# see for regex syntax: http://dlang.org/phobos/std_regex.html`);
231         app.put(null);
232 
233         return app.data.joiner(newline).toUTF8;
234     }
235 
236     void parse(string[] args) {
237         import std.format : format;
238 
239         static import std.getopt;
240 
241         const db_help = "sqlite3 database to use (default: dextool_mutate.sqlite3)";
242         const restrict_help = "restrict mutation to the files in this directory tree (default: .)";
243         const out_help = "path used as the root for mutation/reporting of files (default: .)";
244         const conf_help = "load configuration (default: .dextool_mutate.toml)";
245 
246         // not used but need to be here. The one used is in MiniConfig.
247         string conf_file;
248         string db = data.db;
249 
250         void analyzerG(string[] args) {
251             string[] compile_dbs;
252             string[] exclude_files;
253             bool noPrune;
254 
255             data.toolMode = ToolMode.analyzer;
256             // dfmt off
257             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
258                    "compile-db", "Retrieve compilation parameters from the file", &compile_dbs,
259                    "c|config", conf_help, &conf_file,
260                    "db", db_help, &db,
261                    "diff-from-stdin", "restrict testing to the mutants in the diff", &analyze.unifiedDiffFromStdin,
262                    "fast-db-store", "improve the write speed of the analyze result (may corrupt the database)", &analyze.fastDbStore,
263                    "file-exclude", "exclude files in these directory tree from the analysis (default: none)", &exclude_files,
264                    "force-save", "force the result from the analyze to be saved", &analyze.forceSaveAnalyze,
265                    "in", "Input file to parse (default: all files in the compilation database)", &data.inFiles,
266                    "no-prune", "do not prune the database of files that aren't found during the analyze", &noPrune,
267                    "out", out_help, &workArea.rawRoot,
268                    "profile", "print performance profile for the analyzers that are part of the report", &analyze.profile,
269                    "restrict", restrict_help, &workArea.rawRestrict,
270                    "schema-mutants", "number of mutants per schema (soft upper limit)", &analyze.mutantsPerSchema,
271                    "threads", "number of threads to use for analysing files (default: CPU cores available)", &analyze.poolSize,
272                    );
273             // dfmt on
274 
275             analyze.prune = !noPrune;
276             updateCompileDb(compileDb, compile_dbs);
277             if (!exclude_files.empty)
278                 analyze.rawExclude = exclude_files;
279         }
280 
281         void generateMutantG(string[] args) {
282             data.toolMode = ToolMode.generate_mutant;
283             // dfmt off
284             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
285                    "c|config", conf_help, &conf_file,
286                    "db", db_help, &db,
287                    "out", out_help, &workArea.rawRoot,
288                    "restrict", restrict_help, &workArea.rawRestrict,
289                    std.getopt.config.required, "id", "mutate the source code as mutant ID", &generate.mutationId,
290                    );
291             // dfmt on
292         }
293 
294         void testMutantsG(string[] args) {
295             import std.datetime : Clock;
296 
297             string[] mutationTester;
298             string mutationCompile;
299             string[] mutationTestCaseAnalyze;
300             long mutationTesterRuntime;
301             string maxRuntime;
302             string[] testConstraint;
303             int maxAlive = -1;
304 
305             // set the seed here so if the user specify the CLI the rolling
306             // seed is overridden.
307             mutationTest.pullRequestSeed += Clock.currTime.isoWeek + Clock.currTime.year;
308 
309             data.toolMode = ToolMode.test_mutants;
310             // dfmt off
311             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
312                    "L", "restrict testing to the requested files and lines (<file>:<start>-<end>)", &testConstraint,
313                    "build-cmd", "program used to build the application", &mutationCompile,
314                    "check-schemata", "sanity check a schemata before it is used", &mutationTest.sanityCheckSchemata,
315                    "c|config", conf_help, &conf_file,
316                    "db", db_help, &db,
317                    "diff-from-stdin", "restrict testing to the mutants in the diff", &mutationTest.unifiedDiffFromStdin,
318                    "dry-run", "do not write data to the filesystem", &mutationTest.dryRun,
319                    "log-schemata", "write the mutation schematas to a separate file", &mutationTest.logSchemata,
320                    "max-alive", "stop after NR alive mutants is found (only effective with -L or --diff-from-stdin)", &maxAlive,
321                    "max-runtime", format("max time to run the mutation testing for (default: %s)", mutationTest.maxRuntime), &maxRuntime,
322                    "m|mutant", "kind of mutation to test " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
323                    "only-schemata", "stop testing after the last schema has been executed", &mutationTest.stopAfterLastSchema,
324                    "order", "determine in what order mutants are chosen " ~ format("[%(%s|%)]", [EnumMembers!MutationOrder]), &mutationTest.mutationOrder,
325                    "out", out_help, &workArea.rawRoot,
326                    "pull-request-seed", "seed used when randomly choosing mutants to test in a pull request", &mutationTest.pullRequestSeed,
327                    "restrict", restrict_help, &workArea.rawRestrict,
328                    "test-case-analyze-builtin", "builtin analyzer of output from testing frameworks to find failing test cases", &mutationTest.mutationTestCaseBuiltin,
329                    "test-case-analyze-cmd", "program used to find what test cases killed the mutant", &mutationTestCaseAnalyze,
330                    "test-cmd", "program used to run the test suite", &mutationTester,
331                    "test-timeout", "timeout to use for the test suite (msecs)", &mutationTesterRuntime,
332                    "use-early-stop", "stop executing tests for a mutant as soon as one kill a mutant to speed-up testing", &mutationTest.useEarlyTestCmdStop,
333                    "use-schemata", "use schematas to speed-up testing", &mutationTest.useSchemata,
334                    );
335             // dfmt on
336 
337             if (maxAlive > 0)
338                 mutationTest.maxAlive = maxAlive;
339             if (mutationTester.length != 0)
340                 mutationTest.mutationTester = mutationTester.map!(a => ShellCommand.fromString(a))
341                     .array;
342             if (mutationCompile.length != 0)
343                 mutationTest.mutationCompile = ShellCommand.fromString(mutationCompile);
344             if (mutationTestCaseAnalyze.length != 0)
345                 mutationTest.mutationTestCaseAnalyze = mutationTestCaseAnalyze.map!(
346                         a => ShellCommand.fromString(a)).array;
347             if (mutationTesterRuntime != 0)
348                 mutationTest.mutationTesterRuntime = mutationTesterRuntime.dur!"msecs";
349             if (!maxRuntime.empty)
350                 mutationTest.maxRuntime = parseDuration(maxRuntime);
351             mutationTest.constraint = parseUserTestConstraint(testConstraint);
352         }
353 
354         void reportG(string[] args) {
355             string[] compile_dbs;
356             string logDir;
357 
358             data.toolMode = ToolMode.report;
359             // dfmt off
360             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
361                    "compile-db", "Retrieve compilation parameters from the file", &compile_dbs,
362                    "c|config", conf_help, &conf_file,
363                    "db", db_help, &db,
364                    "diff-from-stdin", "report alive mutants in the areas indicated as changed in the diff", &report.unifiedDiff,
365                    "level", "the report level of the mutation data " ~ format("[%(%s|%)]", [EnumMembers!ReportLevel]), &report.reportLevel,
366                    "logdir", "Directory to write log files to (default: .)", &logDir,
367                    "m|mutant", "kind of mutation to report " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
368                    "out", out_help, &workArea.rawRoot,
369                    "profile", "print performance profile for the analyzers that are part of the report", &report.profile,
370                    "restrict", restrict_help, &workArea.rawRestrict,
371                    "section", "sections to include in the report " ~ format("[%(%s|%)]", [EnumMembers!ReportSection]), &report.reportSection,
372                    "section-tc_stat-num", "number of test cases to report", &report.tcKillSortNum,
373                    "section-tc_stat-sort", "sort order when reporting test case kill stat " ~ format("[%(%s|%)]", [EnumMembers!ReportKillSortOrder]), &report.tcKillSortOrder,
374                    "style", "kind of report to generate " ~ format("[%(%s|%)]", [EnumMembers!ReportKind]), &report.reportKind,
375                    );
376             // dfmt on
377 
378             if (report.reportSection.length != 0 && report.reportLevel != ReportLevel.summary) {
379                 logger.error("Combining --section and --level is not supported");
380                 help_info.helpWanted = true;
381             }
382 
383             if (logDir.empty)
384                 logDir = ".";
385             report.logDir = logDir.Path.AbsolutePath;
386 
387             updateCompileDb(compileDb, compile_dbs);
388         }
389 
390         void adminG(string[] args) {
391             bool dump_conf;
392             bool init_conf;
393             data.toolMode = ToolMode.admin;
394             // dfmt off
395             help_info = getopt(args, std.getopt.config.keepEndOfOptions,
396                 "c|config", conf_help, &conf_file,
397                 "db", db_help, &db,
398                 "dump-config", "dump the detailed configuration used", &dump_conf,
399                 "init", "create an initial config to use", &init_conf,
400                 "m|mutant", "mutants to operate on " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation,
401                 "operation", "administrative operation to perform " ~ format("[%(%s|%)]", [EnumMembers!AdminOperation]), &admin.adminOp,
402                 "test-case-regex", "regex to use when removing test cases", &admin.testCaseRegex,
403                 "status", "change mutants with this state to the value specified by --to-status " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantStatus,
404                 "to-status", "reset mutants to state (default: unknown) " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantToStatus,
405                 "id", "specify mutant by Id", &admin.mutationId,
406                 "rationale", "rationale for marking mutant", &admin.mutantRationale,
407                 "out", out_help, &workArea.rawRoot,
408                 );
409             // dfmt on
410 
411             if (dump_conf)
412                 data.toolMode = ToolMode.dumpConfig;
413             else if (init_conf)
414                 data.toolMode = ToolMode.initConfig;
415         }
416 
417         groups["analyze"] = &analyzerG;
418         groups["generate"] = &generateMutantG;
419         groups["test"] = &testMutantsG;
420         groups["report"] = &reportG;
421         groups["admin"] = &adminG;
422 
423         if (args.length < 2) {
424             logger.error("Missing command");
425             help = true;
426             exitStatus = ExitStatusType.Errors;
427             return;
428         }
429 
430         const string cg = args[1];
431         string[] subargs = args[0 .. 1];
432         if (args.length > 2)
433             subargs ~= args[2 .. $];
434 
435         if (auto f = cg in groups) {
436             try {
437                 // trusted: not any external input.
438                 () @trusted { (*f)(subargs); }();
439                 help = help_info.helpWanted;
440             } catch (std.getopt.GetOptException ex) {
441                 logger.error(ex.msg);
442                 help = true;
443                 exitStatus = ExitStatusType.Errors;
444             } catch (Exception ex) {
445                 logger.error(ex.msg);
446                 help = true;
447                 exitStatus = ExitStatusType.Errors;
448             }
449         } else {
450             logger.error("Unknown command: ", cg);
451             help = true;
452             exitStatus = ExitStatusType.Errors;
453             return;
454         }
455 
456         import std.algorithm : find;
457         import std.range : drop;
458 
459         if (db.empty) {
460             db = "dextool_mutate.sqlite3";
461         }
462         data.db = AbsolutePath(Path(db));
463 
464         if (workArea.rawRoot.empty) {
465             workArea.rawRoot = ".";
466         }
467         workArea.outputDirectory = workArea.rawRoot.Path.AbsolutePath;
468 
469         if (workArea.rawRestrict.empty) {
470             workArea.rawRestrict = [workArea.rawRoot];
471         }
472         workArea.restrictDir = workArea.rawRestrict.map!(
473                 a => AbsolutePath(buildPath(workArea.outputDirectory, a))).array;
474 
475         analyze.exclude = analyze.rawExclude.map!(
476                 a => AbsolutePath(buildPath(workArea.outputDirectory, a))).array;
477 
478         if (data.mutation.empty) {
479             // by default use the recommended operators. These are a good
480             // default that test those with relatively few equivalent mutants
481             // thus most that survive are relevant.
482             with (MutationKind) {
483                 data.mutation = [lcr, lcrb, sdl, uoi, dcr];
484             }
485         }
486 
487         compiler.extraFlags = compiler.extraFlags ~ args.find("--").drop(1).array();
488     }
489 
490     /**
491      * Trusted:
492      * The only input is a static string and data derived from getopt itselt.
493      * Assuming that getopt in phobos behave well.
494      */
495     void printHelp() @trusted {
496         import std.ascii : newline;
497         import std.stdio : writeln;
498 
499         string base_help = "Usage: dextool mutate COMMAND [options]";
500 
501         switch (toolMode) with (ToolMode) {
502         case none:
503             writeln("commands: ", newline,
504                     groups.byKey.array.sort.map!(a => "  " ~ a).joiner(newline));
505             break;
506         case analyzer:
507             base_help = "Usage: dextool mutate analyze [options] [-- CFLAGS...]";
508             break;
509         case generate_mutant:
510             break;
511         case test_mutants:
512             logger.infof("--test-case-analyze-builtin possible values: %(%s|%)",
513                     [EnumMembers!TestCaseAnalyzeBuiltin]);
514             logger.infof(
515                     "--max-runtime supported units are [weeks, days, hours, minutes, seconds, msecs]");
516             logger.infof(`example: --max-runtime "1 hours 30 minutes"`);
517             break;
518         case report:
519             break;
520         case admin:
521             break;
522         default:
523             break;
524         }
525 
526         defaultGetoptPrinter(base_help, help_info.options);
527     }
528 }
529 
530 /// Replace the config from the users input.
531 void updateCompileDb(ref ConfigCompileDb db, string[] compile_dbs) {
532     if (compile_dbs.length != 0)
533         db.rawDbs = compile_dbs;
534 
535     db.dbs = db.rawDbs
536         .filter!(a => a.length != 0)
537         .map!(a => Path(a).AbsolutePath)
538         .array;
539 }
540 
541 /** Print a help message conveying how files in the compilation database will
542  * be analyzed.
543  *
544  * It must be enough information that the user can adjust `--out` and `--restrict`.
545  */
546 void printFileAnalyzeHelp(ref ArgParser ap) @safe {
547     static void printPath(string user, AbsolutePath real_path) {
548         logger.info("  User input: ", user);
549         logger.info("  Real path: ", real_path);
550     }
551 
552     logger.infof("Reading compilation database:\n%-(%s\n%)", ap.compileDb.dbs);
553 
554     logger.info(
555             "Analyze and mutation of files will only be done on those inside this directory root");
556     printPath(ap.workArea.rawRoot, ap.workArea.outputDirectory);
557     logger.info(ap.workArea.rawRestrict.length != 0,
558             "Restricting mutation to files in the following directory tree(s)");
559 
560     assert(ap.workArea.rawRestrict.length == ap.workArea.restrictDir.length);
561     foreach (idx; 0 .. ap.workArea.rawRestrict.length) {
562         if (ap.workArea.rawRestrict[idx] == ap.workArea.rawRoot)
563             continue;
564         printPath(ap.workArea.rawRestrict[idx], ap.workArea.restrictDir[idx]);
565     }
566 
567     logger.info(!ap.analyze.exclude.empty,
568             "Excluding files inside the following directory tree(s) from analysis");
569     foreach (idx; 0 .. ap.analyze.exclude.length) {
570         printPath(ap.analyze.rawExclude[idx], ap.analyze.exclude[idx]);
571     }
572 }
573 
574 /** Load the configuration from file.
575  *
576  * Example of a TOML configuration
577  * ---
578  * [defaults]
579  * check_name_standard = true
580  * ---
581  */
582 void loadConfig(ref ArgParser rval) @trusted {
583     import std.file : exists, readText;
584     import toml;
585 
586     if (!exists(rval.miniConf.confFile))
587         return;
588 
589     static auto tryLoading(string configFile) {
590         auto txt = readText(configFile);
591         auto doc = parseTOML(txt);
592         return doc;
593     }
594 
595     TOMLDocument doc;
596     try {
597         doc = tryLoading(rval.miniConf.confFile);
598     } catch (Exception e) {
599         logger.warning("Unable to read the configuration from ", rval.miniConf.confFile);
600         logger.warning(e.msg);
601         rval.data.exitStatus = ExitStatusType.Errors;
602         return;
603     }
604 
605     rval = loadConfig(rval, doc);
606 }
607 
608 ArgParser loadConfig(ArgParser rval, ref TOMLDocument doc) @trusted {
609     import std.conv : to;
610     import std.path : dirName, buildPath;
611     import toml;
612 
613     alias Fn = void delegate(ref ArgParser c, ref TOMLValue v);
614     Fn[string] callbacks;
615 
616     static ShellCommand toShellCommand(ref TOMLValue v, string errorMsg) {
617         if (v.type == TOML_TYPE.STRING) {
618             return ShellCommand.fromString(v.str);
619         } else if (v.type == TOML_TYPE.ARRAY) {
620             return ShellCommand(v.array.map!(a => a.str).array);
621         }
622         logger.warning(errorMsg);
623         return ShellCommand.init;
624     }
625 
626     static ShellCommand[] toShellCommands(ref TOMLValue v, string errorMsg) {
627         import std.format : format;
628 
629         if (v.type == TOML_TYPE.STRING) {
630             return [ShellCommand.fromString(v.str)];
631         } else if (v.type == TOML_TYPE.ARRAY) {
632             return v.array.map!(a => toShellCommand(a,
633                     format!"%s: failed to parse as an array"(errorMsg))).array;
634         }
635         logger.warning(errorMsg);
636         return ShellCommand[].init;
637     }
638 
639     callbacks["analyze.exclude"] = (ref ArgParser c, ref TOMLValue v) {
640         c.analyze.rawExclude = v.array.map!(a => a.str).array;
641     };
642     callbacks["analyze.threads"] = (ref ArgParser c, ref TOMLValue v) {
643         c.analyze.poolSize = cast(int) v.integer;
644     };
645     callbacks["analyze.prune"] = (ref ArgParser c, ref TOMLValue v) {
646         c.analyze.prune = v == true;
647     };
648     callbacks["analyze.mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) {
649         c.analyze.mutantsPerSchema = cast(int) v.integer;
650     };
651 
652     callbacks["workarea.root"] = (ref ArgParser c, ref TOMLValue v) {
653         c.workArea.rawRoot = v.str;
654     };
655     callbacks["workarea.restrict"] = (ref ArgParser c, ref TOMLValue v) {
656         c.workArea.rawRestrict = v.array.map!(a => a.str).array;
657     };
658 
659     callbacks["database.db"] = (ref ArgParser c, ref TOMLValue v) {
660         c.db = v.str.Path.AbsolutePath;
661     };
662 
663     callbacks["compile_commands.search_paths"] = (ref ArgParser c, ref TOMLValue v) {
664         c.compileDb.rawDbs = v.array.map!"a.str".array;
665     };
666     callbacks["compile_commands.filter"] = (ref ArgParser c, ref TOMLValue v) {
667         import dextool.type : FilterClangFlag;
668 
669         c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array;
670     };
671     callbacks["compile_commands.skip_compiler_args"] = (ref ArgParser c, ref TOMLValue v) {
672         c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer;
673     };
674 
675     callbacks["compiler.extra_flags"] = (ref ArgParser c, ref TOMLValue v) {
676         c.compiler.extraFlags = v.array.map!(a => a.str).array;
677     };
678     callbacks["compiler.force_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
679         c.compiler.forceSystemIncludes = v == true;
680     };
681     callbacks["compiler.use_compiler_system_includes"] = (ref ArgParser c, ref TOMLValue v) {
682         c.compiler.useCompilerSystemIncludes = v.str;
683     };
684 
685     callbacks["mutant_test.test_cmd"] = (ref ArgParser c, ref TOMLValue v) {
686         c.mutationTest.mutationTester = toShellCommands(v,
687                 "config: failed to parse mutant_test.test_cmd");
688     };
689     callbacks["mutant_test.test_cmd_dir"] = (ref ArgParser c, ref TOMLValue v) {
690         c.mutationTest.testCommandDir = v.array.map!(a => Path(a.str)).array;
691     };
692     callbacks["mutant_test.test_cmd_dir_flag"] = (ref ArgParser c, ref TOMLValue v) {
693         c.mutationTest.testCommandDirFlag = v.array.map!(a => a.str).array;
694     };
695     callbacks["mutant_test.test_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) {
696         c.mutationTest.mutationTesterRuntime = v.str.parseDuration;
697     };
698     callbacks["mutant_test.build_cmd"] = (ref ArgParser c, ref TOMLValue v) {
699         c.mutationTest.mutationCompile = toShellCommand(v,
700                 "config: failed to parse mutant_test.build_cmd");
701     };
702     callbacks["mutant_test.build_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) {
703         c.mutationTest.buildCmdTimeout = v.str.parseDuration;
704     };
705     callbacks["mutant_test.analyze_cmd"] = (ref ArgParser c, ref TOMLValue v) {
706         c.mutationTest.mutationTestCaseAnalyze = toShellCommands(v,
707                 "config: failed to parse mutant_test.analyze_cmd");
708     };
709     callbacks["mutant_test.analyze_using_builtin"] = (ref ArgParser c, ref TOMLValue v) {
710         c.mutationTest.mutationTestCaseBuiltin = v.array.map!(
711                 a => a.str.to!TestCaseAnalyzeBuiltin).array;
712     };
713     callbacks["mutant_test.order"] = (ref ArgParser c, ref TOMLValue v) {
714         c.mutationTest.mutationOrder = v.str.to!MutationOrder;
715     };
716     callbacks["mutant_test.detected_new_test_case"] = (ref ArgParser c, ref TOMLValue v) {
717         try {
718             c.mutationTest.onNewTestCases = v.str.to!(ConfigMutationTest.NewTestCases);
719         } catch (Exception e) {
720             logger.info("Available alternatives: ",
721                     [EnumMembers!(ConfigMutationTest.NewTestCases)]);
722         }
723     };
724     callbacks["mutant_test.detected_dropped_test_case"] = (ref ArgParser c, ref TOMLValue v) {
725         try {
726             c.mutationTest.onRemovedTestCases = v.str.to!(ConfigMutationTest.RemovedTestCases);
727         } catch (Exception e) {
728             logger.info("Available alternatives: ",
729                     [EnumMembers!(ConfigMutationTest.RemovedTestCases)]);
730         }
731     };
732     callbacks["mutant_test.oldest_mutants"] = (ref ArgParser c, ref TOMLValue v) {
733         try {
734             c.mutationTest.onOldMutants = v.str.to!(ConfigMutationTest.OldMutant);
735         } catch (Exception e) {
736             logger.info("Available alternatives: ", [
737                     EnumMembers!(ConfigMutationTest.OldMutant)
738                     ]);
739         }
740     };
741     callbacks["mutant_test.oldest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) {
742         c.mutationTest.oldMutantsNr = v.integer;
743     };
744     callbacks["mutant_test.parallel_test"] = (ref ArgParser c, ref TOMLValue v) {
745         c.mutationTest.testPoolSize = cast(int) v.integer;
746     };
747     callbacks["mutant_test.use_early_stop"] = (ref ArgParser c, ref TOMLValue v) {
748         c.mutationTest.useEarlyTestCmdStop = v == true;
749     };
750     callbacks["mutant_test.use_schemata"] = (ref ArgParser c, ref TOMLValue v) {
751         c.mutationTest.useSchemata = v == true;
752     };
753     callbacks["mutant_test.check_schemata"] = (ref ArgParser c, ref TOMLValue v) {
754         c.mutationTest.sanityCheckSchemata = v == true;
755     };
756 
757     callbacks["report.style"] = (ref ArgParser c, ref TOMLValue v) {
758         c.report.reportKind = v.str.to!ReportKind;
759     };
760 
761     void iterSection(ref ArgParser c, string sectionName) {
762         if (auto section = sectionName in doc) {
763             // specific configuration from section members
764             foreach (k, v; *section) {
765                 if (auto cb = (sectionName ~ "." ~ k) in callbacks) {
766                     try {
767                         (*cb)(c, v);
768                     } catch (Exception e) {
769                         logger.error(e.msg).collectException;
770                     }
771                 } else {
772                     logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName);
773                 }
774             }
775         }
776     }
777 
778     iterSection(rval, "analyze");
779     iterSection(rval, "workarea");
780     iterSection(rval, "database");
781     iterSection(rval, "compiler");
782     iterSection(rval, "compile_commands");
783     iterSection(rval, "mutant_test");
784     iterSection(rval, "report");
785 
786     parseTestGroups(rval, doc);
787 
788     return rval;
789 }
790 
791 void parseTestGroups(ref ArgParser c, ref TOMLDocument doc) @trusted {
792     import toml;
793 
794     if ("test_group" !in doc)
795         return;
796 
797     foreach (k, s; *("test_group" in doc)) {
798         if (s.type != TOML_TYPE.TABLE)
799             continue;
800 
801         string desc;
802         if (auto v = "description" in s)
803             desc = v.str;
804         if (auto v = "pattern" in s) {
805             string re = v.str;
806             c.report.testGroups ~= TestGroup(k, desc, re);
807         }
808     }
809 }
810 
811 @("shall populate the test, build and analyze command of an ArgParser from a TOML document")
812 @system unittest {
813     import std.format : format;
814     import toml : parseTOML;
815 
816     immutable txt = `
817 [mutant_test]
818 test_cmd = %s
819 build_cmd = %s
820 analyze_cmd = %s
821 `;
822 
823     {
824         auto doc = parseTOML(format!txt(`"test.sh"`, `"build.sh"`, `"analyze.sh"`));
825         auto ap = loadConfig(ArgParser.init, doc);
826         ap.mutationTest.mutationTester.shouldEqual([ShellCommand(["test.sh"])]);
827         ap.mutationTest.mutationCompile.shouldEqual(ShellCommand(["build.sh"]));
828         ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([
829                 ShellCommand(["analyze.sh"])
830                 ]);
831     }
832 
833     {
834         auto doc = parseTOML(format!txt(`[["test1.sh"], ["test2.sh"]]`,
835                 `["build.sh", "-y"]`, `[["analyze.sh", "-z"]]`));
836         auto ap = loadConfig(ArgParser.init, doc);
837         ap.mutationTest.mutationTester.shouldEqual([
838                 ShellCommand(["test1.sh"]), ShellCommand(["test2.sh"])
839                 ]);
840         ap.mutationTest.mutationCompile.shouldEqual(ShellCommand([
841                     "build.sh", "-y"
842                 ]));
843         ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([
844                 ShellCommand(["analyze.sh", "-z"])
845                 ]);
846     }
847 
848     {
849         auto doc = parseTOML(format!txt(`[["test1.sh", "-x"], ["test2.sh", "-y"]]`,
850                 `"build.sh"`, `"analyze.sh"`));
851         auto ap = loadConfig(ArgParser.init, doc);
852         ap.mutationTest.mutationTester.shouldEqual([
853                 ShellCommand(["test1.sh", "-x"]), ShellCommand([
854                         "test2.sh", "-y"
855                     ])
856                 ]);
857     }
858 }
859 
860 @("shall set the thread analyze limitation from the configuration")
861 @system unittest {
862     import toml : parseTOML;
863 
864     immutable txt = `
865 [analyze]
866 threads = 42
867 `;
868     auto doc = parseTOML(txt);
869     auto ap = loadConfig(ArgParser.init, doc);
870     ap.analyze.poolSize.shouldEqual(42);
871 }
872 
873 @("shall set how many tests are executed in parallel from the configuration")
874 @system unittest {
875     import toml : parseTOML;
876 
877     immutable txt = `
878 [mutant_test]
879 parallel_test = 42
880 `;
881     auto doc = parseTOML(txt);
882     auto ap = loadConfig(ArgParser.init, doc);
883     ap.mutationTest.testPoolSize.shouldEqual(42);
884 }
885 
886 @("shall deactivate prune of old files when analyze")
887 @system unittest {
888     import toml : parseTOML;
889 
890     immutable txt = `
891 [analyze]
892 prune = false
893 `;
894     auto doc = parseTOML(txt);
895     auto ap = loadConfig(ArgParser.init, doc);
896     ap.analyze.prune.shouldBeFalse;
897 }
898 
899 @("shall activate early stop of test commands")
900 @system unittest {
901     import toml : parseTOML;
902 
903     immutable txt = `
904 [mutant_test]
905 use_early_stop = true
906 `;
907     auto doc = parseTOML(txt);
908     auto ap = loadConfig(ArgParser.init, doc);
909     ap.mutationTest.useEarlyTestCmdStop.shouldBeTrue;
910 }
911 
912 @("shall activate schematas and sanity check of schematas")
913 @system unittest {
914     import toml : parseTOML;
915 
916     immutable txt = `
917 [mutant_test]
918 use_schemata = true
919 check_schemata = true
920 `;
921     auto doc = parseTOML(txt);
922     auto ap = loadConfig(ArgParser.init, doc);
923     ap.mutationTest.useSchemata.shouldBeTrue;
924     ap.mutationTest.sanityCheckSchemata.shouldBeTrue;
925 }
926 
927 @("shall set the number of mutants per schema")
928 @system unittest {
929     import toml : parseTOML;
930 
931     immutable txt = `
932 [analyze]
933 mutants_per_schema = 200
934 `;
935     auto doc = parseTOML(txt);
936     auto ap = loadConfig(ArgParser.init, doc);
937     ap.analyze.mutantsPerSchema.shouldEqual(200);
938 }
939 
940 @("shall parse the build command timeout")
941 @system unittest {
942     import toml : parseTOML;
943 
944     immutable txt = `
945 [mutant_test]
946 build_cmd_timeout = "1 hours"
947 `;
948     auto doc = parseTOML(txt);
949     auto ap = loadConfig(ArgParser.init, doc);
950     ap.mutationTest.buildCmdTimeout.shouldEqual(1.dur!"hours");
951 }
952 
953 /// Minimal config to setup path to config file.
954 struct MiniConfig {
955     /// Value from the user via CLI, unmodified.
956     string rawConfFile;
957 
958     /// The configuration file that has been loaded
959     AbsolutePath confFile;
960 
961     bool shortPluginHelp;
962 }
963 
964 /// Returns: minimal config to load settings and setup working directory.
965 MiniConfig cliToMiniConfig(string[] args) @trusted nothrow {
966     import std.file : exists;
967     static import std.getopt;
968 
969     immutable default_conf = ".dextool_mutate.toml";
970 
971     MiniConfig conf;
972 
973     try {
974         std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough,
975                 "c|config", "none not visible to the user", &conf.rawConfFile,
976                 "short-plugin-help", "not visible to the user", &conf.shortPluginHelp);
977         if (conf.rawConfFile.length == 0)
978             conf.rawConfFile = default_conf;
979         conf.confFile = Path(conf.rawConfFile).AbsolutePath;
980     } catch (Exception e) {
981         logger.trace(conf).collectException;
982         logger.error(e.msg).collectException;
983     }
984 
985     return conf;
986 }
987 
988 auto parseDuration(string timeSpec) {
989     import std.conv : to;
990     import std.string : split;
991     import std.datetime : Duration, dur;
992     import std.range : chunks;
993 
994     Duration d;
995     const parts = timeSpec.split;
996 
997     if (parts.length % 2 != 0) {
998         logger.warning("Invalid time specification because either the number or unit is missing");
999         return d;
1000     }
1001 
1002     foreach (const p; parts.chunks(2)) {
1003         const nr = p[0].to!long;
1004         bool validUnit;
1005         immutable Units = [
1006             "msecs", "seconds", "minutes", "hours", "days", "weeks"
1007         ];
1008         static foreach (Unit; Units) {
1009             if (p[1] == Unit) {
1010                 d += nr.dur!Unit;
1011                 validUnit = true;
1012             }
1013         }
1014         if (!validUnit) {
1015             logger.warningf("Invalid unit '%s'. Valid are %-(%s, %).", p[1], Units);
1016             return d;
1017         }
1018     }
1019 
1020     return d;
1021 }
1022 
1023 @("shall parse a string to a duration")
1024 unittest {
1025     const expected = 1.dur!"weeks" + 1.dur!"days" + 3.dur!"hours"
1026         + 2.dur!"minutes" + 5.dur!"seconds" + 9.dur!"msecs";
1027     const d = parseDuration("1 weeks 1 days 3 hours 2 minutes 5 seconds 9 msecs");
1028     d.should == expected;
1029 }
1030 
1031 auto parseUserTestConstraint(string[] raw) {
1032     import std.conv : to;
1033     import std.regex : regex, matchFirst;
1034     import std.typecons : tuple;
1035     import dextool.plugin.mutate.type : TestConstraint;
1036 
1037     TestConstraint rval;
1038     const re = regex(`(?P<file>.*):(?P<start>\d*)-(?P<end>\d*)`);
1039 
1040     foreach (r; raw.map!(a => tuple!("user", "match")(a, matchFirst(a, re)))) {
1041         if (r.match.empty) {
1042             logger.warning("Unable to parse ", r.user);
1043             continue;
1044         }
1045 
1046         const start = r.match["start"].to!uint;
1047         const end = r.match["end"].to!uint + 1;
1048 
1049         if (start > end) {
1050             logger.warningf("Unable to parse %s because start (%s) must be less than end (%s)",
1051                     r.user, r.match["start"], r.match["end"]);
1052             continue;
1053         }
1054 
1055         foreach (const l; start .. end)
1056             rval.value[Path(r.match["file"])] ~= Line(l);
1057     }
1058 
1059     return rval;
1060 }
1061 
1062 @("shall parse a test restriction")
1063 unittest {
1064     const r = parseUserTestConstraint([
1065             "foo/bar:1-10", "smurf bar/i oknen:ea,ting:33-45"
1066             ]);
1067 
1068     Path("foo/bar").shouldBeIn(r.value);
1069     r.value[Path("foo/bar")][0].should == Line(1);
1070     r.value[Path("foo/bar")][9].should == Line(10);
1071 
1072     Path("smurf bar/i oknen:ea,ting").shouldBeIn(r.value);
1073     r.value[Path("smurf bar/i oknen:ea,ting")][0].should == Line(33);
1074     r.value[Path("smurf bar/i oknen:ea,ting")][12].should == Line(45);
1075 }