// Copyright (c) 2025 YAMS Contributors // SPDX-License-Identifier: GPL-3.0-or-later // // Unit tests for CheckpointManager component (PBI-090) // // Catch2 migration from GTest (yams-3s4 / yams-zns) #include #include #include #include #include #include #include #include using namespace yams::daemon; using namespace std::chrono_literals; namespace { struct WalCheckpointRecorder { yams::Result checkpointWal() { ++passiveCalls; return {}; } yams::Result checkpointWalTruncate() { ++truncateCalls; return {}; } int passiveCalls{0}; int truncateCalls{0}; }; void bindWalCallbacks(CheckpointManager::Dependencies& deps, const std::shared_ptr& recorder) { deps.checkpointWal = [recorder]() { return recorder->checkpointWal(); }; deps.checkpointWalTruncate = [recorder]() { return recorder->checkpointWalTruncate(); }; } void createSparseFile(const std::filesystem::path& path, std::uint64_t sizeBytes) { std::ofstream out(path, std::ios::binary | std::ios::trunc); REQUIRE(out.good()); out.seekp(static_cast(sizeBytes - 1)); const char byte = '\0'; out.write(&byte, 1); REQUIRE(out.good()); } struct CheckpointManagerFixture { std::shared_ptr io; std::unique_ptr> workGuard; std::thread ioThread; std::shared_ptr> stopRequested; std::filesystem::path tempDir; CheckpointManagerFixture() { io = std::make_shared(); workGuard = std::make_unique< boost::asio::executor_work_guard>( io->get_executor()); ioThread = std::thread([this]() { io->run(); }); stopRequested = std::make_shared>(false); tempDir = std::filesystem::temp_directory_path() / ("yams_checkpoint_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(tempDir); } ~CheckpointManagerFixture() { stopRequested->store(true); workGuard.reset(); io->stop(); if (ioThread.joinable()) { ioThread.join(); } std::error_code ec; std::filesystem::remove_all(tempDir, ec); } CheckpointManager::Config makeConfig(std::chrono::seconds interval = 1s) { CheckpointManager::Config cfg; cfg.checkpoint_interval = interval; cfg.vector_index_insert_threshold = 100; cfg.enable_hotzone_persistence = false; cfg.data_dir = tempDir; return cfg; } CheckpointManager::Dependencies makeDeps() { CheckpointManager::Dependencies deps; deps.vectorSystemManager = nullptr; deps.hotzoneManager = nullptr; deps.executor = io->get_executor(); deps.stopRequested = stopRequested; return deps; } }; } // namespace TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager construction and state", "[daemon][components][checkpoint][catch2]") { auto cfg = makeConfig(); auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); SECTION("initial state is not running") { CHECK_FALSE(mgr.isRunning()); } SECTION("initial counters are zero") { CHECK((mgr.vectorCheckpointCount() == 0)); CHECK((mgr.hotzoneCheckpointCount() == 0)); CHECK((mgr.checkpointErrorCount() == 0)); } SECTION("stats are initially zero") { CHECK((mgr.lastVectorCheckpointEpoch() == 0)); CHECK((mgr.lastHotzoneCheckpointEpoch() == 0)); } } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager start/stop lifecycle", "[daemon][components][checkpoint][catch2]") { auto cfg = makeConfig(60s); auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); SECTION("start and stop work correctly") { CHECK_FALSE(mgr.isRunning()); mgr.start(); std::this_thread::sleep_for(50ms); CHECK(mgr.isRunning()); mgr.stop(); std::this_thread::sleep_for(50ms); CHECK_FALSE(mgr.isRunning()); } SECTION("start is idempotent") { mgr.start(); mgr.start(); mgr.start(); std::this_thread::sleep_for(50ms); CHECK(mgr.isRunning()); mgr.stop(); } SECTION("stop is idempotent") { mgr.start(); std::this_thread::sleep_for(50ms); mgr.stop(); mgr.stop(); mgr.stop(); CHECK_FALSE(mgr.isRunning()); } } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager manual checkpoint", "[daemon][components][checkpoint][catch2]") { SECTION("checkpoint with no dependencies succeeds") { auto cfg = makeConfig(); auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); bool result = mgr.checkpointNow(); CHECK(result); CHECK((mgr.vectorCheckpointCount() == 0)); CHECK((mgr.hotzoneCheckpointCount() == 0)); CHECK((mgr.checkpointErrorCount() == 0)); } SECTION("checkpoint with hotzone enabled succeeds") { auto cfg = makeConfig(); cfg.enable_hotzone_persistence = true; auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); bool result = mgr.checkpointNow(); CHECK(result); } SECTION("multiple manual checkpoints work") { auto cfg = makeConfig(); auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); for (int i = 0; i < 5; ++i) { CHECK(mgr.checkpointNow()); } CHECK((mgr.checkpointErrorCount() == 0)); } } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager missing WAL callbacks are a no-op", "[daemon][components][checkpoint][catch2]") { constexpr std::uint64_t kWatermarkBytes = 256ULL * 1024ULL * 1024ULL; createSparseFile(tempDir / "yams.db-wal", kWatermarkBytes + 1); auto cfg = makeConfig(); auto deps = makeDeps(); CheckpointManager mgr(cfg, deps); CHECK(mgr.checkpointNow()); CHECK((mgr.checkpointErrorCount() == 0)); } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager routine checkpoint is passive", "[daemon][components][checkpoint][catch2]") { auto recorder = std::make_shared(); auto cfg = makeConfig(); auto deps = makeDeps(); bindWalCallbacks(deps, recorder); CheckpointManager mgr(cfg, deps); CHECK(mgr.checkpointNow()); CHECK((recorder->passiveCalls == 1)); CHECK((recorder->truncateCalls == 0)); } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager watermark falls back to bound passive callback", "[daemon][components][checkpoint][catch2]") { auto recorder = std::make_shared(); constexpr std::uint64_t kWatermarkBytes = 256ULL * 1024ULL * 1024ULL; createSparseFile(tempDir / "yams.db-wal", kWatermarkBytes + 1); auto cfg = makeConfig(); auto deps = makeDeps(); deps.checkpointWal = [recorder]() { return recorder->checkpointWal(); }; CheckpointManager mgr(cfg, deps); CHECK(mgr.checkpointNow()); CHECK((recorder->passiveCalls == 1)); CHECK((recorder->truncateCalls == 0)); } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager watermark checkpoint truncates WAL", "[daemon][components][checkpoint][catch2]") { auto recorder = std::make_shared(); constexpr std::uint64_t kWatermarkBytes = 256ULL * 1024ULL * 1024ULL; createSparseFile(tempDir / "yams.db-wal", kWatermarkBytes + 1); auto cfg = makeConfig(); auto deps = makeDeps(); bindWalCallbacks(deps, recorder); CheckpointManager mgr(cfg, deps); CHECK(mgr.checkpointNow()); CHECK((recorder->passiveCalls == 0)); CHECK((recorder->truncateCalls == 1)); } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager shutdown falls back to bound passive callback", "[daemon][components][checkpoint][catch2]") { auto recorder = std::make_shared(); auto cfg = makeConfig(); auto deps = makeDeps(); deps.checkpointWal = [recorder]() { return recorder->checkpointWal(); }; CheckpointManager mgr(cfg, deps); mgr.start(); std::this_thread::sleep_for(50ms); mgr.stop(); CHECK((recorder->passiveCalls == 1)); CHECK((recorder->truncateCalls == 0)); } TEST_CASE_METHOD(CheckpointManagerFixture, "CheckpointManager shutdown truncates WAL", "[daemon][components][checkpoint][catch2]") { auto recorder = std::make_shared(); auto cfg = makeConfig(); auto deps = makeDeps(); bindWalCallbacks(deps, recorder); CheckpointManager mgr(cfg, deps); mgr.start(); std::this_thread::sleep_for(50ms); mgr.stop(); CHECK((recorder->passiveCalls == 0)); CHECK((recorder->truncateCalls == 1)); } TEST_CASE("CheckpointManager config defaults", "[daemon][components][checkpoint][catch2]") { CheckpointManager::Config cfg; CHECK((cfg.checkpoint_interval.count() == 300)); CHECK((cfg.vector_index_insert_threshold == 1000)); CHECK_FALSE(cfg.enable_hotzone_persistence); }