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