diff options
| author | Leitwolf11 | 2026-10-29 19:22:21 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-29 19:22:21 +0300 |
| commit | f41a9c3c01b4df1100e24a63987716cb4dbf28f2 (patch) | |
| tree | 8e5355fcdcfb0bcd3371006ce11dc2f16e6dfd58 /src | |
| parent | 1d48419b7ef642917ebf7dfd16c3bc1b3050d8f1 (diff) | |
| download | catch3-f41a9c3c01b4df1100e24a63987716cb4dbf28f2.tar.gz | |
Read the options of a project from catch3.json
A project can now keep the options it always wants in a catch3.json
instead of repeating them in every CTest, Bazel or CI command line.
Where it is found: the runner looks in the working directory and in each
directory above it, up to the root of the Git repository that holds the
working directory, and reads the nearest file. Outside a repository it
looks only in the working directory, so a stray file in a home directory
never applies. --config=FILE names a file instead (relative to the
working directory) and --no-config reads none; the last of the two wins.
Workers, --help and --version read no file, so a broken file never stands
in the way of help, and workers keep taking everything from their runner.
What it says: strict JSON with "version": 1 required and "$schema"
allowed for editors. The keys are the long options of the command line,
and each value becomes the arguments it stands for and goes through the
same parser, so there is one set of checks and one set of messages; the
file and the key are named in front of them. Values are typed: switches
take true (false for the negated adaptive-timeout and bisect), counts
take whole numbers, timeout and the confidence interval take numbers,
retries and seed take a number or a word, tests and warn take arrays,
verbosity takes quiet, normal or verbose, and history takes a path or
false. Paths are relative to the directory of the file. An unknown key is
an error with a suggestion when it is close to a known one; a key that
names an option for one run (shard, section, list, replay, ...) is told
that it belongs on the command line. Repeated keys are refused.
Precedence: the file's options are applied first and the command line on
top, so the command line wins; test specs on the command line replace the
file's tests instead of widening them with "or". --verbose names the file
that was read.
Options gains --config and --no-config, a ParseOptions overload that
starts from given options, and a public EditDistance for the suggestions.
The new ProjectConfig module holds the search, the key table and
ParseCommandLine, which Runner now calls in place of ParseOptions.
Tests cover the search (nearest file, the repository boundary, outside a
repository), every key, words and defaults, relative and absolute paths,
the messages for wrong files, precedence, --config, --no-config and the
modes that read no file. A fuzzer reads arbitrary file text and checks
that the worker arguments of the result parse again. The command line
reference, the runner guide and the release notes describe the file; the
amalgamated sources are regenerated.
Diffstat (limited to 'src')
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/Progmasoft/Catch3/meson.build | 1 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/Options.cpp | 78 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/Options.hpp | 19 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/ProjectConfig.cpp | 548 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/ProjectConfig.hpp | 44 | ||||
| -rw-r--r-- | src/Progmasoft/Runner/Runner.cpp | 12 |
7 files changed, 673 insertions, 31 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a33fc0a3..f3b7812d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -308,6 +308,7 @@ set(PROGMASOFT_RUNNER_FILES "${CATCH_DIR}/src/Progmasoft/Runner/Options.hpp" "${CATCH_DIR}/src/Progmasoft/Runner/Planner.hpp" "${CATCH_DIR}/src/Progmasoft/Runner/Process.hpp" + "${CATCH_DIR}/src/Progmasoft/Runner/ProjectConfig.hpp" "${CATCH_DIR}/src/Progmasoft/Runner/Protocol.hpp" "${CATCH_DIR}/src/Progmasoft/Runner/ResultsJson.hpp" "${CATCH_DIR}/src/Progmasoft/Runner/Scheduler.hpp" @@ -326,6 +327,7 @@ set(PROGMASOFT_RUNNER_FILES "${CATCH_DIR}/src/Progmasoft/Runner/Options.cpp" "${CATCH_DIR}/src/Progmasoft/Runner/Planner.cpp" "${CATCH_DIR}/src/Progmasoft/Runner/Process.cpp" + "${CATCH_DIR}/src/Progmasoft/Runner/ProjectConfig.cpp" "${CATCH_DIR}/src/Progmasoft/Runner/Protocol.cpp" "${CATCH_DIR}/src/Progmasoft/Runner/ResultsJson.cpp" "${CATCH_DIR}/src/Progmasoft/Runner/Runner.cpp" diff --git a/src/Progmasoft/Catch3/meson.build b/src/Progmasoft/Catch3/meson.build index 06faf162..70bf336d 100644 --- a/src/Progmasoft/Catch3/meson.build +++ b/src/Progmasoft/Catch3/meson.build @@ -295,6 +295,7 @@ runner_sources = files( '../Runner/Options.cpp', '../Runner/Planner.cpp', '../Runner/Process.cpp', + '../Runner/ProjectConfig.cpp', '../Runner/Protocol.cpp', '../Runner/ResultsJson.cpp', '../Runner/Runner.cpp', diff --git a/src/Progmasoft/Runner/Options.cpp b/src/Progmasoft/Runner/Options.cpp index bb36ab36..5ca48dd4 100644 --- a/src/Progmasoft/Runner/Options.cpp +++ b/src/Progmasoft/Runner/Options.cpp @@ -140,32 +140,6 @@ namespace Progmasoft::Catch3::Runner names + ", not '" + value.value_or("") + "'"}; } - std::size_t - EditDistance(std::string_view left, std::string_view right) - { - std::vector<std::size_t> previous(right.size() + 1); - std::vector<std::size_t> current(right.size() + 1); - for (std::size_t index = 0; index <= right.size(); ++index) - { - previous[index] = index; - } - for (std::size_t row = 1; row <= left.size(); ++row) - { - current[0] = row; - for (std::size_t column = 1; column <= right.size(); ++column) - { - const std::size_t substitution = - previous[column - 1] + - (left[row - 1] == right[column - 1] ? 0 : 1); - current[column] = std::min({previous[column] + 1, - current[column - 1] + 1, - substitution}); - } - std::swap(previous, current); - } - return previous[right.size()]; - } - const std::vector<OptionSpec> & Specs() { @@ -817,6 +791,29 @@ namespace Progmasoft::Catch3::Runner } return ReadCount(value, "slowest", options.slowest, 1); }}, + // Configuration. + {"config", + '\0', + Takes::kValue, + "FILE", + "Configuration", + "read this catch3.json instead of searching for one", + [](Options &options, const std::optional<std::string> &value) { + options.configPath = *value; + options.noConfig = false; + return Status::Ok(); + }}, + {"no-config", + '\0', + Takes::kNothing, + "", + "Configuration", + "read no catch3.json", + [](Options &options, const std::optional<std::string> &) { + options.noConfig = true; + options.configPath.clear(); + return Status::Ok(); + }}, // Information. {"list", 'l', @@ -949,10 +946,35 @@ namespace Progmasoft::Catch3::Runner } } // namespace + std::size_t + EditDistance(std::string_view left, std::string_view right) + { + std::vector<std::size_t> previous(right.size() + 1); + std::vector<std::size_t> current(right.size() + 1); + for (std::size_t index = 0; index <= right.size(); ++index) + { + previous[index] = index; + } + for (std::size_t row = 1; row <= left.size(); ++row) + { + current[0] = row; + for (std::size_t column = 1; column <= right.size(); ++column) + { + const std::size_t substitution = + previous[column - 1] + + (left[row - 1] == right[column - 1] ? 0 : 1); + current[column] = std::min({previous[column] + 1, + current[column - 1] + 1, + substitution}); + } + std::swap(previous, current); + } + return previous[right.size()]; + } + Expected<Options> - ParseOptions(const std::vector<std::string> &arguments) + ParseOptions(const std::vector<std::string> &arguments, Options options) { - Options options; bool onlySpecs = false; for (std::size_t index = 0; index < arguments.size(); ++index) { diff --git a/src/Progmasoft/Runner/Options.hpp b/src/Progmasoft/Runner/Options.hpp index 2820c1c0..f7bf0445 100644 --- a/src/Progmasoft/Runner/Options.hpp +++ b/src/Progmasoft/Runner/Options.hpp @@ -6,8 +6,10 @@ #include <chrono> #include <cstdint> +#include <cstddef> #include <optional> #include <string> +#include <string_view> #include <vector> namespace Progmasoft::Catch3::Runner @@ -165,11 +167,24 @@ namespace Progmasoft::Catch3::Runner /// Created when the run starts and removed when it ends normally, so /// a file left behind shows that the run ended early. std::string exitGuardPath; + + // Configuration. + /// The `catch3.json` named by --config; after ParseCommandLine, the + /// one that was read, if any. + std::string configPath; + bool noConfig = false; }; - /// Reads a command line. `arguments` does not include the program. + /// Reads a command line on top of `options`. `arguments` does not include + /// the program. [[nodiscard]] Expected<Options> - ParseOptions(const std::vector<std::string> &arguments); + ParseOptions(const std::vector<std::string> &arguments, + Options options = {}); + + /// How many single-character edits turn `left` into `right`, for + /// suggesting the name that was meant. + [[nodiscard]] std::size_t + EditDistance(std::string_view left, std::string_view right); /// The help text, for `--help`. [[nodiscard]] std::string diff --git a/src/Progmasoft/Runner/ProjectConfig.cpp b/src/Progmasoft/Runner/ProjectConfig.cpp new file mode 100644 index 00000000..ea5d921f --- /dev/null +++ b/src/Progmasoft/Runner/ProjectConfig.cpp @@ -0,0 +1,548 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <set> +#include <system_error> +#include <utility> + +#include "Files.hpp" +#include "Json.hpp" +#include "ProjectConfig.hpp" + +namespace Progmasoft::Catch3::Runner +{ + namespace + { + /// The version of the file format this runner reads. + constexpr double kConfigVersion = 1; + + /// What the value of a key is, and how it becomes arguments. + enum class ValueKind : std::uint8_t + { + /// true gives the option; false, the default, gives nothing. + kFlag, + /// false gives the negating option; true is the default. + kNegated, + /// A string, given as the value of the option. + kWord, + /// A file or directory, relative to the file's directory. + kPath, + /// A whole number. + kCount, + /// A whole number or a word, such as "auto". + kCountOrWord, + /// A number with a fraction, such as seconds. + kNumber, + /// An array of strings, each given to the option. + kWords, + /// A directory, or false for none. + kPathOrFalse, + /// "quiet", "normal" or "verbose". + kVerbosity, + /// An array of test specs. + kTests, + }; + + struct ConfigKey final + { + std::string_view name; + ValueKind kind; + /// The option it stands for, when it is not spelled like the key. + std::string_view option = {}; + }; + + const std::vector<ConfigKey> & + Keys() + { + static const auto *keys = new std::vector<ConfigKey>{ + { "tests", ValueKind::kTests }, + // Selecting tests. + { "select", ValueKind::kWord }, + { "changes-since", ValueKind::kWord }, + { "incremental", ValueKind::kFlag }, + { "shard-durations", ValueKind::kPath }, + { "filenames-as-tags", ValueKind::kFlag }, + // Running. + { "jobs", ValueKind::kCount }, + { "in-process", ValueKind::kFlag }, + { "isolate", ValueKind::kFlag }, + { "timeout", ValueKind::kNumber }, + { "adaptive-timeout", + ValueKind::kNegated, + "no-adaptive-timeout" }, + { "retries", ValueKind::kCountOrWord }, + { "repeat", ValueKind::kCount }, + { "fail-fast", ValueKind::kCount }, + { "order", ValueKind::kWord }, + { "seed", ValueKind::kCountOrWord }, + { "bisect", ValueKind::kNegated, "no-bisect" }, + // How tests behave. + { "success", ValueKind::kFlag }, + { "no-throw", ValueKind::kFlag }, + { "warn", ValueKind::kWords }, + { "allow-no-tests", ValueKind::kFlag }, + { "skip-benchmarks", ValueKind::kFlag }, + { "benchmark-samples", ValueKind::kCount }, + { "benchmark-resamples", ValueKind::kCount }, + { "benchmark-confidence-interval", ValueKind::kNumber }, + { "benchmark-no-analysis", ValueKind::kFlag }, + { "benchmark-warmup-time", ValueKind::kCount }, + // History and analysis. + { "history", ValueKind::kPathOrFalse }, + { "quarantine", ValueKind::kWord }, + // Output. + { "verbosity", ValueKind::kVerbosity }, + { "color", ValueKind::kWord }, + { "junit", ValueKind::kPath }, + { "json", ValueKind::kPath }, + { "trace", ValueKind::kPath }, + { "slowest", ValueKind::kCount }, + }; + return *keys; + } + + /// Options that describe one run rather than a project. + constexpr std::string_view kCommandLineOnly[] = { + "section", + "generator-index", + "path-filter", + "input-file", + "shard", + "break", + "replay", + "analytics", + "verbose", + "quiet", + "premature-exit-guard-file", + "list", + "list-tags", + "help", + "config", + "no-config", + "catch3-worker", + }; + + std::string + Quoted(std::string_view key) + { + return "\"" + std::string{ key } + "\""; + } + + std::string + UnknownKey(std::string_view key) + { + for (const std::string_view option : kCommandLineOnly) + { + if (key == option) + { + return Quoted(key) + + " belongs on the command line, not in a " + "catch3.json"; + } + } + std::string message = Quoted(key) + " is not a key of catch3.json"; + std::string_view closest; + std::size_t best = 3; + const auto consider = [&](std::string_view name) { + const std::size_t distance = EditDistance(key, name); + if (distance < best) + { + best = distance; + closest = name; + } + }; + consider("version"); + for (const auto &entry : Keys()) + { + consider(entry.name); + } + if (!closest.empty()) + { + message += "; did you mean " + Quoted(closest) + "?"; + } + return message; + } + + /// A whole number JSON can say exactly, or nothing. + std::optional<std::uint64_t> + WholeNumber(const JsonValue &value) + { + if (!value.IsNumber()) + { + return std::nullopt; + } + const double number = value.AsNumber(); + if (!std::isfinite(number) || number < 0 + || number != std::floor(number) || number > 9007199254740992.0) + { + return std::nullopt; + } + return static_cast<std::uint64_t>(number); + } + + /// A number in the form the command line reads: digits and a point. + std::string + DecimalText(double number) + { + char text[400]; + std::snprintf(text, sizeof(text), "%.17f", number); + std::string result{ text }; + for (char &character : result) + { + // The decimal separator of the locale a test may have set. + if (character == ',') + { + character = '.'; + } + } + return result; + } + + std::string + PathText(const std::string &value, + const std::filesystem::path &directory) + { + const std::filesystem::path path{ value }; + if (path.is_absolute() || directory.empty()) + { + return path.lexically_normal().string(); + } + return (directory / path).lexically_normal().string(); + } + + /// The arguments that one key and its value stand for. + Expected<std::vector<std::string>> + KeyArguments(const ConfigKey &key, + const JsonValue &value, + const std::filesystem::path &directory) + { + const std::string option + = "--" + + std::string{ key.option.empty() ? key.name : key.option }; + const auto wrong = [&](std::string_view expected) { + return Error{ Quoted(key.name) + " takes " + + std::string{ expected } + ", not " + + value.ToString() }; + }; + switch (key.kind) + { + case ValueKind::kFlag: + if (value.GetKind() != JsonValue::Kind::kBoolean) + { + return wrong("true or false"); + } + if (value.AsBoolean()) + { + return std::vector<std::string>{ option }; + } + return std::vector<std::string>{}; + case ValueKind::kNegated: + if (value.GetKind() != JsonValue::Kind::kBoolean) + { + return wrong("true or false"); + } + if (!value.AsBoolean()) + { + return std::vector<std::string>{ option }; + } + return std::vector<std::string>{}; + case ValueKind::kWord: + if (!value.IsString()) + { + return wrong("a string"); + } + return std::vector<std::string>{ option + "=" + + value.AsString() }; + case ValueKind::kPath: + if (!value.IsString() || value.AsString().empty()) + { + return wrong("a path"); + } + return std::vector<std::string>{ + option + "=" + PathText(value.AsString(), directory) + }; + case ValueKind::kCount: + if (const auto number = WholeNumber(value)) + { + return std::vector<std::string>{ + option + "=" + std::to_string(*number) + }; + } + return wrong("a whole number"); + case ValueKind::kCountOrWord: + if (value.IsString()) + { + return std::vector<std::string>{ option + "=" + + value.AsString() }; + } + if (const auto number = WholeNumber(value)) + { + return std::vector<std::string>{ + option + "=" + std::to_string(*number) + }; + } + return wrong("a whole number or a string"); + case ValueKind::kNumber: + if (!value.IsNumber() || !std::isfinite(value.AsNumber()) + || value.AsNumber() < 0) + { + return wrong("a number of at least 0"); + } + return std::vector<std::string>{ + option + "=" + DecimalText(value.AsNumber()) + }; + case ValueKind::kWords: + { + if (!value.IsArray()) + { + return wrong("an array of strings"); + } + std::vector<std::string> arguments; + for (const auto &item : value.Items()) + { + if (!item.IsString()) + { + return wrong("an array of strings"); + } + arguments.push_back(option + "=" + item.AsString()); + } + return arguments; + } + case ValueKind::kPathOrFalse: + if (value.GetKind() == JsonValue::Kind::kBoolean + && !value.AsBoolean()) + { + return std::vector<std::string>{ + "--no-" + std::string{ key.name } + }; + } + if (!value.IsString() || value.AsString().empty()) + { + return wrong("a path or false"); + } + return std::vector<std::string>{ + option + "=" + PathText(value.AsString(), directory) + }; + case ValueKind::kVerbosity: + if (value.IsString()) + { + if (value.AsString() == "quiet") + { + return std::vector<std::string>{ "--quiet" }; + } + if (value.AsString() == "verbose") + { + return std::vector<std::string>{ "--verbose" }; + } + if (value.AsString() == "normal") + { + return std::vector<std::string>{}; + } + } + return wrong("\"quiet\", \"normal\" or \"verbose\""); + case ValueKind::kTests: + { + if (!value.IsArray()) + { + return wrong("an array of test specs"); + } + // After --, every argument is a test spec, even one that + // starts with a dash. + std::vector<std::string> arguments{ "--" }; + for (const auto &item : value.Items()) + { + if (!item.IsString() || item.AsString().empty()) + { + return wrong("an array of test specs"); + } + arguments.push_back(item.AsString()); + } + return arguments; + } + } + return wrong("a known kind of value"); + } + } // namespace + + std::optional<std::filesystem::path> + FindProjectConfig(const std::filesystem::path &start) + { + std::error_code error; + std::filesystem::path directory + = std::filesystem::absolute(start, error).lexically_normal(); + if (error) + { + return std::nullopt; + } + if (directory.filename().empty() && directory.has_relative_path()) + { + directory = directory.parent_path(); + } + std::vector<std::filesystem::path> candidates; + for (std::filesystem::path current = directory;; + current = current.parent_path()) + { + candidates.push_back(current); + if (std::filesystem::exists(current / ".git", error)) + { + break; + } + if (!current.has_relative_path()) + { + // No repository holds the directory. + candidates = { directory }; + break; + } + } + for (const auto &candidate : candidates) + { + const auto file = candidate / kProjectConfigName; + if (std::filesystem::is_regular_file(file, error)) + { + return file; + } + } + return std::nullopt; + } + + Expected<Options> + ApplyProjectConfig(std::string_view text, + const std::filesystem::path &directory, + std::string_view file, + Options options) + { + const std::string prefix = std::string{ file } + ": "; + auto document = ParseJson(text); + if (!document) + { + return Error{ prefix + document.GetError().Message() }; + } + if (!document->IsObject()) + { + return Error{ prefix + "the file holds an object, not " + + document->ToString() }; + } + std::set<std::string_view> seen; + bool versioned = false; + for (const auto &[name, value] : document->Members()) + { + if (!seen.insert(name).second) + { + return Error{ prefix + Quoted(name) + " appears twice" }; + } + if (name == "version") + { + if (!value.IsNumber() || value.AsNumber() != kConfigVersion) + { + return Error{ + prefix + "version " + value.ToString() + + " is not known; this Catch3 reads version 1" + }; + } + versioned = true; + continue; + } + if (name == "$schema") + { + // For editors; the runner knows its own format. + continue; + } + const ConfigKey *key = nullptr; + for (const auto &entry : Keys()) + { + if (entry.name == name) + { + key = &entry; + break; + } + } + if (key == nullptr) + { + return Error{ prefix + UnknownKey(name) }; + } + auto arguments = KeyArguments(*key, value, directory); + if (!arguments) + { + return Error{ prefix + arguments.GetError().Message() }; + } + auto applied = ParseOptions(*arguments, std::move(options)); + if (!applied) + { + return Error{ prefix + Quoted(name) + ": " + + applied.GetError().Message() }; + } + options = std::move(*applied); + } + if (!versioned) + { + return Error{ prefix + "\"version\": 1 is missing" }; + } + return options; + } + + Expected<Options> + ParseCommandLine(const std::vector<std::string> &arguments, + const std::filesystem::path &workingDirectory) + { + auto commandLine = ParseOptions(arguments); + if (!commandLine) + { + return commandLine; + } + if (commandLine->noConfig || commandLine->mode == Mode::kWorker + || commandLine->mode == Mode::kHelp + || commandLine->mode == Mode::kVersion) + { + // A worker gets everything it needs from its runner. + return commandLine; + } + std::filesystem::path file; + if (!commandLine->configPath.empty()) + { + file = commandLine->configPath; + if (file.is_relative()) + { + file = workingDirectory / file; + } + } + else if (auto found = FindProjectConfig(workingDirectory)) + { + file = std::move(*found); + } + else + { + return commandLine; + } + file = file.lexically_normal(); + auto text = ReadFile(file); + if (!text) + { + return Error{ "cannot read the configuration " + file.string() + + ": " + text.GetError().Message() }; + } + auto configured + = ApplyProjectConfig(*text, file.parent_path(), file.string()); + if (!configured) + { + return configured; + } + Options base = std::move(*configured); + // Specs on the command line replace those of the file rather than + // widening them with "or". + std::vector<std::string> fileSpecs = std::move(base.testSpecs); + base.testSpecs.clear(); + auto parsed = ParseOptions(arguments, std::move(base)); + if (!parsed) + { + return parsed; + } + if (parsed->testSpecs.empty()) + { + parsed->testSpecs = std::move(fileSpecs); + } + parsed->configPath = file.string(); + return parsed; + } +} // namespace Progmasoft::Catch3::Runner diff --git a/src/Progmasoft/Runner/ProjectConfig.hpp b/src/Progmasoft/Runner/ProjectConfig.hpp new file mode 100644 index 00000000..c715b1ed --- /dev/null +++ b/src/Progmasoft/Runner/ProjectConfig.hpp @@ -0,0 +1,44 @@ +// Copyright (c) 2026 Progmasoft. +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 +#pragma once + +#include <filesystem> +#include <optional> +#include <string> +#include <string_view> +#include <vector> + +#include "Expected.hpp" +#include "Options.hpp" + +namespace Progmasoft::Catch3::Runner +{ + /// The name of the project configuration file. + inline constexpr std::string_view kProjectConfigName = "catch3.json"; + + /// The `catch3.json` that applies in `start`: the nearest one in `start` + /// or a directory above it, up to the root of the Git repository that + /// holds `start`. Outside a repository only `start` itself is searched, + /// so a stray file in a home directory never applies. + [[nodiscard]] std::optional<std::filesystem::path> + FindProjectConfig(const std::filesystem::path &start); + + /// Applies the text of a `catch3.json` to `options`. Its keys are the long + /// options of the command line, and its values go through the same checks; + /// relative paths are read from `directory`, the file's own. `file` names + /// the file in messages. + [[nodiscard]] Expected<Options> + ApplyProjectConfig(std::string_view text, + const std::filesystem::path &directory, + std::string_view file, + Options options = {}); + + /// Reads the command line together with the `catch3.json` it selects: + /// --config names one, --no-config reads none, and otherwise + /// FindProjectConfig searches from `workingDirectory`. Options on the + /// command line win over the file, and test specs on the command line + /// replace those of the file. Workers, --help and --version read no file. + [[nodiscard]] Expected<Options> + ParseCommandLine(const std::vector<std::string> &arguments, + const std::filesystem::path &workingDirectory); +} // namespace Progmasoft::Catch3::Runner diff --git a/src/Progmasoft/Runner/Runner.cpp b/src/Progmasoft/Runner/Runner.cpp index 7e69206a..62acac2c 100644 --- a/src/Progmasoft/Runner/Runner.cpp +++ b/src/Progmasoft/Runner/Runner.cpp @@ -16,8 +16,10 @@ #include <chrono> #include <cstdio> #include <exception> +#include <filesystem> #include <map> #include <random> +#include <system_error> #include "Analysis.hpp" #include "Catalog.hpp" @@ -31,6 +33,7 @@ #include "Options.hpp" #include "Planner.hpp" #include "Process.hpp" +#include "ProjectConfig.hpp" #include "ResultsJson.hpp" #include "Scheduler.hpp" @@ -523,7 +526,9 @@ namespace Progmasoft::Catch3 { rest.assign(arguments.begin() + 1, arguments.end()); } - auto parsed = Runner::ParseOptions(rest); + std::error_code here; + auto parsed = Runner::ParseCommandLine |