#include #include #include #include #include "test_rusage.hpp" #include #include #include #include #include #include #include #include #if defined(_WIN32) #include #else #include #endif #if defined(_MSC_VER) #include #include #include #include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #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 normalizeArgs(int argc, char* argv[]) { std::vector args; args.reserve(static_cast(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(cpu / 1000ULL), static_cast(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 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(::_getpid()); #else const auto pid = static_cast(::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 normalizedArgv; normalizedArgv.reserve(args.size()); for (auto& arg : args) { normalizedArgv.push_back(arg.data()); } return Catch::Session().run(static_cast(normalizedArgv.size()), normalizedArgv.data()); }