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