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 }