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