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.
This commit is contained in:
Martin Hořeňovský
2022-04-04 21:07:04 +02:00
parent 3c06bcb374
commit 423e1d2ebb
29 changed files with 395 additions and 448 deletions

View File

@@ -33,17 +33,6 @@ namespace Catch {
} // unnamed namespace
} // namespace Detail
std::ostream& operator<<( std::ostream& os,
ConfigData::ReporterAndFile const& reporter ) {
os << "{ " << reporter.reporterName << ", ";
if ( reporter.outputFileName ) {
os << *reporter.outputFileName;
} else {
os << "<default-output>";
}
return os << " }";
}
Config::Config( ConfigData const& data ):
m_data( data ) {
// We need to trim filter specs to avoid trouble with superfluous
@@ -76,14 +65,14 @@ namespace Catch {
#else
"console",
#endif
{}
{}, {}, {}
} );
}
bool defaultOutputUsed = false;
m_reporterStreams.reserve( m_data.reporterSpecifications.size() );
for ( auto const& reporterAndFile : m_data.reporterSpecifications ) {
if ( reporterAndFile.outputFileName.none() ) {
for ( auto const& reporterSpec : m_data.reporterSpecifications ) {
if ( reporterSpec.outputFile().none() ) {
CATCH_ENFORCE( !defaultOutputUsed,
"Internal error: cannot use default output for "
"multiple reporters" );
@@ -93,7 +82,7 @@ namespace Catch {
openStream( data.defaultOutputFilename ) );
} else {
m_reporterStreams.push_back(
openStream( *reporterAndFile.outputFileName ) );
openStream( *reporterSpec.outputFile() ) );
}
}
}
@@ -108,7 +97,7 @@ namespace Catch {
std::vector<std::string> const& Config::getTestsOrTags() const { return m_data.testsOrTags; }
std::vector<std::string> const& Config::getSectionsToRun() const { return m_data.sectionsToRun; }
std::vector<ConfigData::ReporterAndFile> const& Config::getReportersAndOutputFiles() const {
std::vector<ReporterSpec> const& Config::getReporterSpecs() const {
return m_data.reporterSpecifications;
}

View File

@@ -14,6 +14,7 @@
#include <catch2/internal/catch_unique_ptr.hpp>
#include <catch2/internal/catch_optional.hpp>
#include <catch2/internal/catch_random_seed_generation.hpp>
#include <catch2/internal/catch_reporter_spec_parser.hpp>
#include <iosfwd>
#include <vector>
@@ -24,17 +25,6 @@ namespace Catch {
class IStream;
struct ConfigData {
struct ReporterAndFile {
std::string reporterName;
// If none, the output goes to the default output.
Optional<std::string> outputFileName;
friend bool operator==(ReporterAndFile const& lhs, ReporterAndFile const& rhs) {
return lhs.reporterName == rhs.reporterName && lhs.outputFileName == rhs.outputFileName;
}
friend std::ostream& operator<<(std::ostream &os, ReporterAndFile const& reporter);
};
bool listTests = false;
bool listTags = false;
@@ -72,7 +62,7 @@ namespace Catch {
std::string defaultOutputFilename;
std::string name;
std::string processName;
std::vector<ReporterAndFile> reporterSpecifications;
std::vector<ReporterSpec> reporterSpecifications;
std::vector<std::string> testsOrTags;
std::vector<std::string> sectionsToRun;
@@ -90,7 +80,7 @@ namespace Catch {
bool listTags() const;
bool listReporters() const;
std::vector<ConfigData::ReporterAndFile> const& getReportersAndOutputFiles() const;
std::vector<ReporterSpec> const& getReporterSpecs() const;
IStream const* getReporterOutputStream(std::size_t reporterIdx) const;
std::vector<std::string> const& getTestsOrTags() const override;

View File

@@ -41,11 +41,18 @@ namespace Catch {
return reporter;
}
IStreamingReporterPtr makeReporter(Config const* config) {
IStreamingReporterPtr prepareReporters(Config const* config) {
if (Catch::getRegistryHub().getReporterRegistry().getListeners().empty()
&& config->getReportersAndOutputFiles().size() == 1) {
&& config->getReporterSpecs().size() == 1) {
auto const& spec = config->getReporterSpecs()[0];
auto stream = config->getReporterOutputStream(0);
return createReporter(config->getReportersAndOutputFiles()[0].reporterName, ReporterConfig(config, stream));
return createReporter(
config->getReporterSpecs()[0].name(),
ReporterConfig(
config,
stream,
spec.colourMode().valueOr( config->colourMode() ),
spec.customOptions() ) );
}
auto multi = Detail::make_unique<MultiReporter>(config);
@@ -56,9 +63,15 @@ namespace Catch {
}
std::size_t reporterIdx = 0;
for (auto const& reporterAndFile : config->getReportersAndOutputFiles()) {
for (auto const& reporterSpec : config->getReporterSpecs()) {
auto stream = config->getReporterOutputStream(reporterIdx);
multi->addReporter(createReporter(reporterAndFile.reporterName, ReporterConfig(config, stream)));
multi->addReporter( createReporter(
reporterSpec.name(),
ReporterConfig( config,
stream,
reporterSpec.colourMode().valueOr(
config->colourMode() ),
reporterSpec.customOptions() ) ) );
reporterIdx++;
}
@@ -304,7 +317,7 @@ namespace Catch {
getCurrentMutableContext().setConfig(m_config.get());
// Create reporter(s) so we can route listings through them
auto reporter = makeReporter(m_config.get());
auto reporter = prepareReporters(m_config.get());
auto const& invalidSpecs = m_config->testSpec().getInvalidSpecs();
if ( !invalidSpecs.empty() ) {

View File

@@ -14,17 +14,31 @@
#include <catch2/internal/catch_string_manip.hpp>
#include <catch2/catch_test_case_info.hpp>
#include <catch2/reporters/catch_reporter_helpers.hpp>
#include <catch2/internal/catch_move_and_forward.hpp>
#include <algorithm>
#include <iomanip>
namespace Catch {
ReporterConfig::ReporterConfig( IConfig const* _fullConfig, IStream const* _stream )
: m_stream( _stream ), m_fullConfig( _fullConfig ) {}
ReporterConfig::ReporterConfig(
IConfig const* _fullConfig,
IStream const* _stream,
ColourMode colourMode,
std::map<std::string, std::string> customOptions ):
m_stream( _stream ),
m_fullConfig( _fullConfig ),
m_colourMode( colourMode ),
m_customOptions( CATCH_MOVE( customOptions ) ) {}
IStream const* ReporterConfig::stream() const { return m_stream; }
IConfig const * ReporterConfig::fullConfig() const { return m_fullConfig; }
ColourMode ReporterConfig::colourMode() const { return m_colourMode; }
std::map<std::string, std::string> const&
ReporterConfig::customOptions() const {
return m_customOptions;
}
AssertionStats::AssertionStats( AssertionResult const& _assertionResult,
std::vector<MessageInfo> const& _infoMessages,

View File

@@ -19,6 +19,7 @@
#include <catch2/benchmark/catch_outlier_classification.hpp>
#include <map>
#include <string>
#include <vector>
#include <iosfwd>
@@ -31,16 +32,24 @@ namespace Catch {
class TestCaseHandle;
struct IConfig;
class IStream;
enum class ColourMode : std::uint8_t;
struct ReporterConfig {
ReporterConfig( IConfig const* _fullConfig, IStream const* _stream );
ReporterConfig( IConfig const* _fullConfig,
IStream const* _stream,
ColourMode colourMode,
std::map<std::string, std::string> customOptions );
IStream const* stream() const;
IConfig const* fullConfig() const;
ColourMode colourMode() const;
std::map<std::string, std::string> const& customOptions() const;
private:
IStream const* m_stream;
IConfig const* m_fullConfig;
ColourMode m_colourMode;
std::map<std::string, std::string> m_customOptions;
};
struct TestRunInfo {

View File

@@ -141,57 +141,42 @@ namespace Catch {
return ParserResult::runtimeError( "Unrecognised verbosity, '" + verbosity + '\'' );
return ParserResult::ok( ParseResultType::Matched );
};
auto const setReporter = [&]( std::string const& reporterSpec ) {
if ( reporterSpec.empty() ) {
auto const setReporter = [&]( std::string const& userReporterSpec ) {
if ( userReporterSpec.empty() ) {
return ParserResult::runtimeError( "Received empty reporter spec." );
}
// Exactly one of the reporters may be specified without an output
// file, in which case it defaults to the output specified by "-o"
// (or standard output).
static constexpr auto separator = "::";
static constexpr size_t separatorSize = 2;
auto fileNameSeparatorPos = reporterSpec.find( separator );
const bool containsFileName = fileNameSeparatorPos != reporterSpec.npos;
if ( containsFileName ) {
auto nextSeparatorPos = reporterSpec.find(
separator, fileNameSeparatorPos + separatorSize );
if ( nextSeparatorPos != reporterSpec.npos ) {
return ParserResult::runtimeError(
"Too many separators in reporter spec '" + reporterSpec + '\'' );
}
Optional<ReporterSpec> parsed =
parseReporterSpec( userReporterSpec );
if ( !parsed ) {
return ParserResult::runtimeError(
"Could not parse reporter spec '" + userReporterSpec +
"'" );
}
std::string reporterName;
Optional<std::string> outputFileName;
reporterName = reporterSpec.substr( 0, fileNameSeparatorPos );
if ( reporterName.empty() ) {
return ParserResult::runtimeError( "Reporter name cannot be empty." );
}
if ( containsFileName ) {
outputFileName = reporterSpec.substr(
fileNameSeparatorPos + separatorSize, reporterSpec.size() );
}
auto const& reporterSpec = *parsed;
IReporterRegistry::FactoryMap const& factories =
getRegistryHub().getReporterRegistry().getFactories();
auto result = factories.find( reporterName );
auto result = factories.find( reporterSpec.name() );
if( result == factories.end() )
return ParserResult::runtimeError( "Unrecognized reporter, '" + reporterName + "'. Check available with --list-reporters" );
if( containsFileName && outputFileName->empty() )
return ParserResult::runtimeError( "Reporter '" + reporterName + "' has empty filename specified as its output. Supply a filename or remove the colons to use the default output." );
if ( result == factories.end() ) {
return ParserResult::runtimeError(
"Unrecognized reporter, '" + reporterSpec.name() +
"'. Check available with --list-reporters" );
}
config.reporterSpecifications.push_back({ std::move(reporterName), std::move(outputFileName) });
// 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 (!containsFileName) {
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.outputFileName.none()) {
if (spec.outputFile().none()) {
n_reporters_without_file++;
}
}

View File

@@ -18,7 +18,9 @@ namespace Catch {
IEventListener( config.fullConfig() ),
m_wrapped_stream( config.stream() ),
m_stream( m_wrapped_stream->stream() ),
m_colour( makeColourImpl( m_config->colourMode(), m_wrapped_stream ) ) {}
m_colour( makeColourImpl( config.colourMode(), m_wrapped_stream ) ),
m_customOptions( config.customOptions() )
{}
ReporterBase::~ReporterBase() = default;

View File

@@ -10,6 +10,9 @@
#include <catch2/interfaces/catch_interfaces_reporter.hpp>
#include <map>
#include <string>
namespace Catch {
class ColourImpl;
@@ -30,7 +33,10 @@ namespace Catch {
//! Cached output stream from `m_wrapped_stream` to reduce
//! number of indirect calls needed to write output.
std::ostream& m_stream;
//! Colour implementation this reporter was configured for
Detail::unique_ptr<ColourImpl> m_colour;
//! The custom reporter options user passed down to the reporter
std::map<std::string, std::string> m_customOptions;
public:
ReporterBase( ReporterConfig const& config );