247 lines
8.7 KiB
C++
247 lines
8.7 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "fgc/Logger.h"
|
|
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
// Run a callable with std::cout redirected to a buffer; return what was written.
|
|
template <typename F>
|
|
std::string captureCout(F&& f) {
|
|
std::ostringstream oss;
|
|
std::streambuf* old = std::cout.rdbuf(oss.rdbuf());
|
|
f();
|
|
std::cout.rdbuf(old);
|
|
return oss.str();
|
|
}
|
|
|
|
unsigned bits(LogCat c) { return static_cast<unsigned>(c); }
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("parseCategories maps names, all/none, and flags unknown tokens") {
|
|
bool ok = true;
|
|
|
|
CHECK(Logger::parseCategories("serial,mqtt", &ok) == (bits(LogCat::Serial) | bits(LogCat::Mqtt)));
|
|
CHECK(ok);
|
|
|
|
CHECK(Logger::parseCategories("all", &ok) == bits(LogCat::All));
|
|
CHECK(ok);
|
|
|
|
CHECK(Logger::parseCategories("none", &ok) == 0u);
|
|
CHECK(ok);
|
|
CHECK(Logger::parseCategories("", &ok) == 0u);
|
|
CHECK(ok);
|
|
|
|
// Case- and whitespace-insensitive.
|
|
CHECK(Logger::parseCategories(" SeRiAl , Camera ", &ok)
|
|
== (bits(LogCat::Serial) | bits(LogCat::Camera)));
|
|
CHECK(ok);
|
|
|
|
// Unknown token: ok=false, but recognized tokens still apply.
|
|
ok = true;
|
|
CHECK(Logger::parseCategories("serial,bogus", &ok) == bits(LogCat::Serial));
|
|
CHECK_FALSE(ok);
|
|
}
|
|
|
|
TEST_CASE("catFromString round-trips and flags unknown") {
|
|
bool ok = true;
|
|
CHECK(Logger::catFromString("control", &ok) == LogCat::Control);
|
|
CHECK(ok);
|
|
CHECK(Logger::catFromString("nope", &ok) == LogCat::None);
|
|
CHECK_FALSE(ok);
|
|
}
|
|
|
|
TEST_CASE("category enable/disable/set round-trip") {
|
|
Logger::setCategories(0);
|
|
CHECK(Logger::categories() == 0u);
|
|
CHECK_FALSE(Logger::categoryEnabled(LogCat::Serial));
|
|
|
|
Logger::enableCategory(LogCat::Serial);
|
|
Logger::enableCategory(LogCat::Mqtt);
|
|
CHECK(Logger::categoryEnabled(LogCat::Serial));
|
|
CHECK(Logger::categoryEnabled(LogCat::Mqtt));
|
|
CHECK_FALSE(Logger::categoryEnabled(LogCat::Camera));
|
|
|
|
Logger::disableCategory(LogCat::Serial);
|
|
CHECK_FALSE(Logger::categoryEnabled(LogCat::Serial));
|
|
CHECK(Logger::categoryEnabled(LogCat::Mqtt));
|
|
|
|
Logger::setCategories(bits(LogCat::All));
|
|
CHECK(Logger::categoryEnabled(LogCat::Camera));
|
|
CHECK(Logger::categoryEnabled(LogCat::Control));
|
|
|
|
Logger::setCategories(0); // leave global state clean for other tests
|
|
}
|
|
|
|
TEST_CASE("LOG_TRACE_CAT gating is independent of the linear level") {
|
|
Logger::setCategories(0);
|
|
|
|
SUBCASE("suppressed when category off (even at trace level)") {
|
|
Logger::setLevel(LogLevel::Trace);
|
|
std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Serial) << "TX kd180"; });
|
|
CHECK(out.empty());
|
|
}
|
|
|
|
SUBCASE("emitted with [SERIAL] tag when category on, at default info level") {
|
|
Logger::setLevel(LogLevel::Info); // NOT trace
|
|
Logger::enableCategory(LogCat::Serial);
|
|
std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Serial) << "TX kd180"; });
|
|
CHECK(out.find("[SERIAL]") != std::string::npos);
|
|
CHECK(out.find("TX kd180") != std::string::npos);
|
|
}
|
|
|
|
SUBCASE("a different category stays silent") {
|
|
Logger::setLevel(LogLevel::Info);
|
|
Logger::enableCategory(LogCat::Serial); // serial on, mqtt off
|
|
std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Mqtt) << "PUB t p"; });
|
|
CHECK(out.empty());
|
|
}
|
|
|
|
SUBCASE("level Off silences categories too") {
|
|
Logger::setLevel(LogLevel::Off);
|
|
Logger::enableCategory(LogCat::Serial);
|
|
std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Serial) << "TX x"; });
|
|
CHECK(out.empty());
|
|
}
|
|
|
|
// Restore defaults so test ordering can't leak state.
|
|
Logger::setCategories(0);
|
|
Logger::setLevel(LogLevel::Info);
|
|
}
|
|
|
|
// --- the file sink: a second, independent destination ----------------------
|
|
|
|
namespace {
|
|
// Logger state is process-global and doctest runs every case in one process, so
|
|
// a case that leaks a file sink makes LATER cases write into a deleted temp dir.
|
|
// Restoring at the end of the body is not enough: a failed REQUIRE skips it.
|
|
struct LoggerStateGuard {
|
|
fgc::LogLevel console = fgc::Logger::level();
|
|
fgc::LogLevel file = fgc::Logger::fileLevel();
|
|
unsigned cats = fgc::Logger::categories();
|
|
~LoggerStateGuard() {
|
|
fgc::Logger::setFileSink({});
|
|
fgc::Logger::setLevel(console);
|
|
fgc::Logger::setFileLevel(file);
|
|
fgc::Logger::setCategories(cats);
|
|
}
|
|
};
|
|
|
|
// Collects what the file sink is handed.
|
|
struct Captured {
|
|
std::vector<std::string> lines;
|
|
std::vector<fgc::LogCat> cats;
|
|
bool has(const std::string& needle) const {
|
|
for (const auto& l : lines)
|
|
if (l.find(needle) != std::string::npos) return true;
|
|
return false;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE("the file is more verbose than the console, independently") {
|
|
LoggerStateGuard guard;
|
|
Captured cap;
|
|
Logger::setFileSink([&](LogLevel, LogCat c, const std::string& l) {
|
|
cap.lines.push_back(l);
|
|
cap.cats.push_back(c);
|
|
});
|
|
Logger::setLevel(LogLevel::Info); // console
|
|
Logger::setFileLevel(LogLevel::Debug); // file
|
|
|
|
// The macros must gate on the MORE verbose of the two, or the debug line
|
|
// would never be built at all.
|
|
CHECK(Logger::effectiveLevel() == LogLevel::Debug);
|
|
CHECK(Logger::enabled(LogLevel::Debug));
|
|
|
|
const std::string out = captureCout([] {
|
|
LOG_DEBUG << "detail-for-file";
|
|
LOG_INFO << "for-both";
|
|
});
|
|
|
|
CHECK(cap.has("detail-for-file")); // file got it
|
|
CHECK(out.find("detail-for-file") == std::string::npos); // console did not
|
|
CHECK(cap.has("for-both"));
|
|
CHECK(out.find("for-both") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("a wire trace reaches the file even though its level is below the threshold") {
|
|
// The regression this design is most likely to introduce. A category line
|
|
// carries level == Trace; filtering it against file_level (debug) would drop
|
|
// every wire trace silently, and `--trace serial` would write nothing.
|
|
LoggerStateGuard guard;
|
|
Captured cap;
|
|
Logger::setFileSink([&](LogLevel, LogCat c, const std::string& l) {
|
|
cap.lines.push_back(l);
|
|
cap.cats.push_back(c);
|
|
});
|
|
Logger::setLevel(LogLevel::Info);
|
|
Logger::setFileLevel(LogLevel::Debug);
|
|
|
|
SUBCASE("category off: nowhere") {
|
|
Logger::setCategories(0);
|
|
const std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Serial) << "TX ping"; });
|
|
CHECK_FALSE(cap.has("TX ping"));
|
|
CHECK(out.find("TX ping") == std::string::npos);
|
|
}
|
|
SUBCASE("category on: both, and the file is told which category") {
|
|
Logger::setCategories(static_cast<unsigned>(LogCat::Serial));
|
|
const std::string out = captureCout([] { LOG_TRACE_CAT(LogCat::Serial) << "TX ping"; });
|
|
CHECK(cap.has("TX ping"));
|
|
CHECK(cap.has("[SERIAL]"));
|
|
CHECK(out.find("TX ping") != std::string::npos);
|
|
REQUIRE_FALSE(cap.cats.empty());
|
|
CHECK(cap.cats.front() == LogCat::Serial);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("the console can be silenced without silencing the file") {
|
|
// `--log-level off` now means the CONSOLE is off. A tower quietened on
|
|
// screen must still be writing its record.
|
|
LoggerStateGuard guard;
|
|
Captured cap;
|
|
Logger::setFileSink([&](LogLevel, LogCat, const std::string& l) { cap.lines.push_back(l); });
|
|
Logger::setLevel(LogLevel::Off);
|
|
Logger::setFileLevel(LogLevel::Info);
|
|
|
|
const std::string out = captureCout([] { LOG_ERROR << "still-recorded"; });
|
|
CHECK(cap.has("still-recorded"));
|
|
CHECK(out.find("still-recorded") == std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("with no file sink the logger behaves exactly as before") {
|
|
LoggerStateGuard guard;
|
|
Logger::setFileSink({});
|
|
Logger::setFileLevel(LogLevel::Off);
|
|
Logger::setLevel(LogLevel::Info);
|
|
|
|
// Off is the identity for the min(), so the gate is bit-identical to the
|
|
// pre-change behaviour.
|
|
CHECK(Logger::effectiveLevel() == LogLevel::Info);
|
|
CHECK_FALSE(Logger::enabled(LogLevel::Debug));
|
|
CHECK(Logger::enabled(LogLevel::Info));
|
|
|
|
const std::string out = captureCout([] { LOG_INFO << "unchanged"; });
|
|
CHECK(out.find("unchanged") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("setFileLevelFromString accepts the same words as the console one") {
|
|
LoggerStateGuard guard;
|
|
CHECK(Logger::setFileLevelFromString("debug"));
|
|
CHECK(Logger::fileLevel() == LogLevel::Debug);
|
|
CHECK(Logger::setFileLevelFromString("WARNING"));
|
|
CHECK(Logger::fileLevel() == LogLevel::Warn);
|
|
CHECK(Logger::setFileLevelFromString("off"));
|
|
CHECK(Logger::fileLevel() == LogLevel::Off);
|
|
// A bad word leaves it untouched rather than guessing.
|
|
CHECK_FALSE(Logger::setFileLevelFromString("loud"));
|
|
CHECK(Logger::fileLevel() == LogLevel::Off);
|
|
}
|