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