While the built-in formats like default and junit cover most standard needs, professional debugging often requires specialized output. You might want to generate JSON logs for automated ingestion, add specific decorative elements for terminal readability, or even change the entire structure of a message based on its origin.
In fcfTest, this is achieved through the Custom Format mechanism.
The Anatomy of a Formatter
A custom format is defined using the fcf::NTest::Logger::Format structure. This structure acts as a configuration object that tells the logger how to transform a raw log event into a final string.
The most critical component is the handler. This is a callback function of type fcf::NTest::Logger::FormatFunction. When a log event occurs, the logger invokes this handler, passing it all the necessary information about the event.
The Heart of Formatting: MessageContext
The handler receives a reference to a fcf::NTest::Logger::MessageContext object. This object is a data carrier that encapsulates everything about the current log entry:
- message: The actual text content of the log message.
- level: The severity level (e.g., Info, Error, Debug).
- category: The bitmask identifying the message group.
- origin: Contains the original message string.
By modifying the message property inside the handler, you effectively change what is eventually written to the output stream.
Example 1: Simple Decorative Formatter
This example demonstrates a basic formatter that wraps every log message in decorative brackets.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
#include <iostream>
#include <string>
FCF_TEST_DEFINE("FormatDemo", "Simple", "BracketsTest") {
// Log a message to see the effect
fcf::NTest::log() << "Hello, World!" << std::endl;
FCF_TEST(true);
}
int main(int a_argc, char* a_argv[]) {
// 1. Create the Format configuration
fcf::NTest::Logger::Format simpleFormat;
simpleFormat.name = "brackets";
// 2. Define the handler logic
// We wrap the message in [ ] and add a newline
simpleFormat.handler = [](fcf::NTest::Logger&, fcf::NTest::Logger::MessageContext& a_context) {
size_t last = a_context.message.find_last_not_of(" \t\n\r");
std::string msg = last != std::string::npos ? a_context.message.substr(0, last + 1)
: std::string();
a_context.message = "[" + msg + "]\n";
};
// 3. Register the format with the global logger
fcf::NTest::logger().appendFormat(simpleFormat);
bool error = false;
fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Run the app using our new "brackets" format.
> ./my_tests --test-format="brackets"
Terminal output:
[Performing the test: "FormatDemo" -> "Simple" -> "BracketsTest" ...]
[ > Hello, World!]
[ [SUCCESS] Test completed successfully (0.000`009`488 sec)]
[]
[[SUCCESS] All tests were completed.]
[Tests: 1 passed, 0 failed, 0 skipped, 1 total]
[Duration: 0.000`009`488 sec]
If you want the application to start with the "brackets" format by default, you can specify the format via the fcf::NTest::Options::format field of the fcf::NTest::Options structure and pass it to the fcf::NTest::cmdRun() function.
Example 2: Creating a custom JSON format.
This is a professional-grade example.
We create a formatter that inspects the category bitmask to apply different processing logic.
To store the internal data required for the custom format, we use the fcf::NTest::Logger::MessageContext::data property, which is passed with every message and is bound to a specific format handler.
#define FCF_TEST_IMPLEMENTATION
#include <fcfTest/test.hpp>
#include <string>
#include <vector>
/// @brief Structure for storing the current logging state
///
/// Used to store current messages in the context of the format
/// for JSON generation
struct TestInfo {
struct Message {
unsigned int category;
std::string message;
};
size_t caseCounter; //< Test number is used to insert commas between objects
std::vector<Message> messages; //< List of logger messages that were output during the test
TestInfo()
: caseCounter(0)
{}
};
///
/// @brief Removes whitespace characters from the right.
///
std::string rtrim(const std::string& a_str) {
size_t last = a_str.find_last_not_of(" \t\n\r");
return last != std::string::npos ? a_str.substr(0, last + 1)
: std::string();
}
///
/// @brief Escapes quotes for string output in JSON.
///
std::string escapeJson(const std::string& input) {
std::string res;
for (char c : input) {
if (c == '"' || c == '\\') res += '\\';
res += c;
}
return res;
}
FCF_TEST_DEFINE("FormatDemo", "Smart", "CategoryTest") {
// 1. A standard user message (default category)
fcf::NTest::log() << "User is logging in..." << std::endl;
// 2. A message explicitly marked as a User Group message with custom code
fcf::NTest::log(fcf::NTest::LMC_USER_GROUP + 1) << "User clicked button." << std::endl;
FCF_TEST(true);
}
int main(int a_argc, char* a_argv[]) {
// 1. Setup the JSON Formatter
fcf::NTest::Logger::Format smartFormat;
smartFormat.name = "my-json";
smartFormat.handler = [](fcf::NTest::Logger&, fcf::NTest::Logger::MessageContext& a_context) {
// Handle system message about the start of testing
if (a_context.category == fcf::NTest::LMC_ROOT_START) {
// Create an object to store data and assign it to the format data
*a_context.data = fcf::NTest::SharedPtrAny::make<TestInfo>();
// Replace the message with the JSON array opening symbol.
a_context.message = "[\n";
// Mark the message as non-system so that this message goes into the output stream.
a_context.system = false;
return;
// Handle system message about the end of testing
} else if (a_context.category == fcf::NTest::LMC_ROOT_END) {
// Replace the message with the JSON array closing symbol.
a_context.message = "\n]\n";
// Mark the message as non-system so that this message goes into the output stream.
a_context.system = false;
// Clear the object for storing internal data.
a_context.data->release();
return;
}
// If internal test data for the current format is not set, stop processing.
if (!a_context.data->is<TestInfo>()){
// Mark the message as system, thereby preventing its output to the output stream
a_context.system = true;
return;
}
// Get the format data that we set up
TestInfo* testInfo = a_context.data->cast<TestInfo>();
// Handle system message about the start of an individual test parameter execution
if (a_context.category == fcf::NTest::LMC_LAUNCH_CASE_START) {
// Increment the test counter.
++testInfo->caseCounter;
// Clear logging messages
testInfo->messages.clear();
// Handle system message about the end of an individual test parameter execution
// Here we form a JSON object with a list of messages
// that were output during the test execution.
} else if (a_context.category == fcf::NTest::LMC_LAUNCH_CASE_END) {
a_context.message = "";
if (testInfo->caseCounter > 1) {
a_context.message += ",\n";
}
a_context.message += " {\n";
a_context.message += std::string() + " \"part\": \"" + escapeJson(fcf::NTest::state().test().part) + "\",\n";
a_context.message += std::string() + " \"group\": \"" + escapeJson(fcf::NTest::state().test().group) + "\",\n";
a_context.message += std::string() + " \"test\": \"" + escapeJson(fcf::NTest::state().test().test) + "\",\n";
a_context.message += std::string() + " \"case\": \"" + std::to_string(fcf::NTest::state().paramIndex()) + "\",\n";
a_context.message += std::string() + " \"messages\": [\n";
for(size_t i = 0; i < testInfo->messages.size(); ++i) {
if (i) {
a_context.message += ",\n";
}
auto& msg = testInfo->messages[i];
a_context.message += std::string() +
" { \"code\": " + std::to_string(msg.category) +
", \"message\": \"" + escapeJson(rtrim(msg.message)) +
"\" }";
}
a_context.message += "\n ]\n";
a_context.message += " }";
// Mark the message as non-system so that this message goes into the output stream.
a_context.system = false;
// Handle all other messages,
// simply put them in a buffer for storage,
// for subsequent JSON output
} else {
testInfo->messages.push_back(TestInfo::Message{a_context.category, a_context.origin});
// Mark the message as system, thereby preventing its output to the output stream
a_context.system = true;
}
};
// 2. Register the smart format
fcf::NTest::logger().appendFormat(smartFormat);
// 3. Run the application
bool error = false;
fcf::NTest::cmdRun(a_argc, a_argv, fcf::NTest::CRM_RUN, &error);
return error ? 1 : 0;
}
Run the application, specifying our new format "my-json".
./my_test --test-format="my-json"
Terminal output:
[
{
"part": "FormatDemo",
"group": "Smart",
"test": "CategoryTest",
"case": "0",
"messages": [
{ "code": 524288, "message": "User is logging in..." },
{ "code": 524289, "message": "User clicked button." }
]
}
]
Summary
Custom formatting is a powerful tool for making your test logs more readable and machine-parsable. By understanding the MessageContext and utilizing the bitmask nature of the category, you can transform a simple stream of text into a rich, structured diagnostic tool.