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