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.parallelism : totalCPUs; 21 import std.path : buildPath; 22 import std.traits : EnumMembers; 23 24 import my.filter : GlobFilter; 25 import my.optional; 26 import my.path; 27 28 import toml : TOMLDocument; 29 30 public import dextool.plugin.mutate.backend : Mutation, Language; 31 public import dextool.plugin.mutate.type; 32 import dextool.plugin.mutate.config; 33 import dextool.type : ExitStatusType; 34 35 version (unittest) { 36 import unit_threaded.assertions; 37 } 38 39 @safe: 40 41 // by default use the recommended operators. These are a good default that test 42 // those with relatively few equivalent mutants thus most that survive are 43 // relevant. 44 private immutable MutationKind[] defaultMutants = [ 45 MutationKind.lcr, MutationKind.lcrb, MutationKind.sdl, MutationKind.uoi, 46 MutationKind.dcr 47 ]; 48 49 /// Extract and cleanup user input from the command line. 50 struct ArgParser { 51 import std.typecons : Nullable; 52 import std.conv : ConvException; 53 import std.getopt : GetoptResult, getopt, defaultGetoptPrinter; 54 55 /// Minimal data needed to bootstrap the configuration. 56 MiniConfig miniConf; 57 58 ConfigAdmin admin; 59 ConfigAnalyze analyze; 60 ConfigCompileDb compileDb; 61 ConfigCompiler compiler; 62 ConfigMutationTest mutationTest; 63 ConfigReport report; 64 ConfigWorkArea workArea; 65 ConfigGenerate generate; 66 67 struct Data { 68 AbsolutePath db; 69 ExitStatusType exitStatus = ExitStatusType.Ok; 70 MutationKind[] mutation = defaultMutants; 71 ToolMode toolMode; 72 bool help; 73 string[] inFiles; 74 } 75 76 Data data; 77 alias data this; 78 79 private GetoptResult help_info; 80 81 alias GroupF = void delegate(string[]) @system; 82 GroupF[string] groups; 83 84 /// Returns: a config object with default values. 85 static ArgParser make() @safe { 86 import dextool.compilation_db : defaultCompilerFlagFilter, CompileCommandFilter; 87 88 ArgParser r; 89 r.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 90 return r; 91 } 92 93 /// Convert the configuration to a TOML file. 94 string toTOML() @trusted { 95 import std.ascii : newline; 96 import std.conv : to; 97 import std.format : format; 98 import std.utf : toUTF8; 99 100 auto app = appender!(string[])(); 101 102 app.put("[workarea]"); 103 app.put(null); 104 app.put("# base path (absolute or relative) to look for C/C++ files to mutate."); 105 app.put(`root = "."`); 106 app.put(null); 107 app.put("# only those files that fully match the glob filter will be mutated."); 108 app.put("# glob filter are relative to root."); 109 app.put(`include = ["*"]`); 110 app.put("exclude = []"); 111 app.put(null); 112 113 app.put("[generic]"); 114 app.put(null); 115 app.put("# default mutants to test if none is specified by --mutant"); 116 app.put( 117 "# note that this affects the analyze phase thus the --mutant argument must be one of those specified here"); 118 app.put(format!"# available options are: [%(%s, %)]"( 119 [EnumMembers!MutationKind].map!(a => a.to!string))); 120 app.put(format("mutants = [%(%s, %)]", defaultMutants.map!(a => a.to!string))); 121 app.put(null); 122 app.put("# Use coverage to reduce the tested mutants"); 123 app.put("use_coverage = true"); 124 app.put(null); 125 app.put( 126 "# Default is to inject the runtime in all roots. A root is a file either provided by --in"); 127 app.put("# or a file in compile_commands.json."); 128 app.put( 129 "# If specified then the coverage and schemata runtime is only injected in these files."); 130 app.put("# paths are relative to root."); 131 app.put(`# inject_runtime_impl = [["file1.c", "c"], ["file2.c", "cpp"]]`); 132 app.put(null); 133 134 app.put("[analyze]"); 135 app.put(null); 136 app.put( 137 "# glob filter which include/exclude matched files (relative to root) from analysis."); 138 app.put(`include = ["*"]`); 139 app.put("exclude = []"); 140 app.put(null); 141 app.put("# number of threads to be used for analysis (default is the number of cores)."); 142 app.put(format!"# threads = %s"(totalCPUs)); 143 app.put(null); 144 app.put("# remove files from the database that are no longer found during analysis."); 145 app.put(`prune = true`); 146 app.put(null); 147 app.put("# minimum number of mutants per schema."); 148 app.put(format!"# min_mutants_per_schema = %s"(analyze.minMutantsPerSchema.get)); 149 app.put(null); 150 app.put("# maximum number of mutants per schema (zero means no limit)."); 151 app.put(format!"# mutants_per_schema = %s"(analyze.mutantsPerSchema.get)); 152 app.put(null); 153 app.put("# checksum the files in this directories and warn if a mutation status is older"); 154 app.put("# than the newest file. The path can be a file or a directory. Directories"); 155 app.put("# are traveresed. All paths are relative to root."); 156 app.put(`# test_paths = ["test/suite1", "test/mytest.cpp"]`); 157 app.put(null); 158 app.put( 159 "# glob filter which include/exclude matched test files (relative to root) from analysis."); 160 app.put(`# test_include = ["*/*.ext"]`); 161 app.put("# test_exclude = []"); 162 app.put(null); 163 164 app.put("[database]"); 165 app.put(null); 166 app.put("# path (absolute or relative) where mutation statistics will be stored."); 167 app.put(`db = "dextool_mutate.sqlite3"`); 168 app.put(null); 169 170 app.put("[compiler]"); 171 app.put(null); 172 app.put("# extra flags to pass on to the compiler such as the C++ standard."); 173 app.put(format(`# extra_flags = [%(%s, %)]`, compiler.extraFlags)); 174 app.put(null); 175 app.put("# force system includes to use -I instead of -isystem"); 176 app.put(format!"# force_system_includes = %s"(compiler.forceSystemIncludes)); 177 app.put(null); 178 app.put("# system include paths to use instead of the ones in compile_commands.json"); 179 app.put(format(`# use_compiler_system_includes = "%s"`, compiler.useCompilerSystemIncludes.length == 0 180 ? "/path/to/c++" : compiler.useCompilerSystemIncludes.value)); 181 app.put(null); 182 app.put("# allow compilation errors."); 183 app.put("# This is useful to active when clang complain about e.g. gcc specific builtins"); 184 app.put("# allow_errors = true"); 185 app.put(null); 186 187 app.put("[compile_commands]"); 188 app.put(null); 189 app.put("# files and/or directories to look for compile_commands.json."); 190 if (compileDb.dbs.length == 0) 191 app.put(`search_paths = ["./compile_commands.json"]`); 192 else 193 app.put(format("search_paths = %s", compileDb.rawDbs)); 194 app.put(null); 195 app.put("# compile flags to remove when analyzing a file."); 196 app.put(format("# filter = [%(%s, %)]", compileDb.flagFilter.filter)); 197 app.put(null); 198 app.put("# number of compiler arguments to skip from the beginning (needed when the first argument is NOT a compiler but rather a wrapper)."); 199 app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs)); 200 app.put(null); 201 202 app.put("[mutant_test]"); 203 app.put(null); 204 app.put("# command to build the program **and** test suite."); 205 app.put(format!`build_cmd = ["cd build && make -j%s"]`(totalCPUs)); 206 app.put(null); 207 app.put("# at least one of test_cmd_dir (recommended) or test_cmd needs to be specified."); 208 app.put(null); 209 app.put(`# path(s) to recursively look for test binaries to execute.`); 210 app.put(`test_cmd_dir = ["./build/test"]`); 211 app.put(null); 212 app.put(`# flags to add to all executables found in test_cmd_dir.`); 213 app.put(`# test_cmd_dir_flag = ["--gtest_filter", "-foo*"]`); 214 app.put(null); 215 app.put("# command(s) to test the program."); 216 app.put("# the arguments for test_cmd can be an array of multiple test commands"); 217 app.put(`# 1. ["test1.sh", "test2.sh"]`); 218 app.put(`# 2. [["test1.sh", "-x"], "test2.sh"]`); 219 app.put(`# 3. [["/bin/make", "test"]]`); 220 app.put(`# test_cmd = ["./test.sh"]`); 221 app.put(null); 222 app.put( 223 "# timeout to use for the test suite (by default a measurement-based heuristic will be used)."); 224 app.put(`# test_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`); 225 app.put(null); 226 app.put("# timeout to use when compiling the program and test suite (default: 30 minutes)"); 227 app.put(`# build_cmd_timeout = "1 hours 1 minutes 1 seconds 1 msecs"`); 228 app.put(null); 229 app.put( 230 "# program used to analyze the output from the test suite for test cases that killed the mutant"); 231 app.put(`# analyze_cmd = "analyze.sh"`); 232 app.put(null); 233 app.put("# built-in analyzer of output from testing frameworks to find failing test cases"); 234 app.put(format("# analyze_using_builtin = [%(%s, %)]", 235 [EnumMembers!TestCaseAnalyzeBuiltin].map!(a => a.to!string))); 236 app.put(null); 237 app.put("# determine in what order mutants are chosen"); 238 app.put(format("# available options are: %(%s %)", 239 [EnumMembers!MutationOrder].map!(a => a.to!string))); 240 app.put(format(`# order = "%s"`, MutationOrder.random)); 241 app.put(null); 242 app.put("# how to behave when new test cases are found"); 243 app.put(format("# available options are: %(%s %)", 244 [EnumMembers!(ConfigMutationTest.NewTestCases)].map!(a => a.to!string))); 245 app.put(`detected_new_test_case = "resetAlive"`); 246 app.put(null); 247 app.put("# how to behave when test cases are detected as having been removed"); 248 app.put("# should the test and the gathered statistics be removed too?"); 249 app.put(format("# available options are: %(%s %)", 250 [EnumMembers!(ConfigMutationTest.RemovedTestCases)].map!(a => a.to!string))); 251 app.put(`detected_dropped_test_case = "remove"`); 252 app.put(null); 253 app.put("# how the oldest mutants should be treated."); 254 app.put("# It is recommended to test them again."); 255 app.put("# Because you may have changed the test suite so mutants that where previously killed by the test suite now survive."); 256 app.put(format!"# available options are: %(%s %)"( 257 [EnumMembers!(ConfigMutationTest.OldMutant)].map!(a => a.to!string))); 258 app.put(`oldest_mutants = "test"`); 259 app.put(null); 260 app.put("# how many of the oldest mutants to do the above with"); 261 app.put("# oldest_mutants_nr = 10"); 262 app.put("# how many of the oldest mutants to do the above with"); 263 app.put("oldest_mutants_percentage = 1.0"); 264 app.put(null); 265 app.put( 266 "# number of threads to be used when running tests in parallel (default is the number of cores)."); 267 app.put(format!"# parallel_test = %s"(totalCPUs)); 268 app.put(null); 269 app.put("# stop executing tests as soon as a test command fails."); 270 app.put( 271 "# This speed up the test phase but the report of test cases killing mutants is less accurate"); 272 app.put("use_early_stop = true"); 273 app.put(null); 274 app.put("# reduce the compile and link time when testing mutants"); 275 app.put("use_schemata = true"); 276 app.put(null); 277 app.put("# sanity check the schemata before it is used by executing the test cases"); 278 app.put("# it is a slowdown but nice robustness that is usually worth having"); 279 app.put("check_schemata = true"); 280 app.put(null); 281 app.put(`# Enable continues sanity check of the build environment and test suite.`); 282 app.put( 283 `# Run the test suite every 100 mutant to see that the test suite is OK when no mutants are injected.`); 284 app.put( 285 `# If the test suite fails the previous 100 mutants will be reverted and mutation testing stops.`); 286 app.put(`continues_check_test_suite = true`); 287 app.put(null); 288 app.put("# How often the test suite check is performed"); 289 app.put(format!`continues_check_test_suite_period = %s`( 290 mutationTest.contCheckTestSuitePeriod.get)); 291 app.put(null); 292 293 app.put("[report]"); 294 app.put(null); 295 app.put("# default style to use"); 296 app.put(format("# available options are: %(%s %)", 297 [EnumMembers!ReportKind].map!(a => a.to!string))); 298 app.put(format!`style = "%s"`(report.reportKind)); 299 app.put(null); 300 app.put("# default report sections when no --section is specified"); 301 app.put(format!"# available options are: [%(%s, %)]"( 302 [EnumMembers!ReportSection].map!(a => a.to!string))); 303 app.put(format!"sections = [%(%s, %)]"(report.reportSection.map!(a => a.to!string))); 304 app.put(null); 305 app.put("# how many mutants to show in the high interest section"); 306 app.put("# high_interest_mutants_nr = 5"); 307 app.put(null); 308 309 app.put("[test_group]"); 310 app.put(null); 311 app.put("# subgroups with a description and pattern. Example:"); 312 app.put("# [test_group.uc1]"); 313 app.put(`# description = "use case 1"`); 314 app.put(`# pattern = "uc_1.*"`); 315 app.put(`# see for regex syntax: http://dlang.org/phobos/std_regex.html`); 316 app.put(null); 317 318 return app.data.joiner(newline).toUTF8; 319 } 320 321 void parse(string[] args) { 322 import std.format : format; 323 324 static import std.getopt; 325 326 const db_help = "sqlite3 database to use (default: dextool_mutate.sqlite3)"; 327 const include_help = "only mutate the files matching at least one of the patterns (default: *)"; 328 const exclude_help = "do not mutate the files matching any the patterns (default: <empty>)"; 329 const out_help = "path used as the root for mutation/reporting of files (default: .)"; 330 const conf_help = "load configuration (default: .dextool_mutate.toml)"; 331 332 // specified by command line. if set it overwride the one from the config. 333 MutationKind[] mutants; 334 335 // not used but need to be here. The one used is in MiniConfig. 336 string conf_file; 337 string db = data.db; 338 339 void analyzerG(string[] args) { 340 bool noPrune; 341 string[] compileDbs; 342 343 data.toolMode = ToolMode.analyzer; 344 // dfmt off 345 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 346 "allow-errors", "allow compilation errors during analysis (default: false)", compiler.allowErrors.getPtr, 347 "compile-db", "Retrieve compilation parameters from the file", &compileDbs, 348 "c|config", conf_help, &conf_file, 349 "db", db_help, &db, 350 "diff-from-stdin", "restrict testing to the mutants in the diff", &analyze.unifiedDiffFromStdin, 351 "exclude", exclude_help, &workArea.rawExclude, 352 "fast-db-store", "improve the write speed of the analyze result (may corrupt the database)", &analyze.fastDbStore, 353 "file-exclude", "glob filter which exclude matched files (relative to root) from analysis (default: <empty>)", &analyze.rawExclude, 354 "file-include", "glob filter which include matched files (relative to root) for analysis (default: *)", &analyze.rawInclude, 355 "force-save", "force the result from the analyze to be saved", &analyze.forceSaveAnalyze, 356 "in", "Input file to parse (default: all files in the compilation database)", &data.inFiles, 357 "include", include_help, &workArea.rawInclude, 358 "m|mutant", "kind of mutation save in the database " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants, 359 "no-prune", "do not prune the database of files that aren't found during the analyze", &noPrune, 360 "out", out_help, &workArea.rawRoot, 361 "profile", "print performance profile for the analyzers that are part of the report", &analyze.profile, 362 "schema-min-mutants", "mini number of mutants per schema", analyze.minMutantsPerSchema.getPtr, 363 "schema-mutants", "number of mutants per schema (soft upper limit)", analyze.mutantsPerSchema.getPtr, 364 "threads", "number of threads to use for analysing files (default: CPU cores available)", &analyze.poolSize, 365 ); 366 // dfmt on 367 368 analyze.prune = !noPrune; 369 updateCompileDb(compileDb, compileDbs); 370 } 371 372 void generateMutantG(string[] args) { 373 data.toolMode = ToolMode.generate_mutant; 374 // dfmt off 375 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 376 "c|config", conf_help, &conf_file, 377 "db", db_help, &db, 378 "exclude", exclude_help, &workArea.rawExclude, 379 "include", include_help, &workArea.rawInclude, 380 "out", out_help, &workArea.rawRoot, 381 std.getopt.config.required, "id", "mutate the source code as mutant ID", &generate.mutationId, 382 ); 383 // dfmt on 384 } 385 386 void testMutantsG(string[] args) { 387 import std.datetime : Clock; 388 389 string[] mutationTester; 390 string mutationCompile; 391 string[] mutationTestCaseAnalyze; 392 long mutationTesterRuntime; 393 string maxRuntime; 394 string[] testConstraint; 395 int maxAlive = -1; 396 397 mutationTest.loadThreshold.get = totalCPUs + 1; 398 399 data.toolMode = ToolMode.test_mutants; 400 // dfmt off 401 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 402 "L", "restrict testing to the requested files and lines (<file>:<start>-<end>)", &testConstraint, 403 "build-cmd", "program used to build the application", &mutationCompile, 404 "cont-test-suite", "enable continues check of the test suite", mutationTest.contCheckTestSuite.getPtr, 405 "cont-test-suite-period", "how often to check the test suite", mutationTest.contCheckTestSuitePeriod.getPtr, 406 "c|config", conf_help, &conf_file, 407 "db", db_help, &db, 408 "diff-from-stdin", "restrict testing to the mutants in the diff", &mutationTest.unifiedDiffFromStdin, 409 "dry-run", "do not write data to the filesystem", &mutationTest.dryRun, 410 "exclude", exclude_help, &workArea.rawExclude, 411 "include", include_help, &workArea.rawInclude, 412 "load-behavior", "how to behave when the threshold is hit " ~ format("[%(%s|%)]", [EnumMembers!(ConfigMutationTest.LoadBehavior)]), &mutationTest.loadBehavior, 413 "load-threshold", format!"the 15min loadavg threshold (default: %s)"(mutationTest.loadThreshold.get), mutationTest.loadThreshold.getPtr, 414 "log-coverage", "write the instrumented coverage files to a separate file", mutationTest.logCoverage.getPtr, 415 "max-alive", "stop after NR alive mutants is found (only effective with -L or --diff-from-stdin)", &maxAlive, 416 "max-runtime", format("max time to run the mutation testing for (default: %s)", mutationTest.maxRuntime), &maxRuntime, 417 "m|mutant", "kind of mutation to test " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants, 418 "order", "determine in what order mutants are chosen " ~ format("[%(%s|%)]", [EnumMembers!MutationOrder]), &mutationTest.mutationOrder, 419 "out", out_help, &workArea.rawRoot, 420 "schema-check", "sanity check a schemata before it is used", &mutationTest.sanityCheckSchemata, 421 "schema-log", "write mutant schematan to a separate file for later inspection", &mutationTest.logSchemata, 422 "schema-min-mutants", "mini number of mutants per schema", mutationTest.minMutantsPerSchema.getPtr, 423 "schema-only", "stop testing after the last schema has been executed", &mutationTest.stopAfterLastSchema, 424 "schema-use", "use schematas to speed-up testing", &mutationTest.useSchemata, 425 "test-case-analyze-builtin", "builtin analyzer of output from testing frameworks to find failing test cases", &mutationTest.mutationTestCaseBuiltin, 426 "test-case-analyze-cmd", "program used to find what test cases killed the mutant", &mutationTestCaseAnalyze, 427 "test-cmd", "program used to run the test suite", &mutationTester, 428 "test-timeout", "timeout to use for the test suite (msecs)", &mutationTesterRuntime, 429 "use-early-stop", "stop executing tests for a mutant as soon as one kill a mutant to speed-up testing", &mutationTest.useEarlyTestCmdStop, 430 ); 431 // dfmt on 432 433 if (maxAlive > 0) 434 mutationTest.maxAlive = maxAlive; 435 if (mutationTester.length != 0) 436 mutationTest.mutationTester = mutationTester.map!(a => ShellCommand([ 437 a 438 ])).array; 439 if (mutationCompile.length != 0) 440 mutationTest.mutationCompile = ShellCommand([mutationCompile]); 441 if (mutationTestCaseAnalyze.length != 0) 442 mutationTest.mutationTestCaseAnalyze = mutationTestCaseAnalyze.map!( 443 a => ShellCommand([a])).array; 444 if (mutationTesterRuntime != 0) 445 mutationTest.mutationTesterRuntime = mutationTesterRuntime.dur!"msecs"; 446 if (!maxRuntime.empty) 447 mutationTest.maxRuntime = parseDuration(maxRuntime); 448 mutationTest.constraint = parseUserTestConstraint(testConstraint); 449 } 450 451 void reportG(string[] args) { 452 string[] compileDbs; 453 string logDir; 454 455 data.toolMode = ToolMode.report; 456 ReportSection[] sections; 457 // dfmt off 458 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 459 "compile-db", "Retrieve compilation parameters from the file", &compileDbs, 460 "c|config", conf_help, &conf_file, 461 "db", db_help, &db, 462 "diff-from-stdin", "report alive mutants in the areas indicated as changed in the diff", &report.unifiedDiff, 463 "exclude", exclude_help, &workArea.rawExclude, 464 "high-interest-mutants-nr", "nr of mutants to show in the section", report.highInterestMutantsNr.getPtr, 465 "include", include_help, &workArea.rawInclude, 466 "logdir", "Directory to write log files to (default: .)", &logDir, 467 "m|mutant", "kind of mutation to report " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants, 468 "out", out_help, &workArea.rawRoot, 469 "profile", "print performance profile for the analyzers that are part of the report", &report.profile, 470 "section", "sections to include in the report " ~ format("[%-(%s|%)]", [EnumMembers!ReportSection]), §ions, 471 "section-tc_stat-num", "number of test cases to report", &report.tcKillSortNum, 472 "section-tc_stat-sort", "sort order when reporting test case kill stat " ~ format("[%(%s|%)]", [EnumMembers!ReportKillSortOrder]), &report.tcKillSortOrder, 473 "style", "kind of report to generate " ~ format("[%(%s|%)]", [EnumMembers!ReportKind]), &report.reportKind, 474 ); 475 // dfmt on 476 477 if (logDir.empty) 478 logDir = "."; 479 report.logDir = logDir.Path.AbsolutePath; 480 if (!sections.empty) 481 report.reportSection = sections; 482 483 updateCompileDb(compileDb, compileDbs); 484 } 485 486 void adminG(string[] args) { 487 bool dump_conf; 488 bool init_conf; 489 data.toolMode = ToolMode.admin; 490 // dfmt off 491 help_info = getopt(args, std.getopt.config.keepEndOfOptions, 492 "c|config", conf_help, &conf_file, 493 "db", db_help, &db, 494 "dump-config", "dump the detailed configuration used", &dump_conf, 495 "init", "create an initial config to use", &init_conf, 496 "m|mutant", "mutants to operate on " ~ format("[%(%s|%)]", [EnumMembers!MutationKind]), &mutants, 497 "mutant-sub-kind", "kind of mutant " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Kind)]), &admin.subKind, 498 "operation", "administrative operation to perform " ~ format("[%(%s|%)]", [EnumMembers!AdminOperation]), &admin.adminOp, 499 "test-case-regex", "regex to use when removing test cases", &admin.testCaseRegex, 500 "status", "change mutants with this state to the value specified by --to-status " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantStatus, 501 "to-status", "reset mutants to state (default: unknown) " ~ format("[%(%s|%)]", [EnumMembers!(Mutation.Status)]), &admin.mutantToStatus, 502 "id", "specify mutant by Id", &admin.mutationId, 503 "rationale", "rationale for marking mutant", &admin.mutantRationale, 504 "out", out_help, &workArea.rawRoot, 505 ); 506 // dfmt on 507 508 if (dump_conf) 509 data.toolMode = ToolMode.dumpConfig; 510 else if (init_conf) 511 data.toolMode = ToolMode.initConfig; 512 } 513 514 groups["analyze"] = &analyzerG; 515 groups["generate"] = &generateMutantG; 516 groups["test"] = &testMutantsG; 517 groups["report"] = &reportG; 518 groups["admin"] = &adminG; 519 520 if (args.length < 2) { 521 logger.error("Missing command"); 522 help = true; 523 exitStatus = ExitStatusType.Errors; 524 return; 525 } 526 527 const string cg = args[1]; 528 string[] subargs = args[0 .. 1]; 529 if (args.length > 2) 530 subargs ~= args[2 .. $]; 531 532 if (auto f = cg in groups) { 533 try { 534 // trusted: not any external input. 535 () @trusted { (*f)(subargs); }(); 536 help = help_info.helpWanted; 537 } catch (std.getopt.GetOptException ex) { 538 logger.error(ex.msg); 539 help = true; 540 exitStatus = ExitStatusType.Errors; 541 } catch (Exception ex) { 542 logger.error(ex.msg); 543 help = true; 544 exitStatus = ExitStatusType.Errors; 545 } 546 } else { 547 logger.error("Unknown command: ", cg); 548 help = true; 549 exitStatus = ExitStatusType.Errors; 550 return; 551 } 552 553 import std.algorithm : find; 554 import std.range : drop; 555 556 if (db.empty) { 557 db = "dextool_mutate.sqlite3"; 558 } 559 data.db = AbsolutePath(Path(db)); 560 561 if (workArea.rawRoot.empty) { 562 workArea.rawRoot = "."; 563 } 564 workArea.root = workArea.rawRoot.Path.AbsolutePath; 565 566 if (workArea.rawInclude.empty) { 567 workArea.rawInclude = ["*"]; 568 } 569 workArea.mutantMatcher = GlobFilter(workArea.rawInclude.map!(a => buildPath(workArea.root, 570 a)).array, workArea.rawExclude.map!(a => buildPath(workArea.root, a)).array,); 571 572 if (analyze.rawInclude.empty) { 573 analyze.rawInclude = ["*"]; 574 } 575 analyze.fileMatcher = GlobFilter(analyze.rawInclude.map!(a => buildPath(workArea.root, 576 a)).array, analyze.rawExclude.map!(a => buildPath(workArea.root, a)).array); 577 578 analyze.testPaths = analyze.rawTestPaths.map!( 579 a => AbsolutePath(buildPath(workArea.root, a))).array; 580 if (analyze.rawTestInclude.empty) { 581 analyze.rawTestInclude = ["*"]; 582 } 583 analyze.testFileMatcher = GlobFilter(analyze.rawTestInclude.map!( 584 a => buildPath(workArea.root, a)).array, 585 analyze.rawTestExclude.map!(a => buildPath(workArea.root, a)).array); 586 587 if (!mutants.empty) { 588 data.mutation = mutants; 589 } 590 591 compiler.extraFlags = compiler.extraFlags ~ args.find("--").drop(1).array(); 592 } 593 594 /** 595 * Trusted: 596 * The only input is a static string and data derived from getopt itselt. 597 * Assuming that getopt in phobos behave well. 598 */ 599 void printHelp() @trusted { 600 import std.ascii : newline; 601 import std.stdio : writeln; 602 603 string base_help = "Usage: dextool mutate COMMAND [options]"; 604 605 switch (toolMode) with (ToolMode) { 606 case none: 607 writeln("commands: ", newline, 608 groups.byKey.array.sort.map!(a => " " ~ a).joiner(newline)); 609 break; 610 case analyzer: 611 base_help = "Usage: dextool mutate analyze [options] [-- CFLAGS...]"; 612 break; 613 case generate_mutant: 614 break; 615 case test_mutants: 616 logger.infof("--test-case-analyze-builtin possible values: %(%s|%)", 617 [EnumMembers!TestCaseAnalyzeBuiltin]); 618 logger.infof( 619 "--max-runtime supported units are [weeks, days, hours, minutes, seconds, msecs]"); 620 logger.infof(`example: --max-runtime "1 hours 30 minutes"`); 621 break; 622 case report: 623 break; 624 case admin: 625 break; 626 default: 627 break; 628 } 629 630 defaultGetoptPrinter(base_help, help_info.options); 631 } 632 } 633 634 /// Replace the config from the users input. 635 void updateCompileDb(ref ConfigCompileDb db, string[] compileDbs) { 636 if (compileDbs.length != 0) 637 db.rawDbs = compileDbs; 638 639 db.dbs = db.rawDbs 640 .filter!(a => a.length != 0) 641 .map!(a => Path(a).AbsolutePath) 642 .array; 643 } 644 645 /** Print a help message conveying how files in the compilation database will 646 * be analyzed. 647 * 648 * It must be enough information that the user can adjust `--out` and `--include`. 649 */ 650 void printFileAnalyzeHelp(ref ArgParser ap) @safe { 651 static void printPath(string user, string real_) { 652 logger.info(" User: ", user); 653 logger.info(" Real: ", real_); 654 } 655 656 logger.infof("Reading compilation database:\n%-(%s\n%)", ap.compileDb.dbs); 657 658 logger.info( 659 "Analyze and mutation of files will only be done on those inside this directory root"); 660 printPath(ap.workArea.rawRoot, ap.workArea.root); 661 662 logger.info(!ap.workArea.rawInclude.empty, 663 "Only mutating files matching any of the following glob patterns:"); 664 foreach (idx; 0 .. ap.workArea.rawInclude.length) { 665 printPath(ap.workArea.rawInclude[idx], ap.workArea.mutantMatcher.include[idx]); 666 } 667 logger.info(!ap.workArea.rawExclude.empty, 668 "Excluding mutation of files matching any of the following glob patterns:"); 669 foreach (idx; 0 .. ap.workArea.rawExclude.length) { 670 printPath(ap.workArea.rawExclude[idx], ap.workArea.mutantMatcher.exclude[idx]); 671 } 672 673 logger.info(!ap.analyze.fileMatcher.include.empty, 674 "Only analyzing files matching any of the following glob patterns"); 675 foreach (idx; 0 .. ap.analyze.rawInclude.length) { 676 printPath(ap.analyze.rawInclude[idx], ap.analyze.fileMatcher.include[idx]); 677 } 678 679 logger.info(!ap.analyze.rawExclude.empty, 680 "Excluding files matching any of the following glob patterns from analysis"); 681 foreach (idx; 0 .. ap.analyze.rawExclude.length) { 682 printPath(ap.analyze.rawExclude[idx], ap.analyze.fileMatcher.exclude[idx]); 683 } 684 } 685 686 /** Load the configuration from file. 687 * 688 * Example of a TOML configuration 689 * --- 690 * [defaults] 691 * check_name_standard = true 692 * --- 693 */ 694 void loadConfig(ref ArgParser rval) @trusted { 695 import std.file : exists, readText; 696 import toml; 697 698 if (!exists(rval.miniConf.confFile)) 699 return; 700 701 static auto tryLoading(string configFile) { 702 auto txt = readText(configFile); 703 auto doc = parseTOML(txt); 704 return doc; 705 } 706 707 TOMLDocument doc; 708 try { 709 doc = tryLoading(rval.miniConf.confFile); 710 } catch (Exception e) { 711 logger.warning("Unable to read the configuration from ", rval.miniConf.confFile); 712 logger.warning(e.msg); 713 rval.data.exitStatus = ExitStatusType.Errors; 714 return; 715 } 716 717 rval = loadConfig(rval, doc); 718 } 719 720 ArgParser loadConfig(ArgParser rval, ref TOMLDocument doc) @trusted { 721 import std.conv : to; 722 import std.path : dirName, buildPath; 723 import toml; 724 725 alias Fn = void delegate(ref ArgParser c, ref TOMLValue v); 726 Fn[string] callbacks; 727 728 static ShellCommand toShellCommand(ref TOMLValue v, string errorMsg) { 729 if (v.type == TOML_TYPE.STRING) { 730 return ShellCommand([v.str]); 731 } else if (v.type == TOML_TYPE.ARRAY) { 732 return ShellCommand(v.array.map!(a => a.str).array); 733 } 734 logger.warning(errorMsg); 735 return ShellCommand.init; 736 } 737 738 static ShellCommand[] toShellCommands(ref TOMLValue v, string errorMsg) { 739 import std.format : format; 740 741 if (v.type == TOML_TYPE.STRING) { 742 return [ShellCommand([v.str])]; 743 } else if (v.type == TOML_TYPE.ARRAY) { 744 return v.array.map!(a => toShellCommand(a, 745 format!"%s: failed to parse as an array"(errorMsg))).array; 746 } 747 logger.warning(errorMsg); 748 return ShellCommand[].init; 749 } 750 751 static UserRuntime toUserRuntime(ref TOMLValue v) { 752 if (v.type != TOML_TYPE.ARRAY) 753 throw new Exception("the data must be an array of arrays"); 754 auto tmp = v.array; 755 if (tmp.length != 2) 756 throw new Exception("the inner array must be size 2"); 757 try { 758 return UserRuntime(Path(tmp[0].str), tmp[1].str.to!Language); 759 } catch (Exception e) { 760 logger.warningf("Available options for language are %-(%s, %)", 761 [EnumMembers!Language]); 762 throw e; 763 } 764 } 765 766 callbacks["analyze.include"] = (ref ArgParser c, ref TOMLValue v) { 767 c.analyze.rawInclude = v.array.map!(a => a.str).array; 768 }; 769 callbacks["analyze.exclude"] = (ref ArgParser c, ref TOMLValue v) { 770 c.analyze.rawExclude = v.array.map!(a => a.str).array; 771 }; 772 callbacks["analyze.threads"] = (ref ArgParser c, ref TOMLValue v) { 773 c.analyze.poolSize = cast(int) v.integer; 774 }; 775 callbacks["analyze.prune"] = (ref ArgParser c, ref TOMLValue v) { 776 c.analyze.prune = v == true; 777 }; 778 callbacks["analyze.mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) { 779 c.analyze.mutantsPerSchema.get = cast(int) v.integer; 780 }; 781 callbacks["analyze.min_mutants_per_schema"] = (ref ArgParser c, ref TOMLValue v) { 782 c.analyze.minMutantsPerSchema.get = cast(int) v.integer; 783 }; 784 callbacks["analyze.test_paths"] = (ref ArgParser c, ref TOMLValue v) { 785 try { 786 c.analyze.rawTestPaths = v.array.map!(a => a.str).array; 787 } catch (Exception e) { 788 logger.error(e.msg); 789 } 790 }; 791 callbacks["analyze.test_include"] = (ref ArgParser c, ref TOMLValue v) { 792 c.analyze.rawTestInclude = v.array.map!(a => a.str).array; 793 }; 794 callbacks["analyze.test_exclude"] = (ref ArgParser c, ref TOMLValue v) { 795 c.analyze.rawTestExclude = v.array.map!(a => a.str).array; 796 }; 797 798 callbacks["workarea.root"] = (ref ArgParser c, ref TOMLValue v) { 799 c.workArea.rawRoot = v.str; 800 }; 801 callbacks["workarea.restrict"] = (ref ArgParser c, ref TOMLValue v) { 802 throw new Exception( 803 "workarea.restrict is deprecated. Use workarea.exclude instead as glob patterns"); 804 }; 805 callbacks["workarea.include"] = (ref ArgParser c, ref TOMLValue v) { 806 c.workArea.rawInclude = v.array.map!(a => a.str).array; 807 }; 808 callbacks["workarea.exclude"] = (ref ArgParser c, ref TOMLValue v) { 809 c.workArea.rawExclude = v.array.map!(a => a.str).array; 810 }; 811 812 callbacks["generic.mutants"] = (ref ArgParser c, ref TOMLValue v) { 813 try { 814 c.mutation = v.array.map!(a => a.str.to!MutationKind).array; 815 } catch (Exception e) { 816 logger.info("Available mutation kinds ", [EnumMembers!MutationKind]); 817 logger.error(e.msg); 818 } 819 }; 820 callbacks["generic.use_coverage"] = (ref ArgParser c, ref TOMLValue v) { 821 c.analyze.saveCoverage.get = v == true; 822 c.mutationTest.useCoverage.get = v == true; 823 }; 824 callbacks["generic.inject_runtime_impl"] = (ref ArgParser c, ref TOMLValue v) { 825 try { 826 c.mutationTest.userRuntimeCtrl = v.array.map!(a => toUserRuntime(a)).array; 827 } catch (Exception e) { 828 logger.error("generic.inject_runtime_impl: failed parsing"); 829 logger.error(e.msg); 830 } 831 }; 832 833 callbacks["database.db"] = (ref ArgParser c, ref TOMLValue v) { 834 c.db = v.str.Path.AbsolutePath; 835 }; 836 837 callbacks["compile_commands.search_paths"] = (ref ArgParser c, ref TOMLValue v) { 838 c.compileDb.rawDbs = v.array.map!"a.str".array; 839 }; 840 callbacks["compile_commands.filter"] = (ref ArgParser c, ref TOMLValue v) { 841 import dextool.type : FilterClangFlag; 842 843 c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array; 844 }; 845 callbacks["compile_commands.skip_compiler_args"] = (ref ArgParser c, ref TOMLValue v) { 846 c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer; 847 }; 848 849 callbacks["compiler.extra_flags"] = (ref ArgParser c, ref TOMLValue v) { 850 c.compiler.extraFlags = v.array.map!(a => a.str).array; 851 }; 852 callbacks["compiler.force_system_includes"] = (ref ArgParser c, ref TOMLValue v) { 853 c.compiler.forceSystemIncludes = v == true; 854 }; 855 callbacks["compiler.use_compiler_system_includes"] = (ref ArgParser c, ref TOMLValue v) { 856 c.compiler.useCompilerSystemIncludes = v.str; 857 }; 858 859 callbacks["compiler.allow_errors"] = (ref ArgParser c, ref TOMLValue v) { 860 c.compiler.allowErrors.get = v == true; 861 }; 862 863 callbacks["mutant_test.test_cmd"] = (ref ArgParser c, ref TOMLValue v) { 864 c.mutationTest.mutationTester = toShellCommands(v, 865 "config: failed to parse mutant_test.test_cmd"); 866 }; 867 callbacks["mutant_test.test_cmd_dir"] = (ref ArgParser c, ref TOMLValue v) { 868 c.mutationTest.testCommandDir = v.array.map!(a => Path(a.str)).array; 869 }; 870 callbacks["mutant_test.test_cmd_dir_flag"] = (ref ArgParser c, ref TOMLValue v) { 871 c.mutationTest.testCommandDirFlag = v.array.map!(a => a.str).array; 872 }; 873 callbacks["mutant_test.test_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) { 874 c.mutationTest.mutationTesterRuntime = v.str.parseDuration; 875 }; 876 callbacks["mutant_test.build_cmd"] = (ref ArgParser c, ref TOMLValue v) { 877 c.mutationTest.mutationCompile = toShellCommand(v, 878 "config: failed to parse mutant_test.build_cmd"); 879 }; 880 callbacks["mutant_test.build_cmd_timeout"] = (ref ArgParser c, ref TOMLValue v) { 881 c.mutationTest.buildCmdTimeout = v.str.parseDuration; 882 }; 883 callbacks["mutant_test.analyze_cmd"] = (ref ArgParser c, ref TOMLValue v) { 884 c.mutationTest.mutationTestCaseAnalyze = toShellCommands(v, 885 "config: failed to parse mutant_test.analyze_cmd"); 886 }; 887 callbacks["mutant_test.analyze_using_builtin"] = (ref ArgParser c, ref TOMLValue v) { 888 c.mutationTest.mutationTestCaseBuiltin = v.array.map!( 889 a => a.str.to!TestCaseAnalyzeBuiltin).array; 890 }; 891 callbacks["mutant_test.order"] = (ref ArgParser c, ref TOMLValue v) { 892 c.mutationTest.mutationOrder = v.str.to!MutationOrder; 893 }; 894 callbacks["mutant_test.detected_new_test_case"] = (ref ArgParser c, ref TOMLValue v) { 895 try { 896 c.mutationTest.onNewTestCases = v.str.to!(ConfigMutationTest.NewTestCases); 897 } catch (Exception e) { 898 logger.info("Available alternatives: ", 899 [EnumMembers!(ConfigMutationTest.NewTestCases)]); 900 logger.error(e.msg); 901 } 902 }; 903 callbacks["mutant_test.detected_dropped_test_case"] = (ref ArgParser c, ref TOMLValue v) { 904 try { 905 c.mutationTest.onRemovedTestCases = v.str.to!(ConfigMutationTest.RemovedTestCases); 906 } catch (Exception e) { 907 logger.info("Available alternatives: ", 908 [EnumMembers!(ConfigMutationTest.RemovedTestCases)]); 909 logger.error(e.msg); 910 } 911 }; 912 callbacks["mutant_test.oldest_mutants"] = (ref ArgParser c, ref TOMLValue v) { 913 try { 914 c.mutationTest.onOldMutants = v.str.to!(ConfigMutationTest.OldMutant); 915 } catch (Exception e) { 916 logger.info("Available alternatives: ", [ 917 EnumMembers!(ConfigMutationTest.OldMutant) 918 ]); 919 logger.error(e.msg); 920 } 921 }; 922 callbacks["mutant_test.oldest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) { 923 c.mutationTest.oldMutantsNr = v.integer; 924 }; 925 callbacks["mutant_test.oldest_mutants_percentage"] = (ref ArgParser c, ref TOMLValue v) { 926 c.mutationTest.oldMutantPercentage.get = v.floating; 927 }; 928 callbacks["mutant_test.parallel_test"] = (ref ArgParser c, ref TOMLValue v) { 929 c.mutationTest.testPoolSize = cast(int) v.integer; 930 }; 931 callbacks["mutant_test.use_early_stop"] = (ref ArgParser c, ref TOMLValue v) { 932 c.mutationTest.useEarlyTestCmdStop = v == true; 933 }; 934 callbacks["mutant_test.use_schemata"] = (ref ArgParser c, ref TOMLValue v) { 935 c.mutationTest.useSchemata = v == true; 936 }; 937 callbacks["mutant_test.check_schemata"] = (ref ArgParser c, ref TOMLValue v) { 938 c.mutationTest.sanityCheckSchemata = v == true; 939 }; 940 callbacks["mutant_test.continues_check_test_suite"] = (ref ArgParser c, ref TOMLValue v) { 941 c.mutationTest.contCheckTestSuite.get = v == true; 942 }; 943 callbacks["mutant_test.continues_check_test_suite_period"] = (ref ArgParser c, ref TOMLValue v) { 944 c.mutationTest.contCheckTestSuitePeriod.get = cast(int) v.integer; 945 }; 946 947 callbacks["report.style"] = (ref ArgParser c, ref TOMLValue v) { 948 c.report.reportKind = v.str.to!ReportKind; 949 }; 950 callbacks["report.sections"] = (ref ArgParser c, ref TOMLValue v) { 951 try { 952 c.report.reportSection = v.array.map!(a => a.str.to!ReportSection).array; 953 } catch (Exception e) { 954 logger.info("Available mutation kinds ", [ 955 EnumMembers!ReportSection 956 ]); 957 logger.error(e.msg); 958 } 959 }; 960 callbacks["report.high_interest_mutants_nr"] = (ref ArgParser c, ref TOMLValue v) { 961 c.report.highInterestMutantsNr.get = cast(uint) v.integer; 962 }; 963 964 void iterSection(ref ArgParser c, string sectionName) { 965 if (auto section = sectionName in doc) { 966 // specific configuration from section members 967 foreach (k, v; *section) { 968 const key = sectionName ~ "." ~ k; 969 if (auto cb = key in callbacks) { 970 try { 971 (*cb)(c, v); 972 } catch (Exception e) { 973 logger.error(e.msg).collectException; 974 logger.error("section ", key).collectException; 975 logger.error("value ", v).collectException; 976 } 977 } else { 978 logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName); 979 } 980 } 981 } 982 } 983 984 iterSection(rval, "generic"); 985 iterSection(rval, "analyze"); 986 iterSection(rval, "workarea"); 987 iterSection(rval, "database"); 988 iterSection(rval, "compiler"); 989 iterSection(rval, "compile_commands"); 990 iterSection(rval, "mutant_test"); 991 iterSection(rval, "report"); 992 993 parseTestGroups(rval, doc); 994 995 return rval; 996 } 997 998 void parseTestGroups(ref ArgParser c, ref TOMLDocument doc) @trusted { 999 import toml; 1000 1001 if ("test_group" !in doc) 1002 return; 1003 1004 foreach (k, s; *("test_group" in doc)) { 1005 if (s.type != TOML_TYPE.TABLE) 1006 continue; 1007 1008 string desc; 1009 if (auto v = "description" in s) 1010 desc = v.str; 1011 if (auto v = "pattern" in s) { 1012 string re = v.str; 1013 c.report.testGroups ~= TestGroup(k, desc, re); 1014 } 1015 } 1016 } 1017 1018 @("shall populate the test, build and analyze command of an ArgParser from a TOML document") 1019 @system unittest { 1020 import std.format : format; 1021 import toml : parseTOML; 1022 1023 immutable txt = ` 1024 [mutant_test] 1025 test_cmd = %s 1026 build_cmd = %s 1027 analyze_cmd = %s 1028 `; 1029 1030 { 1031 auto doc = parseTOML(format!txt(`"test.sh"`, `"build.sh"`, `"analyze.sh"`)); 1032 auto ap = loadConfig(ArgParser.init, doc); 1033 ap.mutationTest.mutationTester.shouldEqual([ShellCommand(["test.sh"])]); 1034 ap.mutationTest.mutationCompile.shouldEqual(ShellCommand(["build.sh"])); 1035 ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([ 1036 ShellCommand(["analyze.sh"]) 1037 ]); 1038 } 1039 1040 { 1041 auto doc = parseTOML(format!txt(`[["test1.sh"], ["test2.sh"]]`, 1042 `["build.sh", "-y"]`, `[["analyze.sh", "-z"]]`)); 1043 auto ap = loadConfig(ArgParser.init, doc); 1044 ap.mutationTest.mutationTester.shouldEqual([ 1045 ShellCommand(["test1.sh"]), ShellCommand(["test2.sh"]) 1046 ]); 1047 ap.mutationTest.mutationCompile.shouldEqual(ShellCommand([ 1048 "build.sh", "-y" 1049 ])); 1050 ap.mutationTest.mutationTestCaseAnalyze.shouldEqual([ 1051 ShellCommand(["analyze.sh", "-z"]) 1052 ]); 1053 } 1054 1055 { 1056 auto doc = parseTOML(format!txt(`[["test1.sh", "-x"], ["test2.sh", "-y"]]`, 1057 `"build.sh"`, `"analyze.sh"`)); 1058 auto ap = loadConfig(ArgParser.init, doc); 1059 ap.mutationTest.mutationTester.shouldEqual([ 1060 ShellCommand(["test1.sh", "-x"]), ShellCommand([ 1061 "test2.sh", "-y" 1062 ]) 1063 ]); 1064 } 1065 } 1066 1067 @("shall set the thread analyze limitation from the configuration") 1068 @system unittest { 1069 import toml : parseTOML; 1070 1071 immutable txt = ` 1072 [analyze] 1073 threads = 42 1074 `; 1075 auto doc = parseTOML(txt); 1076 auto ap = loadConfig(ArgParser.init, doc); 1077 ap.analyze.poolSize.shouldEqual(42); 1078 } 1079 1080 @("shall set how many tests are executed in parallel from the configuration") 1081 @system unittest { 1082 import toml : parseTOML; 1083 1084 immutable txt = ` 1085 [mutant_test] 1086 parallel_test = 42 1087 `; 1088 auto doc = parseTOML(txt); 1089 auto ap = loadConfig(ArgParser.init, doc); 1090 ap.mutationTest.testPoolSize.shouldEqual(42); 1091 } 1092 1093 @("shall deactivate prune of old files when analyze") 1094 @system unittest { 1095 import toml : parseTOML; 1096 1097 immutable txt = ` 1098 [analyze] 1099 prune = false 1100 `; 1101 auto doc = parseTOML(txt); 1102 auto ap = loadConfig(ArgParser.init, doc); 1103 ap.analyze.prune.shouldBeFalse; 1104 } 1105 1106 @("shall activate early stop of test commands") 1107 @system unittest { 1108 import toml : parseTOML; 1109 1110 immutable txt = ` 1111 [mutant_test] 1112 use_early_stop = true 1113 `; 1114 auto doc = parseTOML(txt); 1115 auto ap = loadConfig(ArgParser.init, doc); 1116 ap.mutationTest.useEarlyTestCmdStop.shouldBeTrue; 1117 } 1118 1119 @("shall activate schematas and sanity check of schematas") 1120 @system unittest { 1121 import toml : parseTOML; 1122 1123 immutable txt = ` 1124 [mutant_test] 1125 use_schemata = true 1126 check_schemata = true 1127 `; 1128 auto doc = parseTOML(txt); 1129 auto ap = loadConfig(ArgParser.init, doc); 1130 ap.mutationTest.useSchemata.shouldBeTrue; 1131 ap.mutationTest.sanityCheckSchemata.shouldBeTrue; 1132 } 1133 1134 @("shall set the number of mutants per schema") 1135 @system unittest { 1136 import toml : parseTOML; 1137 1138 immutable txt = ` 1139 [analyze] 1140 mutants_per_schema = 200 1141 `; 1142 auto doc = parseTOML(txt); 1143 auto ap = loadConfig(ArgParser.init, doc); 1144 ap.analyze.mutantsPerSchema.get.shouldEqual(200); 1145 } 1146 1147 @("shall parse if compilation errors are allowed") 1148 @system unittest { 1149 import toml : parseTOML; 1150 1151 immutable txt = ` 1152 [compiler] 1153 allow_errors = true 1154 `; 1155 auto doc = parseTOML(txt); 1156 auto ap = loadConfig(ArgParser.init, doc); 1157 ap.compiler.allowErrors.get.shouldBeTrue; 1158 } 1159 1160 @("shall parse the build command timeout") 1161 @system unittest { 1162 import toml : parseTOML; 1163 1164 immutable txt = ` 1165 [mutant_test] 1166 build_cmd_timeout = "1 hours" 1167 `; 1168 auto doc = parseTOML(txt); 1169 auto ap = loadConfig(ArgParser.init, doc); 1170 ap.mutationTest.buildCmdTimeout.shouldEqual(1.dur!"hours"); 1171 } 1172 1173 @("shall parse the continues test suite test") 1174 @system unittest { 1175 import toml : parseTOML; 1176 1177 immutable txt = ` 1178 [mutant_test] 1179 continues_check_test_suite = true 1180 continues_check_test_suite_period = 3 1181 `; 1182 auto doc = parseTOML(txt); 1183 auto ap = loadConfig(ArgParser.init, doc); 1184 ap.mutationTest.contCheckTestSuite.get.shouldBeTrue; 1185 ap.mutationTest.contCheckTestSuitePeriod.get.shouldEqual(3); 1186 } 1187 1188 @("shall parse the mutants to test") 1189 @system unittest { 1190 import toml : parseTOML; 1191 1192 immutable txt = ` 1193 [generic] 1194 mutants = ["lcr"] 1195 `; 1196 auto doc = parseTOML(txt); 1197 auto ap = loadConfig(ArgParser.init, doc); 1198 ap.data.mutation.shouldEqual([MutationKind.lcr]); 1199 } 1200 1201 @("shall parse the files to inject the runtime to") 1202 @system unittest { 1203 import toml : parseTOML; 1204 1205 immutable txt = ` 1206 [generic] 1207 inject_runtime_impl = [["foo", "cpp"]] 1208 `; 1209 auto doc = parseTOML(txt); 1210 auto ap = loadConfig(ArgParser.init, doc); 1211 ap.mutationTest.userRuntimeCtrl.shouldEqual([ 1212 UserRuntime(Path("foo"), Language.cpp) 1213 ]); 1214 } 1215 1216 @("shall parse the report sections") 1217 @system unittest { 1218 import toml : parseTOML; 1219 1220 immutable txt = ` 1221 [report] 1222 sections = ["all_mut", "summary"] 1223 `; 1224 auto doc = parseTOML(txt); 1225 auto ap = loadConfig(ArgParser.init, doc); 1226 ap.report.reportSection.shouldEqual([ 1227 ReportSection.all_mut, ReportSection.summary 1228 ]); 1229 } 1230 1231 @("shall parse the number of high interest mutants") 1232 @system unittest { 1233 import toml : parseTOML; 1234 1235 immutable txt = ` 1236 [report] 1237 high_interest_mutants_nr = 10 1238 `; 1239 auto doc = parseTOML(txt); 1240 auto ap = loadConfig(ArgParser.init, doc); 1241 ap.report.highInterestMutantsNr.get.shouldEqual(10); 1242 } 1243 1244 /// Minimal config to setup path to config file. 1245 struct MiniConfig { 1246 /// Value from the user via CLI, unmodified. 1247 string rawConfFile; 1248 1249 /// The configuration file that has been loaded 1250 AbsolutePath confFile; 1251 1252 bool shortPluginHelp; 1253 } 1254 1255 /// Returns: minimal config to load settings and setup working directory. 1256 MiniConfig cliToMiniConfig(string[] args) @trusted nothrow { 1257 import std.file : exists; 1258 static import std.getopt; 1259 1260 immutable default_conf = ".dextool_mutate.toml"; 1261 1262 MiniConfig conf; 1263 1264 try { 1265 std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough, 1266 "c|config", "none not visible to the user", &conf.rawConfFile, 1267 "short-plugin-help", "not visible to the user", &conf.shortPluginHelp); 1268 if (conf.rawConfFile.length == 0) 1269 conf.rawConfFile = default_conf; 1270 conf.confFile = Path(conf.rawConfFile).AbsolutePath; 1271 } catch (Exception e) { 1272 logger.trace(conf).collectException; 1273 logger.error(e.msg).collectException; 1274 } 1275 1276 return conf; 1277 } 1278 1279 auto parseDuration(string timeSpec) { 1280 import std.conv : to; 1281 import std.string : split; 1282 import std.datetime : Duration, dur; 1283 import std.range : chunks; 1284 1285 Duration d; 1286 const parts = timeSpec.split; 1287 1288 if (parts.length % 2 != 0) { 1289 logger.warning("Invalid time specification because either the number or unit is missing"); 1290 return d; 1291 } 1292 1293 foreach (const p; parts.chunks(2)) { 1294 const nr = p[0].to!long; 1295 bool validUnit; 1296 immutable Units = [ 1297 "msecs", "seconds", "minutes", "hours", "days", "weeks" 1298 ]; 1299 static foreach (Unit; Units) { 1300 if (p[1] == Unit) { 1301 d += nr.dur!Unit; 1302 validUnit = true; 1303 } 1304 } 1305 if (!validUnit) { 1306 logger.warningf("Invalid unit '%s'. Valid are %-(%s, %).", p[1], Units); 1307 return d; 1308 } 1309 } 1310 1311 return d; 1312 } 1313 1314 @("shall parse a string to a duration") 1315 unittest { 1316 const expected = 1.dur!"weeks" + 1.dur!"days" + 3.dur!"hours" 1317 + 2.dur!"minutes" + 5.dur!"seconds" + 9.dur!"msecs"; 1318 const d = parseDuration("1 weeks 1 days 3 hours 2 minutes 5 seconds 9 msecs"); 1319 d.should == expected; 1320 } 1321 1322 auto parseUserTestConstraint(string[] raw) { 1323 import std.conv : to; 1324 import std.regex : regex, matchFirst; 1325 import std.typecons : tuple; 1326 import dextool.plugin.mutate.type : TestConstraint; 1327 1328 TestConstraint rval; 1329 const re = regex(`(?P<file>.*):(?P<start>\d*)-(?P<end>\d*)`); 1330 1331 foreach (r; raw.map!(a => tuple!("user", "match")(a, matchFirst(a, re)))) { 1332 if (r.match.empty) { 1333 logger.warning("Unable to parse ", r.user); 1334 continue; 1335 } 1336 1337 const start = r.match["start"].to!uint; 1338 const end = r.match["end"].to!uint + 1; 1339 1340 if (start > end) { 1341 logger.warningf("Unable to parse %s because start (%s) must be less than end (%s)", 1342 r.user, r.match["start"], r.match["end"]); 1343 continue; 1344 } 1345 1346 foreach (const l; start .. end) 1347 rval.value[Path(r.match["file"])] ~= Line(l); 1348 } 1349 1350 return rval; 1351 } 1352 1353 @("shall parse a test restriction") 1354 unittest { 1355 const r = parseUserTestConstraint([ 1356 "foo/bar:1-10", "smurf bar/i oknen:ea,ting:33-45" 1357 ]); 1358 1359 Path("foo/bar").shouldBeIn(r.value); 1360 r.value[Path("foo/bar")][0].should == Line(1); 1361 r.value[Path("foo/bar")][9].should == Line(10); 1362 1363 Path("smurf bar/i oknen:ea,ting").shouldBeIn(r.value); 1364 r.value[Path("smurf bar/i oknen:ea,ting")][0].should == Line(33); 1365 r.value[Path("smurf bar/i oknen:ea,ting")][12].should == Line(45); 1366 }