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