In complex testing environments, simply knowing whether a test passed or failed is often not enough. You might need to track the internal state of your system, monitor long-running processes, or capture diagnostic information during a specific phase of execution. The fcfTest library provides a built-in logging system designed specifically for this purpose.
How the Logger Works: Internal Nuances
To use the logging system effectively, it is important to understand its internal architecture.
The Singleton Pattern
The logger is implemented as a global singleton. This means that when you call logger(), you are interacting with the exact same instance throughout your entire application. Any configuration change (like changing the log level) made in one part of the code will immediately affect all subsequent logging calls in all tests.
The Threshold Mechanism
The filtering is not based on "importance" in the traditional sense, but on a strict numerical threshold. Internally, each level is assigned a value. When a log call is made, the framework compares the message level to the current global threshold. If Message_Level < Threshold_Level, the message is discarded immediately, minimizing the performance overhead of string construction in production runs.
Output Targets
The logger does not simply print to std::cout. It directs messages to one or more Targets. A target is an abstraction of an output destination. By default, the framework provides a console target, but the architecture allows the logger to simultaneously send output to multiple streams, such as:
- The standard terminal output.
- A log file on disk.
- A
std::stringstream for capturing logs during automated testing of the testing framework itself.
This decoupling of what is logged from where it is sent is what makes fcfTest's logging system both flexible and powerful.
Basic Usage
Logging in fcfTest is designed to be as intuitive as using std::cout. You access the global logger through the log function, which returns a stream object.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
#include <iostream>
FCF_TEST_DEFINE("Demo", "Logging", "Basic") {
// Simple logging during test execution
fcf::NTest::log() << "Starting the process..." << std::endl;
int status = 42;
fcf::NTest::log() << "Current status: " << status << std::endl;
FCF_TEST(status == 42);
}
int main(int argc, char** argv) {
bool error = false;
fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Terminal output:
Performing the test: "Demo" -> "Logging" -> "Basic" ...
> Starting the process...
> Current status: 42
[SUCCESS] Test completed successfully (0.000`016`017 sec)
[SUCCESS] All tests were completed.
Tests: 1 passed, 0 failed, 0 skipped, 1 total
Duration: 0.000`016`017 sec
Controlling Verbosity (Log Levels)
To prevent your console from being flooded with unnecessary information, fcfTest uses a threshold-based filtering system. Every log message is associated with a specific level. If the current global logging level is "higher" than the message's level, the message is ignored.
The following table lists all available levels, ordered from least verbose (highest priority/restriction) to most verbose:
| Level (str) |
Level (int) |
Level (function) |
Description |
| "off" |
fcf::NTest::LL_OFF |
|
All logging is disabled. |
| "ftl" |
fcf::NTest::LL_FTL |
fcf::NTest::ftl() |
Fatal: Critical errors that prevent further execution. |
| "err" |
fcf::NTest::LL_ERR |
fcf::NTest::err() |
Error: Significant issues that caused a test failure. |
| "wrn" |
fcf::NTest::LL_WRN |
fcf::NTest::wrn() |
Warning: Potential issues that don't break the test. |
| "att" |
fcf::NTest::LL_ATT |
fcf::NTest::att() |
Attention: Important notices for the developer. |
| "att" |
fcf::NTest::LL_ATT |
fcf::NTest::att() |
Attention: Important notices for the developer. |
| "log" |
fcf::NTest::LL_LOG |
fcf::NTest::log() |
Log: Standard execution messages (Default). |
| "inf" |
fcf::NTest::LL_INF |
fcf::NTest::inf() |
Info: General information about the system state. |
| "dbg" |
fcf::NTest::LL_DBG |
fcf::NTest::dbg() |
Debug: Detailed information for troubleshooting. |
| "trc" |
fcf::NTest::LL_TRC |
fcf::NTest::trc() |
Trace: Extremely granular step-by-step execution details. |
| "all" |
fcf::NTest::LL_ALL |
|
All messages are shown. |
| "def" |
fcf::NTest::LL_DEF |
|
Uses the application's default level. |
Configuration Options
You can change the logging threshold in two ways: via the command line (ideal for CI/CD and manual runs) or directly in your C++ code (ideal for hardcoding specific debug modes).
1. Command Line Interface
Use the --test-log-level parameter to set the threshold without recompiling:
# Run tests with only Error and Fatal messages
$ ./my_tests --test-log-level err
# Run tests with maximum verbosity for deep tracing
$ ./my_tests --test-log-level trc
2. Programmatic Configuration
You can control the logger using the logger singleton. This is useful if you want to programmatically enable more verbosity when certain conditions are met.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") {
fcf::NTest::inf() << "This is a trace message." << std::endl;
}
int main(int argc, char** argv) {
// Set the logging threshold to INFO via code.
// This means 'dbg' and 'trc' will be hidden.
// And 'ftl', 'err', 'wrn', 'att', 'log', and 'inf' will be shown.
// Note that calling fcf::NTest::cmdRun will override this value
// if the --test-log-level parameter is specified.
// Therefore, this method sets the default logging level.
fcf::NTest::logger().level(fcf::NTest::ELogLevel::LL_INF);
bool error = false;
fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Launching the application normally will start it with the "info" logging level.
$ test_app
Terminal output:
Performing the test: "Demo" -> "Logging" -> "CodeConfig" ...
> This is a trace message.
[SUCCESS] Test completed successfully (0.000`009`807 sec)
[SUCCESS] All tests were completed.
Tests: 1 passed, 0 failed, 0 skipped, 1 total
Duration: 0.000`009`807 sec
However, you can specify a particular logging level if needed.
$ test_app --test-log-level log
Terminal output:
Performing the test: "Demo" -> "Logging" -> "CodeConfig" ...
[SUCCESS] Test completed successfully (0.000`003`085 sec)
[SUCCESS] All tests were completed.
Tests: 1 passed, 0 failed, 0 skipped, 1 total
Duration: 0.000`003`085 sec
Formats
A Format determines the structural layout of the log message. The framework allows you to set a global default format or specify a unique format for each individual target stream.
By default, the framework supports two output formats:
1. Command Line Interface
Set the global default format for all targets:
# Set the global default format to JUnit
$ ./my_tests --test-format junit
Terminal output:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="1" failure="0" skipped="0" time="0.000010434">
<testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000010434">
<testcase classname="Demo/Logging" name="CodeConfig" time="0.000010434"/>
</testsuite>
</testsuites>
2. Programmatic Configuration
You can specify the default format for your application programmatically.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") {
FCF_TEST(true);
}
int main(int argc, char** argv) {
// Set the logger output format via default options.
// The fcf::NTest::cmdRun function will apply the output format upon execution.
fcf::NTest::Options options;
options.format = "junit";
bool error = false;
fcf::NTest::cmdRun(options, argc, argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Let's run the application without specifying the format (default format):
$ ./my_tests
Terminal output:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="1" failure="0" skipped="0" time="0.000000804">
<testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000000804">
<testcase classname="Demo/Logging" name="CodeConfig" time="0.000000804"/>
</testsuite>
</testsuites>
The logger class can simultaneously output logs to different stream targets, and you can specify a separate output format for each stream. We will cover this approach to setting the format in the next section.
Output Targets
The logger does not simply print to std::cout. It directs messages to one or more Targets. A target is an abstraction of an output destination. This decoupling of what is logged from where it is sent is what makes fcfTest's logging system both flexible and powerful.
1. Command Line Interface
You can define multiple output targets and assign specific formats to them using the command line:
# Log to a file using the default format
$ ./my_tests --test-file output.log
# Log to a file using a specific pre-defined format (e.g., JUnit)
$ ./my_tests --test-file-junit report.xml
2. Programmatic Configuration
You can dynamically add new targets to the logger during runtime. This is useful for redirecting output to files or memory streams.
#include <fstream>
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") {
FCF_TEST(true);
}
int main(int argc, char** argv) {
// 1. Add an output target directed to a file.
// Use the default format, which can be set via command-line arguments.
std::ofstream log_file("debug.log");
fcf::NTest::Logger::OutputTarget fileOutputTarget;
fileOutputTarget.name = "file_target"; // The target name is defined by the user.
fileOutputTarget.stream = &log_file; // Log output stream
fileOutputTarget.format = ""; // The name of the format in which the data will be output.
// An empty string indicates the format currently set for the logger.
// Add a new target that writes to the file stream using the default format
fcf::NTest::logger().appendTarget(fileOutputTarget);
// 2. Replace the default output target
// so that it outputs the log in JUnit format
fcf::NTest::Logger::OutputTarget coutOutputTarget;
coutOutputTarget.name = "default"; // The name "default" is already reserved, so this will update the existing target.
coutOutputTarget.stream = &std::cout; // Log output stream
coutOutputTarget.format = "junit"; // Set the output format
// Replace the default target that is directed to the terminal output stream.
fcf::NTest::logger().appendTarget(coutOutputTarget);
bool error = false;
fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Terminal output:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="1" failure="0" skipped="0" time="0.000000482">
<testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000000482">
<testcase classname="Demo/Logging" name="CodeConfig" time="0.000000482"/>
</testsuite>
</testsuites>
debug.log file:
Performing the test: "Demo" -> "Logging" -> "CodeConfig" ...
^[[1;32m[SUCCESS]^[[0m Test completed successfully (0.000`000`555 sec)
^[[1;32m[SUCCESS]^[[0m All tests were completed.
Tests: 1 passed, 0 failed, 0 skipped, 1 total
Duration: 0.000`000`555 sec