#include "PersistenceWriter.h" #include #include #include #include #include #include #include namespace { int gFailures = 0; void Expect(bool condition, const char* message) { if (condition) return; std::cerr << "FAIL: " << message << "\n"; ++gFailures; } PersistenceSnapshot MakeRuntimeSnapshot(const std::string& contents) { PersistenceSnapshot snapshot; snapshot.targetKind = PersistenceTargetKind::RuntimeState; snapshot.targetPath = std::filesystem::temp_directory_path() / "video-shader-persistence-writer-test.json"; snapshot.contents = contents; snapshot.reason = "test"; snapshot.debounceKey = "runtime-state"; snapshot.debounceAllowed = true; return snapshot; } void TestDebouncedRequestsCoalesceToNewestSnapshot() { std::mutex mutex; std::vector writtenSnapshots; PersistenceWriter writer( std::chrono::milliseconds(1000), [&](const PersistenceSnapshot& snapshot, std::string&) { std::lock_guard lock(mutex); writtenSnapshots.push_back(snapshot); return true; }); std::string error; Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("first"), error), "first debounced snapshot enqueues"); Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("second"), error), "second debounced snapshot enqueues"); PersistenceWriterMetrics metrics = writer.GetMetrics(); Expect(metrics.pendingCount == 1, "debounced snapshots share one pending slot"); Expect(metrics.enqueuedCount == 1, "first debounced snapshot counts as enqueue"); Expect(metrics.coalescedCount == 1, "second debounced snapshot counts as coalesced"); writer.StopAndFlush(); { std::lock_guard lock(mutex); Expect(writtenSnapshots.size() == 1, "flush writes one coalesced snapshot"); Expect(!writtenSnapshots.empty() && writtenSnapshots[0].contents == "second", "coalesced writer keeps newest snapshot"); } metrics = writer.GetMetrics(); Expect(metrics.pendingCount == 0, "flush drains pending debounced snapshot"); Expect(metrics.writtenCount == 1, "flush records one successful write"); } void TestImmediateRequestsAreNotCoalesced() { std::mutex mutex; std::vector writtenSnapshots; PersistenceWriter writer( std::chrono::milliseconds(1000), [&](const PersistenceSnapshot& snapshot, std::string&) { std::lock_guard lock(mutex); writtenSnapshots.push_back(snapshot); return true; }); PersistenceSnapshot first = MakeRuntimeSnapshot("first"); first.debounceAllowed = false; PersistenceSnapshot second = MakeRuntimeSnapshot("second"); second.debounceAllowed = false; std::string error; Expect(writer.EnqueueSnapshot(first, error), "first immediate snapshot enqueues"); Expect(writer.EnqueueSnapshot(second, error), "second immediate snapshot enqueues"); writer.StopAndFlush(); { std::lock_guard lock(mutex); Expect(writtenSnapshots.size() == 2, "immediate snapshots are written independently"); Expect(writtenSnapshots.size() == 2 && writtenSnapshots[0].contents == "first" && writtenSnapshots[1].contents == "second", "immediate snapshots preserve order"); } } void TestWriteFailureReportsStructuredResult() { std::vector results; PersistenceWriter writer( std::chrono::milliseconds(1), [](const PersistenceSnapshot&, std::string& error) { error = "simulated failure"; return false; }); writer.SetResultCallback([&results](const PersistenceWriteResult& result) { results.push_back(result); }); PersistenceSnapshot snapshot = MakeRuntimeSnapshot("payload"); snapshot.debounceAllowed = false; snapshot.reason = "failure-test"; std::string error; Expect(writer.EnqueueSnapshot(snapshot, error), "failing snapshot still enqueues"); writer.StopAndFlush(); Expect(results.size() == 1, "writer reports one failure result"); Expect(!results.empty() && !results[0].succeeded, "writer result records failure"); Expect(!results.empty() && results[0].reason == "failure-test", "writer result preserves reason"); Expect(!results.empty() && results[0].errorMessage == "simulated failure", "writer result preserves error message"); Expect(!results.empty() && !results[0].newerRequestPending, "writer result reports no newer pending request"); Expect(writer.GetMetrics().failedCount == 1, "writer metrics count failed writes"); } void TestShutdownFlushDrainsPendingSnapshotAndRejectsNewRequests() { std::mutex mutex; std::vector writtenSnapshots; PersistenceWriter writer( std::chrono::milliseconds(1000), [&](const PersistenceSnapshot& snapshot, std::string&) { std::lock_guard lock(mutex); writtenSnapshots.push_back(snapshot); return true; }); std::string error; Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("pending"), error), "pending snapshot enqueues before shutdown"); Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "bounded shutdown flush completes"); { std::lock_guard lock(mutex); Expect(writtenSnapshots.size() == 1, "shutdown flush writes pending debounced snapshot"); Expect(!writtenSnapshots.empty() && writtenSnapshots[0].contents == "pending", "shutdown flush preserves pending snapshot contents"); } Expect(!writer.EnqueueSnapshot(MakeRuntimeSnapshot("late"), error), "writer rejects requests after shutdown flush"); } void TestShutdownFlushTimeoutCanBeRetried() { std::mutex mutex; std::condition_variable condition; bool sinkStarted = false; bool releaseSink = false; PersistenceWriter writer( std::chrono::milliseconds(1), [&](const PersistenceSnapshot&, std::string&) { std::unique_lock lock(mutex); sinkStarted = true; condition.notify_all(); condition.wait(lock, [&]() { return releaseSink; }); return true; }); PersistenceSnapshot snapshot = MakeRuntimeSnapshot("slow"); snapshot.debounceAllowed = false; std::string error; Expect(writer.EnqueueSnapshot(snapshot, error), "slow snapshot enqueues"); { std::unique_lock lock(mutex); Expect(condition.wait_for(lock, std::chrono::seconds(1), [&]() { return sinkStarted; }), "slow sink starts before timeout test"); } Expect(!writer.StopAndFlush(std::chrono::milliseconds(10), error), "bounded shutdown flush reports timeout"); Expect(error.find("Timed out") != std::string::npos, "shutdown timeout returns a useful error"); { std::lock_guard lock(mutex); releaseSink = true; } condition.notify_all(); error.clear(); Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "shutdown flush can complete after earlier timeout"); } } int main() { TestDebouncedRequestsCoalesceToNewestSnapshot(); TestImmediateRequestsAreNotCoalesced(); TestWriteFailureReportsStructuredResult(); TestShutdownFlushDrainsPendingSnapshotAndRejectsNewRequests(); TestShutdownFlushTimeoutCanBeRetried(); if (gFailures != 0) { std::cerr << gFailures << " persistence writer test(s) failed.\n"; return 1; } std::cout << "Persistence writer tests passed.\n"; return 0; }