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