FCF 2.0 development in progress...
> > > >
[News] [C++ Libraries API] [C++ Downloads] [Donate to the project] [Contacts]

Logging System: Custom Targets and Multi-Stream Output

While the default behavior of fcfTest is to output logs to the standard console, real-world testing scenarios often demand more. You might need to simultaneously stream human-readable logs to the terminal while generating machine-readable junit XML files for your CI/CD pipeline. Or perhaps you want to log high-verbosity debug information to a dedicated file without cluttering the console.

Understanding Output Targets

Each individual logger output stream is described by the fcf::NTest::Logger::OutputTarget object.

An fcf::NTest::Logger::OutputTarget is a configuration object that defines a single destination for log messages. It contains four essential properties:

  • stream: A pointer to an std::ostream (e.g., std::cout, std::cerr, or a file stream).
  • name: A unique identifier for this target.
  • format: The name of the formatter to use for this specific target. If an empty string is specified, the format set globally for the logger is used.
  • level: The logging level threshold for this specific output target. If the property value is equal to fcf::NTest::LL_DEF, the logging level set in the logger object (via the fcf::NTest::Logger::level method) is used.
Output Target Names

Each target in the logging system must have a unique name within the logger object. Adding a new target is performed by calling fcf::NTest::Logger::appendTarget. If a target with the same name is already registered, it will be overwritten.

By default, the system always registers at least one target named "default", which directs output to the console.

Additionally, if you configure file output via command-line arguments or by setting the fcf::NTest::Options::files property for fcf::NTest::cmdRun, the system automatically creates targets named "file-[FORMAT]".

Programmatic Multi-Targeting

You can register multiple targets using the appendTarget method. Once registered, the logger will broadcast every log message to every active target. This is how you achieve simultaneous output to different streams.

The following example demonstrates how to configure a logger to output standard messages to the console while simultaneously writing a JUnit XML report to a file.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> #include <iostream> #include <fstream> // A simple test to trigger logging FCF_TEST_DEFINE("TargetDemo", "Logger", "MultiTargetTest") { fcf::NTest::log() << "This message goes to both streams!" << std::endl; FCF_TEST(true); } int main(int a_argc, char* a_argv[]) { fcf::NTest::Logger& logger = fcf::NTest::logger(); // 1. Prepare a file stream for the XML report // We use 'static' to ensure the stream remains valid during the entire test run static std::ofstream fileStream("test_results.xml"); // 2. Configure a target for the standard console output fcf::NTest::Logger::OutputTarget consoleTarget; consoleTarget.name = "default"; consoleTarget.stream = &std::cout; // Using "default" format for the terminal consoleTarget.format = "default"; // 3. Configure a target for the file output fcf::NTest::Logger::OutputTarget fileTarget; fileTarget.name = "my-junit-file"; fileTarget.stream = &fileStream; // Specifying "junit" format for the XML file fileTarget.format = "junit"; // 4. Register both targets // The logger will now broadcast every log event to both destinations logger.appendTarget(consoleTarget); logger.appendTarget(fileTarget); bool error = false; fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }
Terminal Output:
Performing the test: "TargetDemo" -> "Logger" -> "MultiTargetTest" ... > This message goes to both streams! [SUCCESS] Test completed successfully (0.000`005`123 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`005`123 sec
File Content (test_results.xml):
<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="1" failure="0" skipped="0" time="0.000005123"> <testsuite name="TargetDemo/Logger" tests="1" failure="0" skipped="0" time="0.000005123"> <testcase classname="TargetDemo/Logger" name="MultiTargetTest" time="0.000005123"/> </testsuite> </testsuites>

Controlling the Default Format

Every fcf::NTest::Logger::OutputTarget has a format property. This property determines which formatter is used for that specific stream. However, you don't always have to specify it manually.

If you leave the format as an empty string, the target will automatically inherit the global default format currently set in the logger's environment.

The global default format can be established in two ways:

1. Via Command Line

This is the most convenient method for CI/CD pipelines. Use the --test-format flag to change the default for all targets that haven't specified their own format:

# All targets without a specific format will now use JUnit $ ./my_tests --test-format junit
2. Via Programmatic Configuration

If you want to hardcode a default format within your application, you can configure the fcf::NTest::Options structure before running the tests:

fcf::NTest::Options options; // Set the global default to junit options.format = "junit"; bool error = false; // The runner will apply this format to all targets with an empty format string fcf::NTest::cmdRun(options, a_argc, a_argv, fcf::NTest::CRM_RUN, &error);
Summary of Format Resolution

When a log message is sent to a target, the logger follows this logic to decide the format:

  1. Check if the fcf::NTest::Logger::OutputTarget has a non-empty format string. If yes, use that specific format.
  2. If the target's format is empty, use the format specified in the fcf::NTest::Options passed to cmdRun.
  3. If no options were provided, use the format currently set in the fcf::NTest::Logger singleton.