xo-umbrella2/xo-ppsink/utest/scope.test.cpp
Roland Conybeare fb013ea267
All checks were successful
cmake-docker / cmake-build (clang, clang++) (push) Successful in 30m28s
cmake-docker / cmake-build (gcc, g++) (push) Successful in 29m12s
CI / smoke-test (push) Successful in 53m49s
xo-ppsink +: use appcx for pretty printer config [REFACTOR]
2026-09-02 00:10:49 -04:00

244 lines
8.5 KiB
C++

/** @file scope.test.cpp **/
#include <xo/ppsink/FlatSink.hpp>
#include <xo/ppsink/color.hpp>
#include <xo/ppsink/scope.hpp>
#include <xo/ppsink/scope_macros.hpp>
#include <catch2/catch.hpp>
#include <cctype>
#include <sstream>
#include <memory>
namespace ut {
using xo::pp::scope;
using xo::pp::scope_config;
using xo::pp::FlatSink;
using xo::pp::PpStyle;
using xo::pp::ThreadLogState;
using std::stringstream;
/* The gate's DEFAULT, asserted directly. Every other test in this file
* turns color off to pin text, so without this one the default could be
* flipped back to false and the suite would stay green -- which is how it
* came to be false in the first place (color.hpp).
*/
TEST_CASE("color-enabled-by-default", "[scope][color]")
{
scope_config::time_enabled = false;
stringstream ss;
auto sink = std::make_unique<FlatSink>(PpStyle::colored(), ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
{
scope outer("outer");
}
ThreadLogState::log_set_sink(nullptr);
/* the "(N)" nesting level is colored (scope_config::nesting_level_color,
* xterm 153), so the banner carries SGR escapes
*/
REQUIRE(ss.str().find('\033') != std::string::npos);
/* ... and with the gate off, byte-identical text without them */
{
stringstream plain_ss;
auto plain_sink = std::make_unique<FlatSink>(plain_ss.rdbuf());
ThreadLogState::log_set_sink(std::move(plain_sink));
{
scope outer("outer");
}
ThreadLogState::log_set_sink(nullptr);
REQUIRE(plain_ss.str() == "+(0) outer\n-(0) outer\n");
}
}
TEST_CASE("scope-indent-flat", "[scope]")
{
scope_config::time_enabled = false;
/* capture scope output into a stringstream via a FlatSink */
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
{
scope outer("outer");
outer.log("hello");
{
scope inner("inner");
inner.log("world", 42);
}
outer.log("bye");
}
ThreadLogState::log_set_sink(nullptr); /* restore default (clog) */
REQUIRE(ss.str() ==
"+(0) outer\n"
" hello\n"
" +(1) inner\n"
" world42\n"
" -(1) inner\n"
" bye\n"
"-(0) outer\n");
}
TEST_CASE("scope-timestamp-format", "[scope]")
{
/* with time_enabled, a banner line is prefixed by a UTC time-of-day
* "HH:MM:SS.uuuuuu " (16 chars). Value is wall-clock (nondeterministic),
* so we check the FORMAT/width only.
*/
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
scope_config::time_enabled = true;
{ scope s("foo"); }
scope_config::time_enabled = false; /* reset global BEFORE asserting */
ThreadLogState::log_set_sink(nullptr);
std::string out = ss.str();
/* "HH:MM:SS.uuuuuu +foo\n..." */
REQUIRE(out.size() >= 20);
auto dig = [&](std::size_t i) { return std::isdigit((unsigned char)out[i]) != 0; };
REQUIRE(dig(0)); REQUIRE(dig(1)); REQUIRE(out[2] == ':');
REQUIRE(dig(3)); REQUIRE(dig(4)); REQUIRE(out[5] == ':');
REQUIRE(dig(6)); REQUIRE(dig(7)); REQUIRE(out[8] == '.');
REQUIRE(dig(9)); REQUIRE(dig(10)); REQUIRE(dig(11));
REQUIRE(dig(12)); REQUIRE(dig(13)); REQUIRE(dig(14));
REQUIRE(out[15] == ' ');
REQUIRE(out.substr(16, 8) == "+(0) foo");
}
TEST_CASE("scope-location-inline", "[scope]")
{
/* FlatSink reports no column, so the code location falls back to inline
* placement: one space, then "[<basename>:<line>]". (basename:line
* layout is deterministic; the line value itself is not asserted.)
*/
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
scope_config::location_enabled = true;
{ XO_SCOPE_(s, always); } /* 'always' so it logs regardless of min_log_level */
scope_config::location_enabled = false; /* reset global BEFORE asserting */
ThreadLogState::log_set_sink(nullptr);
std::string out = ss.str();
auto b = out.find("[scope.test.cpp:");
REQUIRE(b != std::string::npos);
REQUIRE(b >= 1);
REQUIRE(out[b - 1] == ' '); /* inline: single space precedes '[' */
}
TEST_CASE("scope-retroactively-enable", "[scope]") {
/* a scope created below the log threshold stays silent, but
* retroactively_enable() turns it on and emits the deferred banner.
*/
bool saved_time = scope_config::time_enabled;
scope_config::time_enabled = false; /* deterministic output */
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
std::string before;
{
/* min_log_level defaults to error; 'chatty' < error => disabled */
scope log(XO_ENTER0_(chatty), "ctor-args");
before = ss.str();
log.retroactively_enable("late", 42);
}
scope_config::time_enabled = saved_time; /* reset BEFORE asserting */
ThreadLogState::log_set_sink(nullptr);
std::string out = ss.str();
REQUIRE(before.empty()); /* disabled: nothing logged */
REQUIRE(out.find("late42") != std::string::npos); /* retro banner emitted */
REQUIRE(out.find("ctor-args") == std::string::npos); /* original ctor args discarded */
}
TEST_CASE("scope-xo-debug-gating", "[scope]") {
/* XO_DEBUG_(flag) enables logging (at 'always') iff flag is true. */
bool saved_time = scope_config::time_enabled;
scope_config::time_enabled = false; /* deterministic output */
auto run = [](bool flag) {
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
{
scope log(XO_DEBUG_(flag));
log && log("banner", flag);
}
ThreadLogState::log_set_sink(nullptr);
return ss.str();
};
std::string on = run(true);
std::string off = run(false);
scope_config::time_enabled = saved_time; /* reset BEFORE asserting */
REQUIRE(on.find("banner") != std::string::npos); /* flag=true => enabled */
REQUIRE(off.empty()); /* flag=false => never => silent */
}
/* XO_LITERAL_ banners from two runtime names, mirroring legacy XO_LITERAL:
* name1 is styled/colored, name2 appended verbatim with no separator (so
* the caller supplies its own "::"), e.g. self_type + "::ctor".
*/
TEST_CASE("scope-literal-two-names", "[scope]")
{
bool saved_time = scope_config::time_enabled;
scope_config::time_enabled = false;
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
{
scope lscope(XO_LITERAL_(always, "Refcounted", "::ctor"));
}
ThreadLogState::log_set_sink(nullptr);
scope_config::time_enabled = saved_time; /* reset BEFORE asserting */
REQUIRE(ss.str() ==
"+(0) Refcounted::ctor\n"
"-(0) Refcounted::ctor\n");
}
/* an omitted second name must change nothing: every existing call site
* builds a scope_setup without one.
*/
TEST_CASE("scope-literal-one-name-unchanged", "[scope]")
{
bool saved_time = scope_config::time_enabled;
scope_config::time_enabled = false;
stringstream ss;
auto sink = std::make_unique<FlatSink>(ss.rdbuf());
ThreadLogState::log_set_sink(std::move(sink));
{
scope lscope(XO_ENTER2_(always, true, "Refcounted"));
}
ThreadLogState::log_set_sink(nullptr);
scope_config::time_enabled = saved_time;
REQUIRE(ss.str() ==
"+(0) Refcounted\n"
"-(0) Refcounted\n");
}
} /*namespace ut*/
/* end scope.test.cpp */