A Catch2 File Listener
Catch2 has support for reporting the test results output to a file
using the -o, --out
flag. This redirection, redirects all output. We had a
desire to output all testing information in a style like Junit while also
providing the tester the nice failure only output on the command line.
Catch2 does not support multiple reporters, but it can support multiple listeners.
Reporters and Listeners
Catch2 uses the terms listeners and reporters. From a users perspective a reporter is basically the output style of the test results. By default only the failed test information is provided.
A listener recieves all of the test events, even passing test info. A listener is a bit more generic in that it is meant do anything an integrator might find a use for. This is most likely the reason multiple listeners are supported and there is no generic output file flag for them.
An interesting example of a listener that I ran across on the googles is https://doc.froglogic.com/squish-coco/latest/Catch2Listener.cpp.html and the accompanying documentation https://doc.froglogic.com/squish-coco/latest/integration.html#catch2.
Doing a quick look, I wasn’t sure on the licensing of the code so I chose not to include the raw code here.
This listener ends up separating the coverage report for each test case and/or section. I haven’t read the full documentation, but it seems one could more easly know which tests target which parts of the code.
The Custom Listener
At the time of this writing Catch2 is transitioning to version 3. This custom listener example will be based on version 2.
#pragma once
#include <iostream>
#define CATCH_CONFIG_EXTERNAL_INTERFACES
#include "catch2/catch.hpp"
class Listener : public Catch::TestEventListenerBase {
public:
Listener( Catch::ReporterConfig const& _config ) : Catch::TestEventListenerBase(_config){
m_stream = std::unique_ptr<Catch::IStream const>(Catch::makeStream(outputFilename));
}
void testCaseStarting( Catch::TestCaseInfo const& testInfo ) override {
m_stream->stream() << "Starting a test case";
}
static void SetOutputFilename(const std::string& name){
outputFilename = name;
}
protected:
std::unique_ptr<Catch::IStream const> m_stream;
static std::string outputFilename;
};
What we have here is a class derving from Catch2’s base listener class
Catch::TestEventListenerBase
. Per the documentation on listeners,
all of the base class methods have default implementations. One only needs to
override the listener events they care about. To keep the example small we’ve
only overridden the test case starting method.
Since Catch2 won’t pass any other arguments to the constructur, we utilize a
static method and member to hold the destination filename. When the listener is
instantiated we provide the filename to Catch2’s makeStream()
function. This
function isn’t publicly documented, but the code is visible in the header. If
the input stream name is empty then the provided stream will be Catch2’s
standard out stream. See
CATCH_CONFIG_NOSTDOUT.
When the input stream name isn’t empty it’s assumed to be a filename to create a
stream for.
The Test main()
#define CATCH_CONFIG_RUNNER
#include "catch2/catch.hpp"
#include "Listener.hpp"
CATCH_REGISTER_LISTENER( Listener )
int main( int argc, char* argv[] ) {
Listener::SetOutputFilename("my_file.txt");
int result = Catch::Session().run( argc, argv );
return result;
}
TEST_CASE("Testing stuff"){
REQUIRE(3==4);
}
In order to ensure we can set the output file name up before the listener is
instantiated we create our own main function. Catch2 can provide a default for
you via the CATCH_CONFIG_MAIN
macro, but this will prevent setting the output
filename up in time.
CATCH_REGISTER_LISTENER( Listener )
is how we tell Catch2 to utilize our
listener. This adds the class to the internal listener registry for Catch2, but
it does not instantiate the instance. One thing to be aware of is that if the
CATCH_REGISTER_LISTENER( Listener )
is placed in a separate file and there are
no other symbols in that file that the linker needs for the program, then the
CATCH_REGISTER_LISTENER()
macro will not get linked in and thus the listener
will not be available.
Listener::SetOutputFilename("my_file.txt");
sets the filename of the output
file. One may want better logic here as this example will be relative to the
current working directory when the test executable is ran.
We then kick off the test execution with,
int result = Catch::Session().run( argc, argv );
. It’s during the run()
method that our custom listener will be instantiated.
If one runs this test there will be a file named my_file.txt
created in the
current working directory and it’s contents will have Starting a test case
. At
the same time one will get the normal Catch2 output in the terminal.
Summary
Though Catch2 does not natively provide file redirection for listeners, one can add it to their specific listener class. Adding a listener, instead of a reporter, allows one to maintain the default terminal output of Catch2 while still archiving test result information in a file.
The example was kept short and simply so as to prevent overload. Hopefully it’s not much more work for one to figure out the other event methods listed for listeners. It may also take some work, but one might imagine how they could make the listener conditional on the availability of an output file name.