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 std.array : empty; 17 import logger = std.experimental.logger; 18 import std.exception : collectException; 19 import std.traits : EnumMembers; 20 21 import toml : TOMLDocument; 22 23 public import dextool.plugin.mutate.backend : Mutation; 24 public import dextool.plugin.mutate.type; 25 import dextool.plugin.mutate.config; 26 import dextool.utility : asAbsNormPath; 27 import dextool.type : AbsolutePath, Path, ExitStatusType, ShellCommand; 28 29 @safe: 30 31 /// Extract and cleanup user input from the command line. 32 struct ArgParser { 33 import std.typecons : Nullable; 34 import std.conv : ConvException; 35 import std.getopt : GetoptResult, getopt, defaultGetoptPrinter; 36 import dextool.type : FileName; 37 38 /// Minimal data needed to bootstrap the configuration. 39 MiniConfig miniConf; 40 41 ConfigCompileDb compileDb; 42 ConfigCompiler compiler; 43 ConfigMutationTest mutationTest; 44 ConfigAdmin admin; 45 ConfigWorkArea workArea; 46 ConfigReport report; 47 48 struct Data { 49 string[] inFiles; 50 51 AbsolutePath db; 52 53 bool help; 54 ExitStatusType exitStatus = ExitStatusType.Ok; 55 56 MutationKind[] mutation; 57 58 Nullable!long mutationId; 59 60 ToolMode toolMode; 61 } 62 63 Data data; 64 alias data this; 65 66 private GetoptResult help_info; 67 68 alias GroupF = void delegate(string[]) @system; 69 GroupF[string] groups; 70 71 /// Returns: a config object with default values. 72 static ArgParser make() @safe { 73 import dextool.compilation_db : defaultCompilerFlagFilter, CompileCommandFilter; 74 75 ArgParser r; 76 r.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 77 return r; 78 } 79 80 /// Convert the configuration to a TOML file. 81 string toTOML() @trusted { 82 import std.algorithm : joiner, map; 83 import std.array : appender, array; 84 import std.ascii : newline; 85 import std.conv : to; 86 import std.format : format; 87 import std.traits : EnumMembers; 88 import std.utf : toUTF8; 89 90 auto app = appender!(string[])(); 91 92 app.put("[workarea]"); 93 app.put("# path used as the root for accessing files"); 94 app.put( 95 "# dextool will not modify files with a path outside the root when it perform mutation testing"); 96 app.put(`# root = "."`); 97 app.put("# restrict analysis to files in this directory tree"); 98 app.put("# this make it possible to only mutate certain parts of an application"); 99 app.put("# use relative paths that are inside the root"); 100 app.put("# restrict = []"); 101 app.put(null); 102 103 app.put("[database]"); 104 app.put("# path to where to store the sqlite3 database"); 105 app.put(`# db = "dextool_mutate.sqlite3"`); 106 app.put(null); 107 108 app.put("[compiler]"); 109 app.put("# extra flags to pass on to the compiler such as the C++ standard"); 110 app.put(format(`# extra_flags = [%(%s, %)]`, compiler.extraFlags)); 111 app.put("# toggle this to force system include paths to use -I instead of -isystem"); 112 app.put("# force_system_includes = true"); 113 app.put( 114 "# use this compilers system includes instead of the one used in the compile_commands.json"); 115 app.put(format(`# use_compiler_system_includes = "%s"`, compiler.useCompilerSystemIncludes.length == 0 116 ? "/path/to/c++" : compiler.useCompilerSystemIncludes.value)); 117 app.put(null); 118 119 app.put("[compile_commands]"); 120 app.put("# search for compile_commands.json in this paths"); 121 if (compileDb.dbs.length == 0) 122 app.put(`# search_paths = ["./compile_commands.json"]`); 123 else 124 app.put(format("search_paths = %s", compileDb.rawDbs)); 125 app.put("# flags to remove when analyzing a file in the DB"); 126 app.put(format("# filter = [%(%s, %)]", compileDb.flagFilter.filter)); 127 app.put("# compiler arguments to skip from the beginning. Needed when the first argument is NOT a compiler but rather a wrapper"); 128 app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs)); 129 app.put(null); 130 131 app.put("[mutant_test]"); 132 app.put("# (required) program used to run the test suite"); 133 app.put(`test_cmd = "test.sh"`); 134 app.put("# timeout to use for the test suite (msecs)"); 135 app.put("# test_cmd_timeout = 1000"); 136 app.put("# (required) program used to build the application"); 137 app.put(`build_cmd = "build.sh"`); 138 app.put( 139 "# program used to analyze the output from the test suite for test cases that killed the mutant"); 140 app.put(`# analyze_cmd = "analyze.sh"`); 141 app.put("# builtin analyzer of output from testing frameworks to find failing test cases"); 142 app.put(format("# analyze_using_builtin = [%(%s, %)]", 143 [EnumMembers!TestCaseAnalyzeBuiltin].map!(a => a.to!string))); 144 app.put("# determine in what order mutations are chosen"); 145 app.put(format("# order = %(%s|%)", [EnumMembers!MutationOrder].map!(a => a.to!string))); 146 app.put("# how to behave when new test cases are found"); 147 app.put(format("# detected_new_test_case = %(%s|%)", 148 [EnumMembers!(ConfigMutationTest.NewTestCases)].map!(a => a.to!string))); 149 app.put("# how to behave when test cases are detected as having been removed"); 150 app.put("# should the test and the gathered statistics be remove too?"); 151 app.put(format("# detected_dropped_test_case = %(%s|%)", 152 [EnumMembers!(ConfigMutationTest.RemovedTestCases)].map!(a => a.to!string))); 153 app.put("# how the oldest mutants should be treated."); 154 app.put("# It is recommended to test them again."); 155 app.put("# Because you may have changed the test suite so mutants that where previously killed by the test suite now survive."); 156 app.put(format("# oldest_mutants = %(%s|%)", 157 [EnumMembers!(ConfigMutationTest.OldMutant)].map!(a => a.to!string))); 158 app.put("# How many of the oldest mutants to do the above with"); 159 app.put("# oldest_mutants_nr = 10"); 160 app.put(null); 161 162 app.put("[report]"); 163 app.put("# default style to use"); 164 app.put(format("# style = %(%s|%)", [EnumMembers!ReportKind].map!(a => a.to!string))); 165 app.put(null); 166 167 app.put("[test_group]"); 168 app.put("# subgroups with a description and pattern. Example:"); 169 app.put("# [test_group.uc1]"); 170 app.put(`# description = "use case 1"`); 171 app.put(`# pattern = "uc_1.*"`); 172 app.put(`# see for regex syntax: http://dlang.org/phobos/std_regex.html`); 173 app.put(null); 174 175 return app.data.joiner(newline).toUTF8; 176 } 177 178 void parse(string[] args) { 179 import std.algorithm : filter, map; 180 import std.array : array; 181 import std.format : format; 182 183 static import std.getopt; 184 185 const db_help = "sqlite3 database to use (default: dextool_mutate.sqlite3)"; 186 const restrict_help = "restrict analysis to files in this directory tree (default: .)"; 187 const out_help = "path used as the root for mutation/reporting of files (default: .)"; 188 const conf_help = "load configuration (default: .dextool_mutate.toml)"; 189 190 // not used but need to be here. The one used is in MiniConfig. 191 string conf_file; 192 193 string db; 194 195 void analyzerG(string[] args) { 196 string[] compile_dbs; 197 data.toolMode = ToolMode.analyzer; 198 // dfmt off 199 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 200 "compile-db", "Retrieve compilation parameters from the file", &compile_dbs, 201 "c|config", conf_help, &conf_file, 202 "db", db_help, &db, 203 "in", "Input file to parse (default: all files in the compilation database)", &data.inFiles, 204 "out", out_help, &workArea.rawRoot, 205 "restrict", restrict_help, &workArea.rawRestrict, 206 ); 207 // dfmt on 208 209 updateCompileDb(compileDb, compile_dbs); 210 } 211 212 void generateMutantG(string[] args) { 213 data.toolMode = ToolMode.generate_mutant; 214 string cli_mutation_id; 215 // dfmt off 216 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 217 "c|config", conf_help, &conf_file, 218 "db", db_help, &db, 219 "out", out_help, &workArea.rawRoot, 220 "restrict", restrict_help, &workArea.rawRestrict, 221 "id", "mutate the source code as mutant ID", &cli_mutation_id, 222 ); 223 // dfmt on 224 225 try { 226 import std.conv : to; 227 228 if (cli_mutation_id.length != 0) 229 data.mutationId = cli_mutation_id.to!long; 230 } catch (ConvException e) { 231 logger.infof("Invalid mutation point '%s'. It must be in the range [0, %s]", 232 cli_mutation_id, long.max); 233 } 234 } 235 236 void testMutantsG(string[] args) { 237 string mutationTester; 238 string mutationCompile; 239 string mutationTestCaseAnalyze; 240 long mutationTesterRuntime; 241 242 data.toolMode = ToolMode.test_mutants; 243 // dfmt off 244 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 245 "build-cmd", "program used to build the application", &mutationCompile, 246 "c|config", conf_help, &conf_file, 247 "db", db_help, &db, 248 "dry-run", "do not write data to the filesystem", &mutationTest.dryRun, 249 "mutant", "kind of mutation to test " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation, 250 "order", "determine in what order mutations are chosen " ~ format("[%(%s|%)]", [EnumMembers!MutationOrder]), &mutationTest.mutationOrder, 251 "out", out_help, &workArea.rawRoot, 252 "restrict", restrict_help, &workArea.rawRestrict, 253 "test-cmd", "program used to run the test suite", &mutationTester, 254 "test-case-analyze-builtin", "builtin analyzer of output from testing frameworks to find failing test cases", &mutationTest.mutationTestCaseBuiltin, 255 "test-case-analyze-cmd", "program used to find what test cases killed the mutant", &mutationTestCaseAnalyze, 256 "test-timeout", "timeout to use for the test suite (msecs)", &mutationTesterRuntime, 257 ); 258 // dfmt on 259 260 if (mutationTester.length != 0) 261 mutationTest.mutationTester = ShellCommand(mutationTester); 262 if (mutationCompile.length != 0) 263 mutationTest.mutationCompile = ShellCommand(mutationCompile); 264 if (mutationTestCaseAnalyze.length != 0) 265 mutationTest.mutationTestCaseAnalyze = Path(mutationTestCaseAnalyze).AbsolutePath; 266 if (mutationTesterRuntime != 0) 267 mutationTest.mutationTesterRuntime = mutationTesterRuntime.dur!"msecs"; 268 } 269 270 void reportG(string[] args) { 271 string[] compile_dbs; 272 string logDir; 273 274 data.toolMode = ToolMode.report; 275 // dfmt off 276 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 277 "compile-db", "Retrieve compilation parameters from the file", &compile_dbs, 278 "c|config", conf_help, &conf_file, 279 "db", db_help, &db, 280 "diff-from-stdin", "report alive mutants in the areas indicated as changed in the diff", &report.unifiedDiff, 281 "level", "the report level of the mutation data " ~ format("[%(%s|%)]", [EnumMembers!ReportLevel]), &report.reportLevel, 282 "logdir", "Directory to write log files to (default: .)", &logDir, 283 "mutant", "kind of mutation to report " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation, 284 "out", out_help, &workArea.rawRoot, 285 "restrict", restrict_help, &workArea.rawRestrict, 286 "section", "sections to include in the report " ~ format("[%(%s|%)]", [EnumMembers!ReportSection]), &report.reportSection, 287 "section-tc_stat-num", "number of test cases to report", &report.tcKillSortNum, 288 "section-tc_stat-sort", "sort order when reporting test case kill stat " ~ format("[%(%s|%)]", [EnumMembers!ReportKillSortOrder]), &report.tcKillSortOrder, 289 "style", "kind of report to generate " ~ format("[%(%s|%)]", [EnumMembers!ReportKind]), &report.reportKind, 290 ); 291 // dfmt on 292 293 if (report.reportSection.length != 0 && report.reportLevel != ReportLevel.summary) { 294 logger.error("Combining --section and --level is not supported"); 295 help_info.helpWanted = true; 296 } 297 298 if (logDir.empty) 299 logDir = "."; 300 report.logDir = logDir.Path.AbsolutePath; 301 302 updateCompileDb(compileDb, compile_dbs); 303 } 304 305 void adminG(string[] args) { 306 bool dump_conf; 307 bool init_conf; 308 data.toolMode = ToolMode.admin; 309 // dfmt off 310 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 311 "c|config", conf_help, &conf_file, 312 "db", db_help, &db, 313 "dump-config", "dump the detailed configuration used", &dump_conf, 314 "init", "create an initial config to use", &init_conf, 315 "mutant", "mutants to operate on " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &data.mutation, 316 "operation", "administrative operation to perform " ~ format("[%(%s|%)]", [EnumMembers!AdminOperation]), &admin.adminOp, 317 "test-case-regex", "regex to use when removing test cases", &admin.testCaseRegex, 318 "status", "change mutants with this state to the value specified by --to-status " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantStatus, 319 "to-status", "reset mutants to state (default: unknown) " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantToStatus, 320 ); 321 // dfmt on 322 323 if (dump_conf) 324 data.toolMode = ToolMode.dumpConfig; 325 else if (init_conf) 326 data.toolMode = ToolMode.initConfig; 327 } 328 329 groups["analyze"] = &analyzerG; 330 groups["generate"] = &generateMutantG; 331 groups["test"] = &testMutantsG; 332 groups["report"] = &reportG; 333 groups["admin"] = &adminG; 334 335 if (args.length < 2) { 336 logger.error("Missing command"); 337 help = true; 338 exitStatus = ExitStatusType.Errors; 339 return; 340 } 341 342 const string cg = args[1]; 343 string[] subargs = args[0 .. 1]; 344 if (args.length > 2) 345 subargs ~= args[2 .. $]; 346 347 if (auto f = cg in groups) { 348 try { 349 // trusted: not any external input. 350 () @trusted { (*f)(subargs); }(); 351 help = help_info.helpWanted; 352 } catch (std.getopt.GetOptException ex) { 353 logger.error(ex.msg); 354 help = true; 355 exitStatus = ExitStatusType.Errors; 356 } catch (Exception ex) { 357 logger.error(ex.msg); 358 help = true; 359 exitStatus = ExitStatusType.Errors; 360 } 361 } else { 362 logger.error("Unknown command: ", cg); 363 help = true; 364 exitStatus = ExitStatusType.Errors; 365 return; 366 } 367 368 import std.algorithm : find; 369 import std.array : array; 370 import std.range : drop; 371 372 if (db.length != 0) 373 data.db = AbsolutePath(FileName(db)); 374 else if (data.db.length == 0) 375 data.db = "dextool_mutate.sqlite3".Path.AbsolutePath; 376 377 if (workArea.rawRoot.length != 0) 378 workArea.outputDirectory = AbsolutePath(Path(workArea.rawRoot.asAbsNormPath)); 379 else if (workArea.outputDirectory.length == 0) { 380 workArea.rawRoot = "."; 381 workArea.outputDirectory = workArea.rawRoot.Path.AbsolutePath; 382 } 383 384 if (workArea.rawRestrict.length != 0) 385 workArea.restrictDir = workArea.rawRestrict.map!(a => AbsolutePath(FileName(a))).array; 386 else if (workArea.restrictDir.length == 0) { 387 workArea.rawRestrict = [workArea.rawRoot]; 388 workArea.restrictDir = [workArea.outputDirectory]; 389 } 390 391 compiler.extraFlags = compiler.extraFlags ~ args.find("--").drop(1).array(); 392 } 393 394 /** 395 * Trusted: 396 * The only input is a static string and data derived from getopt itselt. 397 * Assuming that getopt in phobos behave well. 398 */ 399 void printHelp() @trusted { 400 import std.array : array; 401 import std.algorithm : joiner, sort, map; 402 import std.ascii : newline; 403 import std.stdio : writeln; 404 405 string base_help = "Usage: dextool mutate COMMAND [options]"; 406 407 switch (toolMode) with (ToolMode) { 408 case none: 409 writeln("commands: ", newline, 410 groups.byKey.array.sort.map!(a => " " ~ a).joiner(newline)); 411 break; 412 case analyzer: 413 base_help = "Usage: dextool mutate analyze [options] [-- CFLAGS...]"; 414 break; 415 case generate_mutant: 416 break; 417 case test_mutants: 418 logger.infof("--test-case-analyze-builtin possible values: %(%s|%)", 419 [EnumMembers!TestCaseAnalyzeBuiltin]); 420 break; 421 case report: 422 break; 423 case admin: 424 break; 425 default: 426 break; 427 } 428 429 defaultGetoptPrinter(base_help, help_info.options); 430 } 431 } 432 433 /// Update the config from the users input. 434 void updateCompileDb(ref ConfigCompileDb db, string[] compile_dbs) { 435 import std.array : array; 436 import std.algorithm : filter, map; 437 438 if (compile_dbs.length != 0) 439 db.rawDbs = compile_dbs; 440 db.dbs = db.rawDbs 441 .filter!(a => a.length != 0) 442 .map!(a => Path(a).AbsolutePath) 443 .array; 444 } 445 446 /** Print a help message conveying how files in the compilation database will 447 * be analyzed. 448 * 449 * It must be enough information that the user can adjust `--out` and `--restrict`. 450 */ 451 void printFileAnalyzeHelp(ref ArgParser ap) @safe { 452 logger.infof("Reading compilation database:\n%-(%s\n%)", ap.compileDb.dbs); 453 454 logger.info( 455 "Analyze and mutation of files will only be done on those inside this directory root"); 456 logger.info(" User input: ", ap.workArea.rawRoot); 457 logger.info(" Real path: ", ap.workArea.outputDirectory); 458 logger.info(ap.workArea.rawRestrict.length != 0, 459 "Further restricted to those inside these paths"); 460 461 assert(ap.workArea.rawRestrict.length == ap.workArea.restrictDir.length); 462 foreach (idx; 0 .. ap.workArea.rawRestrict.length) { 463 if (ap.workArea.rawRestrict[idx] == ap.workArea.rawRoot) 464 continue; 465 logger.info(" User input: ", ap.workArea.rawRestrict[idx]); 466 logger.info(" Real path: ", ap.workArea.restrictDir[idx]); 467 } 468 } 469 470 /** Load the configuration from file. 471 * 472 * Example of a TOML configuration 473 * --- 474 * [defaults] 475 * check_name_standard = true 476 * --- 477 */ 478 void loadConfig(ref ArgParser rval) @trusted { 479 import std.algorithm : filter, map; 480 import std.array : array; 481 import std.conv : to; 482 import std.file : exists, readText; 483 import std.path : dirName, buildPath; 484 import toml; 485 486 if (!exists(rval.miniConf.confFile)) 487 return; 488 489 static auto tryLoading(string configFile) { 490 auto txt = readText(configFile); 491 auto doc = parseTOML(txt); 492 return doc; 493 } 494 495 TOMLDocument doc; 496 try { 497 doc = tryLoading(rval.miniConf.confFile); 498 } catch (Exception e) { 499 logger.warning("Unable to read the configuration from ", rval.miniConf.confFile); 500 logger.warning(e.msg); 501 rval.data.exitStatus = ExitStatusType.Errors; 502 return; 503 } 504 505 alias Fn = void delegate(ref ArgParser c, ref TOMLValue v); 506 Fn[string] callbacks; 507 508 callbacks["workarea.root"] = (ref ArgParser c, ref TOMLValue v) { 509 c.workArea.rawRoot = v.str; 510 }; 511 callbacks["workarea.restrict"] = (ref ArgParser c, ref TOMLValue v) { 512 c.workArea.rawRestrict = v.array.map!(a => a.str).array; 513 }; 514 callbacks["database.db"] = (ref ArgParser c, ref TOMLValue v) { 515 c.db = v.str.Path.AbsolutePath; 516 }; 517 callbacks["compile_commands.search_paths"] = (ref ArgParser c, ref TOMLValue v) { 518 c.compileDb.rawDbs = v.array.map!"a.str".array; 519 }; 520 callbacks["compile_commands.filter"] = (ref ArgParser c, ref TOMLValue v) { 521 import dextool.type : FilterClangFlag; 522 523 c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array; 524 }; 525 callbacks["compile_commands.skip_compiler_args"] = (ref ArgParser c, ref TOMLValue v) { 526 c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer; 527 }; 528 529 callbacks["compiler.extra_flags"] = (ref ArgParser c, ref TOMLValue v) { 530 c.compiler.extraFlags = v.array.map!(a => a.str).array; 531 }; 532 callbacks["compiler.force_system_includes"] = (ref ArgParser c, ref TOMLValue v) { 533 c.compiler.forceSystemIncludes = v == true; 534 }; 535 callbacks["compiler.use_compiler_system_includes"] = (ref ArgParser c, ref TOMLValue v) { 536 c.compiler.useCompilerSystemIncludes = v.str; 537 }; 538 539 callbacks["mutant_test.test_cmd"] = (ref ArgParser c, ref TOMLValue v) { 540 c.mutationTest.mutationTester = ShellCommand(v.str); 541 }; 542 callbacks["mutant_test.test_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) { 543 c.mutationTest.mutationTesterRuntime = v.integer.dur!"msecs"; 544 }; 545 callbacks["mutant_test.build_cmd"] = (ref ArgParser c, ref TOMLValue v) { 546 c.mutationTest.mutationCompile = ShellCommand(v.str); 547 }; 548 callbacks["mutant_test.analyze_cmd"] = (ref ArgParser c, ref TOMLValue v) { 549 c.mutationTest.mutationTestCaseAnalyze = Path(v.str).AbsolutePath; 550 }; 551 callbacks["mutant_test.analyze_using_builtin"] = (ref ArgParser c, ref TOMLValue v) { 552 c.mutationTest.mutationTestCaseBuiltin = v.array.map!( 553 a => a.str.to!TestCaseAnalyzeBuiltin).array; 554 }; 555 callbacks["mutant_test.order"] = (ref ArgParser c, ref TOMLValue v) { 556 c.mutationTest.mutationOrder = v.str.to!MutationOrder; 557 }; 558 callbacks["mutant_test.detected_new_test_case"] = (ref ArgParser c, ref TOMLValue v) { 559 try { 560 c.mutationTest.onNewTestCases = v.str.to!(ConfigMutationTest.NewTestCases); 561 } catch (Exception e) { 562 logger.info("Available alternatives: ", 563 [EnumMembers!(ConfigMutationTest.NewTestCases)]); 564 } 565 }; 566 callbacks["mutant_test.detected_dropped_test_case"] = (ref ArgParser c, ref TOMLValue v) { 567 try { 568 c.mutationTest.onRemovedTestCases = v.str.to!(ConfigMutationTest.RemovedTestCases); 569 } catch (Exception e) { 570 logger.info("Available alternatives: ", 571 [EnumMembers!(ConfigMutationTest.RemovedTestCases)]); 572 } 573 }; 574 callbacks["mutant_test.oldest_mutants"] = (ref ArgParser c, ref TOMLValue v) { 575 try { 576 c.mutationTest.onOldMutants = v.str.to!(ConfigMutationTest.OldMutant); 577 } catch (Exception e) { 578 logger.info("Available alternatives: ", [EnumMembers!(ConfigMutationTest.OldMutant)]); 579 } 580 }; 581 callbacks["mutant_test.oldest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) { 582 c.mutationTest.oldMutantsNr = v.integer; 583 }; 584 callbacks["report.style"] = (ref ArgParser c, ref TOMLValue v) { 585 c.report.reportKind = v.str.to!ReportKind; 586 }; 587 588 void iterSection(ref ArgParser c, string sectionName) { 589 if (auto section = sectionName in doc) { 590 // specific configuration from section members 591 foreach (k, v; *section) { 592 if (auto cb = (sectionName ~ "." ~ k) in callbacks) { 593 try { 594 (*cb)(c, v); 595 } catch (Exception e) { 596 logger.error(e.msg).collectException; 597 } 598 } else { 599 logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName); 600 } 601 } 602 } 603 } 604 605 iterSection(rval, "workarea"); 606 iterSection(rval, "database"); 607 iterSection(rval, "compiler"); 608 iterSection(rval, "compile_commands"); 609 iterSection(rval, "mutant_test"); 610 iterSection(rval, "report"); 611 612 parseTestGroups(rval, doc); 613 } 614 615 void parseTestGroups(ref ArgParser c, ref TOMLDocument doc) @trusted { 616 import toml; 617 618 if ("test_group" !in doc) 619 return; 620 621 foreach (k, s; *("test_group" in doc)) { 622 if (s.type != TOML_TYPE.TABLE) 623 continue; 624 625 string desc; 626 if (auto v = "description" in s) 627 desc = v.str; 628 if (auto v = "pattern" in s) { 629 string re = v.str; 630 c.report.testGroups ~= TestGroup(k, desc, re); 631 } 632 } 633 } 634 635 /// Minimal config to setup path to config file. 636 struct MiniConfig { 637 /// Value from the user via CLI, unmodified. 638 string rawConfFile; 639 640 /// The configuration file that has been loaded 641 AbsolutePath confFile; 642 643 bool shortPluginHelp; 644 } 645 646 /// Returns: minimal config to load settings and setup working directory. 647 MiniConfig cliToMiniConfig(string[] args) @trusted nothrow { 648 import std.file : exists; 649 static import std.getopt; 650 651 immutable default_conf = ".dextool_mutate.toml"; 652 653 MiniConfig conf; 654 655 try { 656 std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough, 657 "c|config", "none not visible to the user", &conf.rawConfFile, 658 "short-plugin-help", "not visible to the user", &conf.shortPluginHelp); 659 if (conf.rawConfFile.length == 0) 660 conf.rawConfFile = default_conf; 661 conf.confFile = Path(conf.rawConfFile).AbsolutePath; 662 } catch (Exception e) { 663 logger.trace(conf).collectException; 664 logger.error(e.msg).collectException; 665 } 666 667 return conf; 668 }