Catch2/src/catch2/internal/catch_commandline.cpp
Martin Hořeňovský 423e1d2ebb
Integrate the new reporter specs into Catch2
This means that the CLI interface now uses the new key-value oriented
reporter spec, the common reporter base creates the colour implementation
based on the reporter-specific configuration, and it also stores the
custom configuration options for each reporter instance.

Closes #339 as it allows per-reporter forcing of ansi colour codes.
2022-04-07 12:50:08 +02:00

332 lines
16 KiB
C++

// Copyright Catch2 Authors
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// SPDX-License-Identifier: BSL-1.0
#include <catch2/internal/catch_commandline.hpp>
#include <catch2/internal/catch_compiler_capabilities.hpp>
#include <catch2/catch_config.hpp>
#include <catch2/internal/catch_string_manip.hpp>
#include <catch2/interfaces/catch_interfaces_registry_hub.hpp>
#include <catch2/interfaces/catch_interfaces_reporter_registry.hpp>
#include <catch2/interfaces/catch_interfaces_reporter.hpp>
#include <catch2/internal/catch_console_colour.hpp>
#include <catch2/internal/catch_reporter_spec_parser.hpp>
#include <algorithm>
#include <fstream>
#include <string>
namespace Catch {
Clara::Parser makeCommandLineParser( ConfigData& config ) {
using namespace Clara;
auto const setWarning = [&]( std::string const& warning ) {
if ( warning == "NoAssertions" ) {
config.warnings = static_cast<WarnAbout::What>(config.warnings | WarnAbout::NoAssertions);
return ParserResult::ok( ParseResultType::Matched );
} else if ( warning == "UnmatchedTestSpec" ) {
config.warnings = static_cast<WarnAbout::What>(config.warnings | WarnAbout::UnmatchedTestSpec);
return ParserResult::ok( ParseResultType::Matched );
}
return ParserResult ::runtimeError(
"Unrecognised warning option: '" + warning + '\'' );
};
auto const loadTestNamesFromFile = [&]( std::string const& filename ) {
std::ifstream f( filename.c_str() );
if( !f.is_open() )
return ParserResult::runtimeError( "Unable to load input file: '" + filename + '\'' );
std::string line;
while( std::getline( f, line ) ) {
line = trim(line);
if( !line.empty() && !startsWith( line, '#' ) ) {
if( !startsWith( line, '"' ) )
line = '"' + line + '"';
config.testsOrTags.push_back( line );
config.testsOrTags.emplace_back( "," );
}
}
//Remove comma in the end
if(!config.testsOrTags.empty())
config.testsOrTags.erase( config.testsOrTags.end()-1 );
return ParserResult::ok( ParseResultType::Matched );
};
auto const setTestOrder = [&]( std::string const& order ) {
if( startsWith( "declared", order ) )
config.runOrder = TestRunOrder::Declared;
else if( startsWith( "lexical", order ) )
config.runOrder = TestRunOrder::LexicographicallySorted;
else if( startsWith( "random", order ) )
config.runOrder = TestRunOrder::Randomized;
else
return ParserResult::runtimeError( "Unrecognised ordering: '" + order + '\'' );
return ParserResult::ok( ParseResultType::Matched );
};
auto const setRngSeed = [&]( std::string const& seed ) {
if( seed == "time" ) {
config.rngSeed = generateRandomSeed(GenerateFrom::Time);
return ParserResult::ok(ParseResultType::Matched);
} else if (seed == "random-device") {
config.rngSeed = generateRandomSeed(GenerateFrom::RandomDevice);
return ParserResult::ok(ParseResultType::Matched);
}
CATCH_TRY {
std::size_t parsedTo = 0;
unsigned long parsedSeed = std::stoul(seed, &parsedTo, 0);
if (parsedTo != seed.size()) {
return ParserResult::runtimeError("Could not parse '" + seed + "' as seed");
}
// TODO: Ideally we could parse unsigned int directly,
// but the stdlib doesn't provide helper for that
// type. After this is refactored to use fixed size
// type, we should check the parsed value is in range
// of the underlying type.
config.rngSeed = static_cast<unsigned int>(parsedSeed);
return ParserResult::ok(ParseResultType::Matched);
} CATCH_CATCH_ANON(std::exception const&) {
return ParserResult::runtimeError("Could not parse '" + seed + "' as seed");
}
};
auto const setColourMode = [&]( std::string const&
colourMode ) {
Optional<ColourMode> maybeMode = Catch::Detail::stringToColourMode(toLower( colourMode ));
if ( !maybeMode ) {
return ParserResult::runtimeError(
"colour mode must be one of: default, ansi, win32, "
"or none. '" +
colourMode + "' is not recognised" );
}
auto mode = *maybeMode;
if ( !isColourImplAvailable( mode ) ) {
return ParserResult::runtimeError(
"colour mode '" + colourMode +
"' is not supported in this binary" );
}
config.colourMode = mode;
return ParserResult::ok( ParseResultType::Matched );
};
auto const setWaitForKeypress = [&]( std::string const& keypress ) {
auto keypressLc = toLower( keypress );
if (keypressLc == "never")
config.waitForKeypress = WaitForKeypress::Never;
else if( keypressLc == "start" )
config.waitForKeypress = WaitForKeypress::BeforeStart;
else if( keypressLc == "exit" )
config.waitForKeypress = WaitForKeypress::BeforeExit;
else if( keypressLc == "both" )
config.waitForKeypress = WaitForKeypress::BeforeStartAndExit;
else
return ParserResult::runtimeError( "keypress argument must be one of: never, start, exit or both. '" + keypress + "' not recognised" );
return ParserResult::ok( ParseResultType::Matched );
};
auto const setVerbosity = [&]( std::string const& verbosity ) {
auto lcVerbosity = toLower( verbosity );
if( lcVerbosity == "quiet" )
config.verbosity = Verbosity::Quiet;
else if( lcVerbosity == "normal" )
config.verbosity = Verbosity::Normal;
else if( lcVerbosity == "high" )
config.verbosity = Verbosity::High;
else
return ParserResult::runtimeError( "Unrecognised verbosity, '" + verbosity + '\'' );
return ParserResult::ok( ParseResultType::Matched );
};
auto const setReporter = [&]( std::string const& userReporterSpec ) {
if ( userReporterSpec.empty() ) {
return ParserResult::runtimeError( "Received empty reporter spec." );
}
Optional<ReporterSpec> parsed =
parseReporterSpec( userReporterSpec );
if ( !parsed ) {
return ParserResult::runtimeError(
"Could not parse reporter spec '" + userReporterSpec +
"'" );
}
auto const& reporterSpec = *parsed;
IReporterRegistry::FactoryMap const& factories =
getRegistryHub().getReporterRegistry().getFactories();
auto result = factories.find( reporterSpec.name() );
if ( result == factories.end() ) {
return ParserResult::runtimeError(
"Unrecognized reporter, '" + reporterSpec.name() +
"'. Check available with --list-reporters" );
}
const bool hadOutputFile = reporterSpec.outputFile().some();
config.reporterSpecifications.push_back( CATCH_MOVE( *parsed ) );
// It would be enough to check this only once at the very end, but
// there is not a place where we could call this check, so do it
// every time it could fail. For valid inputs, this is still called
// at most once.
if (!hadOutputFile) {
int n_reporters_without_file = 0;
for (auto const& spec : config.reporterSpecifications) {
if (spec.outputFile().none()) {
n_reporters_without_file++;
}
}
if (n_reporters_without_file > 1) {
return ParserResult::runtimeError( "Only one reporter may have unspecified output file." );
}
}
return ParserResult::ok( ParseResultType::Matched );
};
auto const setShardCount = [&]( std::string const& shardCount ) {
CATCH_TRY{
std::size_t parsedTo = 0;
int64_t parsedCount = std::stoll(shardCount, &parsedTo, 0);
if (parsedTo != shardCount.size()) {
return ParserResult::runtimeError("Could not parse '" + shardCount + "' as shard count");
}
if (parsedCount <= 0) {
return ParserResult::runtimeError("Shard count must be a positive number");
}
config.shardCount = static_cast<unsigned int>(parsedCount);
return ParserResult::ok(ParseResultType::Matched);
} CATCH_CATCH_ANON(std::exception const&) {
return ParserResult::runtimeError("Could not parse '" + shardCount + "' as shard count");
}
};
auto const setShardIndex = [&](std::string const& shardIndex) {
CATCH_TRY{
std::size_t parsedTo = 0;
int64_t parsedIndex = std::stoll(shardIndex, &parsedTo, 0);
if (parsedTo != shardIndex.size()) {
return ParserResult::runtimeError("Could not parse '" + shardIndex + "' as shard index");
}
if (parsedIndex < 0) {
return ParserResult::runtimeError("Shard index must be a non-negative number");
}
config.shardIndex = static_cast<unsigned int>(parsedIndex);
return ParserResult::ok(ParseResultType::Matched);
} CATCH_CATCH_ANON(std::exception const&) {
return ParserResult::runtimeError("Could not parse '" + shardIndex + "' as shard index");
}
};
auto cli
= ExeName( config.processName )
| Help( config.showHelp )
| Opt( config.showSuccessfulTests )
["-s"]["--success"]
( "include successful tests in output" )
| Opt( config.shouldDebugBreak )
["-b"]["--break"]
( "break into debugger on failure" )
| Opt( config.noThrow )
["-e"]["--nothrow"]
( "skip exception tests" )
| Opt( config.showInvisibles )
["-i"]["--invisibles"]
( "show invisibles (tabs, newlines)" )
| Opt( config.defaultOutputFilename, "filename" )
["-o"]["--out"]
( "default output filename" )
| Opt( accept_many, setReporter, "name[:output-file]" )
["-r"]["--reporter"]
( "reporter to use (defaults to console)" )
| Opt( config.name, "name" )
["-n"]["--name"]
( "suite name" )
| Opt( [&]( bool ){ config.abortAfter = 1; } )
["-a"]["--abort"]
( "abort at first failure" )
| Opt( [&]( int x ){ config.abortAfter = x; }, "no. failures" )
["-x"]["--abortx"]
( "abort after x failures" )
| Opt( accept_many, setWarning, "warning name" )
["-w"]["--warn"]
( "enable warnings" )
| Opt( [&]( bool flag ) { config.showDurations = flag ? ShowDurations::Always : ShowDurations::Never; }, "yes|no" )
["-d"]["--durations"]
( "show test durations" )
| Opt( config.minDuration, "seconds" )
["-D"]["--min-duration"]
( "show test durations for tests taking at least the given number of seconds" )
| Opt( loadTestNamesFromFile, "filename" )
["-f"]["--input-file"]
( "load test names to run from a file" )
| Opt( config.filenamesAsTags )
["-#"]["--filenames-as-tags"]
( "adds a tag for the filename" )
| Opt( config.sectionsToRun, "section name" )
["-c"]["--section"]
( "specify section to run" )
| Opt( setVerbosity, "quiet|normal|high" )
["-v"]["--verbosity"]
( "set output verbosity" )
| Opt( config.listTests )
["--list-tests"]
( "list all/matching test cases" )
| Opt( config.listTags )
["--list-tags"]
( "list all/matching tags" )
| Opt( config.listReporters )
["--list-reporters"]
( "list all reporters" )
| Opt( setTestOrder, "decl|lex|rand" )
["--order"]
( "test case order (defaults to decl)" )
| Opt( setRngSeed, "'time'|'random-device'|number" )
["--rng-seed"]
( "set a specific seed for random numbers" )
| Opt( setColourMode, "ansi|win32|none|default" )
["--colour-mode"]
( "should output be colourised" )
| Opt( config.libIdentify )
["--libidentify"]
( "report name and version according to libidentify standard" )
| Opt( setWaitForKeypress, "never|start|exit|both" )
["--wait-for-keypress"]
( "waits for a keypress before exiting" )
| Opt( config.benchmarkSamples, "samples" )
["--benchmark-samples"]
( "number of samples to collect (default: 100)" )
| Opt( config.benchmarkResamples, "resamples" )
["--benchmark-resamples"]
( "number of resamples for the bootstrap (default: 100000)" )
| Opt( config.benchmarkConfidenceInterval, "confidence interval" )
["--benchmark-confidence-interval"]
( "confidence interval for the bootstrap (between 0 and 1, default: 0.95)" )
| Opt( config.benchmarkNoAnalysis )
["--benchmark-no-analysis"]
( "perform only measurements; do not perform any analysis" )
| Opt( config.benchmarkWarmupTime, "benchmarkWarmupTime" )
["--benchmark-warmup-time"]
( "amount of time in milliseconds spent on warming up each test (default: 100)" )
| Opt( setShardCount, "shard count" )
["--shard-count"]
( "split the tests to execute into this many groups" )
| Opt( setShardIndex, "shard index" )
["--shard-index"]
( "index of the group of tests to execute (see --shard-count)" ) |
Opt( config.allowZeroTests )
["--allow-running-no-tests"]
( "Treat 'No tests run' as a success" )
| Arg( config.testsOrTags, "test name|pattern|tags" )
( "which test or tests to use" );
return cli;
}
} // end namespace Catch