yams/tests/support/catch2_main.cpp
Trevon 837bc641b3
test(infra): give every Catch2 process a private XDG_STATE_HOME (#296)
SessionService reads the current session from XDG_STATE_HOME. On a build
host whose user had an auto-created session ("auto: /data") with watch
enabled, every test that booted a ServiceManager inherited it, the session
watcher walked /data during init, and six daemon tests timed out. With the
state dir isolated the same six pass.

The shared test main now points XDG_STATE_HOME at a per-process temp dir
when the caller did not set one, and removes it at exit. A small test checks
the process sees no host session.

Signed-off-by: trvon <me@trevon.dev>
2026-10-14 08:32:56 -06:00

202 lines
6.4 KiB
C++

#include <catch2/catch_session.hpp>
#include <catch2/catch_test_case_info.hpp>
#include <catch2/reporters/catch_reporter_event_listener.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
#include "test_rusage.hpp"
#include <algorithm>
#include <filesystem>
#include <system_error>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
#if defined(_WIN32)
#include <process.h>
#else
#include <unistd.h>
#endif
#if defined(_MSC_VER)
#include <crtdbg.h>
#include <cstdio>
#include <cstdlib>
#include <stdlib.h>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace {
#if defined(_MSC_VER)
// A Debug CRT assertion (debug-iterator check, _ASSERTE, invalid parameter) or abort() opens a
// modal dialog by default. In a non-interactive test run nobody can dismiss it, so the case hangs
// until the harness timeout with no output. Route every such report to stderr and suppress the
// Windows Error Reporting dialogs so the case fails fast with the message instead.
void reportCrtFailuresToStderr() {
::SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
_set_error_mode(_OUT_TO_STDERR);
_set_abort_behavior(0, _CALL_REPORTFAULT);
for (int reportType : {_CRT_WARN, _CRT_ERROR, _CRT_ASSERT}) {
_CrtSetReportMode(reportType, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
_CrtSetReportFile(reportType, _CRTDBG_FILE_STDERR);
}
std::setvbuf(stderr, nullptr, _IONBF, 0);
}
#endif
bool isOption(std::string_view arg) {
return !arg.empty() && arg.front() == '-';
}
std::vector<std::string> normalizeArgs(int argc, char* argv[]) {
std::vector<std::string> args;
args.reserve(static_cast<std::size_t>(argc));
bool hasOption = false;
for (int i = 1; i < argc; ++i) {
if (argv[i] && isOption(argv[i])) {
hasOption = true;
break;
}
}
if (argc <= 2 || hasOption) {
for (int i = 0; i < argc; ++i) {
args.emplace_back(argv[i] ? argv[i] : "");
}
return args;
}
// Meson --test-args splits a test name like "Foo bar" into multiple argv entries.
// Keep Catch2's parser on the single test-spec path; Catch2 3.12 + libc++ ASAN can
// otherwise trip a container-overflow while collecting multiple positional specs.
args.emplace_back(argv[0] ? argv[0] : "");
std::string testSpec;
for (int i = 1; i < argc; ++i) {
if (!testSpec.empty()) {
testSpec.push_back(' ');
}
testSpec += argv[i] ? argv[i] : "";
}
args.push_back(std::move(testSpec));
return args;
}
// Per-test-case CPU and run-peak RSS accounting for every Catch2 binary. Lives in this TU so the
// static-library linker cannot drop the registrar. Summary goes to stderr; YAMS_TEST_RUSAGE=1 adds
// one line per case (test-only knob).
class ResourceAccountingListener final : public Catch::EventListenerBase {
public:
using Catch::EventListenerBase::EventListenerBase;
void testRunStarting(const Catch::TestRunInfo&) override {
const char* env = std::getenv("YAMS_TEST_RUSAGE");
perCase_ = env && env[0] == '1';
cases_.clear();
}
void testCaseStarting(const Catch::TestCaseInfo&) override {
start_ = yams::test::rusage::sampleSelf();
}
void testCaseEnded(const Catch::TestCaseStats& stats) override {
const auto end = yams::test::rusage::sampleSelf();
const auto cpu = yams::test::rusage::cpuDeltaMicros(start_, end);
cases_.push_back({stats.testInfo->name, cpu});
if (perCase_) {
std::fprintf(stderr, "[rusage] case: %s cpu=%llums peak_rss=%lluMB\n",
yams::test::rusage::clampName(stats.testInfo->name).c_str(),
static_cast<unsigned long long>(cpu / 1000ULL),
static_cast<unsigned long long>(end.peakRssBytes / (1024ULL * 1024ULL)));
}
}
void testRunEnded(const Catch::TestRunStats&) override {
if (cases_.empty()) {
return;
}
const auto top = yams::test::rusage::topByCpu(cases_, 5);
std::fprintf(
stderr, "%s\n%s\n", yams::test::rusage::formatTopCpu(top).c_str(),
yams::test::rusage::formatPeakRss(yams::test::rusage::sampleSelf().peakRssBytes)
.c_str());
}
private:
bool perCase_{false};
yams::test::rusage::Sample start_{};
std::vector<yams::test::rusage::CaseCost> cases_;
};
CATCH_REGISTER_LISTENER(ResourceAccountingListener)
} // namespace
// Every test process gets its own session-state directory. SessionService reads the current
// session (and its watch directories) from XDG_STATE_HOME, so without this a developer's or
// build host's live session leaks into any test that boots a ServiceManager: the session watcher
// then walks the pinned trees during init and the test times out. A caller that already set
// XDG_STATE_HOME keeps it.
class IsolatedStateHome {
public:
IsolatedStateHome() {
if (const char* existing = std::getenv("XDG_STATE_HOME"); existing && existing[0] != '\0') {
return;
}
std::error_code ec;
auto base = std::filesystem::temp_directory_path(ec);
if (ec) {
return;
}
#if defined(_WIN32)
const auto pid = static_cast<unsigned long long>(::_getpid());
#else
const auto pid = static_cast<unsigned long long>(::getpid());
#endif
dir_ = base / ("yams-test-state-" + std::to_string(pid));
std::filesystem::create_directories(dir_, ec);
if (ec) {
dir_.clear();
return;
}
#if defined(_WIN32)
_putenv_s("XDG_STATE_HOME", dir_.string().c_str());
#else
::setenv("XDG_STATE_HOME", dir_.c_str(), 1);
#endif
}
~IsolatedStateHome() {
if (!dir_.empty()) {
std::error_code ec;
std::filesystem::remove_all(dir_, ec);
}
}
private:
std::filesystem::path dir_;
};
int main(int argc, char* argv[]) {
#if defined(_MSC_VER)
reportCrtFailuresToStderr();
#endif
IsolatedStateHome isolatedState;
auto args = normalizeArgs(argc, argv);
std::vector<char*> normalizedArgv;
normalizedArgv.reserve(args.size());
for (auto& arg : args) {
normalizedArgv.push_back(arg.data());
}
return Catch::Session().run(static_cast<int>(normalizedArgv.size()), normalizedArgv.data());
}