diff --git a/CMakeLists.txt b/CMakeLists.txt
index a9559569..6ee94950 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -870,6 +870,10 @@ if (BUILD_TESTING)
tests/test_parsedmessage.cpp
tests/test_networkengine.h
tests/test_networkengine.cpp
+ tests/test_opcache.h
+ tests/test_opcache.cpp
+ tests/test_valuecache.h
+ tests/test_valuecache.cpp
)
if (OPENDHT_PROXY_CLIENT)
list (APPEND test_FILES
diff --git a/tests/test_opcache.cpp b/tests/test_opcache.cpp
new file mode 100644
index 00000000..d640c903
--- /dev/null
+++ b/tests/test_opcache.cpp
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2025 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "test_opcache.h"
+
+// Hack to test internal class
+#include "../src/op_cache.cpp"
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(OpCacheTester);
+
+void
+OpCacheTester::setUp()
+{}
+
+void
+OpCacheTester::tearDown()
+{}
+
+void
+OpCacheTester::testUpdateRefCount()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->seq = 1;
+ std::string data1 = "data1";
+ v1->data = {data1.begin(), data1.end()};
+
+ auto v2 = std::make_shared();
+ v2->id = 1;
+ v2->seq = 2;
+ std::string data2 = "data2";
+ v2->data = {data2.begin(), data2.end()};
+
+ // Add v1
+ cache.onValuesAdded({v1});
+ // Should have 1 value
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+ auto stored = cache.get(1);
+ CPPUNIT_ASSERT(stored);
+ CPPUNIT_ASSERT_EQUAL(v1->seq, stored->seq);
+
+ // Add v2 (update)
+ cache.onValuesAdded({v2});
+ // Should still have 1 value (updated)
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+ stored = cache.get(1);
+ CPPUNIT_ASSERT(stored);
+ CPPUNIT_ASSERT_EQUAL(v2->seq, stored->seq);
+
+ // Now expire once — since refCount is not incremented on edit,
+ // a single expire removes the value.
+ cache.onValuesExpired({v1});
+
+ stored = cache.get(1);
+ CPPUNIT_ASSERT_MESSAGE("Value should be removed after single expire (refCount=1)", stored == nullptr);
+}
+
+void
+OpCacheTester::testBasicAddExpire()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 10;
+ v1->data = {'A'};
+
+ cache.onValuesAdded({v1});
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+ CPPUNIT_ASSERT(cache.get(10));
+
+ std::vector ids = {v1->id};
+ cache.onValuesExpired(ids);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+ CPPUNIT_ASSERT(!cache.get(10));
+}
+
+void
+OpCacheTester::testMultipleSources()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 20;
+ v1->data = {'B'};
+
+ // Add same value twice (simulating two sources)
+ // refCount does not increment on duplicate: stays at 1.
+ cache.onValuesAdded({v1});
+ cache.onValuesAdded({v1});
+
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // Single expire removes the value
+ cache.onValuesExpired({v1});
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+OpCacheTester::testUpdateSequence()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 30;
+ v1->seq = 10;
+ v1->data = {'C'};
+
+ auto v2 = std::make_shared();
+ v2->id = 30;
+ v2->seq = 20;
+ v2->data = {'D'};
+
+ auto v3 = std::make_shared();
+ v3->id = 30;
+ v3->seq = 5;
+ v3->data = {'E'};
+
+ // Add seq 10
+ cache.onValuesAdded({v1});
+ auto stored = cache.get(30);
+ CPPUNIT_ASSERT_EQUAL((uint16_t) 10, stored->seq);
+
+ // Add seq 20 (update)
+ cache.onValuesAdded({v2});
+ stored = cache.get(30);
+ CPPUNIT_ASSERT_EQUAL((uint16_t) 20, stored->seq);
+
+ // Add seq 5 (older, should not update content)
+ cache.onValuesAdded({v3});
+ stored = cache.get(30);
+ CPPUNIT_ASSERT_EQUAL((uint16_t) 20, stored->seq);
+
+ // RefCount stays at 1 regardless of how many times we add.
+ // A single expire removes the value.
+ cache.onValuesExpired({v1});
+ CPPUNIT_ASSERT(!cache.get(30));
+}
+
+void
+OpCacheTester::testCallbacks()
+{
+ int addCount = 0;
+ int expireCount = 0;
+ dht::OpValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ return true;
+ });
+
+ auto v1 = std::make_shared();
+ v1->id = 40;
+
+ cache.onValuesAdded({v1});
+ CPPUNIT_ASSERT_EQUAL(1, addCount);
+ CPPUNIT_ASSERT_EQUAL(0, expireCount);
+
+ // Adding same value again should NOT trigger callback (no new value added)
+ // Wait, looking at code:
+ // if (viop.second) { newValues.emplace_back(v); } ...
+ // return newValues.empty() ? true : callback(newValues, false);
+ // So if it's a duplicate or update, does it callback?
+ // If update: newValues.emplace_back(v) IS called.
+ // If duplicate (refCount++ only): newValues is NOT added.
+
+ cache.onValuesAdded({v1});
+ CPPUNIT_ASSERT_EQUAL(1, addCount); // Should still be 1
+
+ auto v2 = std::make_shared();
+ v2->id = 40;
+ v2->seq = 100; // Update
+ v2->data = {'M', 'O', 'D'}; // Change data to ensure inequality
+
+ cache.onValuesAdded({v2});
+ CPPUNIT_ASSERT_EQUAL(2, addCount); // Should be 2 now (update triggers callback)
+
+ // RefCount is 1 — single expire removes the value.
+ cache.onValuesExpired({v1});
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+}
+
+void
+OpCacheTester::testFilters()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 50;
+ v1->type = 1;
+
+ auto v2 = std::make_shared();
+ v2->id = 51;
+ v2->type = 2;
+
+ cache.onValuesAdded({v1, v2});
+
+ auto f1 = dht::Value::TypeFilter(1);
+ auto res1 = cache.get(f1);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, res1.size());
+ CPPUNIT_ASSERT_EQUAL(v1->id, res1[0]->id);
+
+ auto f2 = dht::Value::TypeFilter(2);
+ auto res2 = cache.get(f2);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, res2.size());
+ CPPUNIT_ASSERT_EQUAL(v2->id, res2[0]->id);
+
+ auto all = cache.getValues();
+ CPPUNIT_ASSERT_EQUAL((size_t) 2, all.size());
+}
+
+void
+OpCacheTester::testSyncStatus()
+{
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ // Initial state
+ CPPUNIT_ASSERT(!cache.isSynced());
+
+ // Add a node
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ // 1 node, 0 synced -> not synced
+ CPPUNIT_ASSERT(!cache.isSynced());
+
+ // Sync the node
+ cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
+ // 1 node, 1 synced -> synced
+ CPPUNIT_ASSERT(cache.isSynced());
+
+ // Add another node
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ // 2 nodes, 1 synced -> not synced
+ CPPUNIT_ASSERT(!cache.isSynced());
+
+ // Sync second node
+ cache.onNodeChanged(dht::ListenSyncStatus::SYNCED);
+ // 2 nodes, 2 synced -> synced
+ CPPUNIT_ASSERT(cache.isSynced());
+
+ // Unsync one
+ cache.onNodeChanged(dht::ListenSyncStatus::UNSYNCED);
+ CPPUNIT_ASSERT(!cache.isSynced());
+
+ // Remove one (the unsynced one)
+ cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
+ // 1 node, 1 synced -> synced
+ CPPUNIT_ASSERT(cache.isSynced());
+}
+
+void
+OpCacheTester::testGetWhileSynced()
+{
+ // Test that get() on a SearchCache with a synced listen returns cached values
+ // and completes the get (returns true) without causing duplicate delivery.
+ dht::SearchCache searchCache;
+
+ int getCallCount = 0;
+ bool doneCalled = false;
+ bool doneSuccess = false;
+
+ auto v1 = std::make_shared();
+ v1->id = 100;
+ v1->data = {'X'};
+ auto v2 = std::make_shared();
+ v2->id = 101;
+ v2->data = {'Y'};
+
+ auto query = std::make_shared();
+
+ // Start a listen
+ size_t listenToken = searchCache.listen([](const std::vector>&, bool) { return true; },
+ query,
+ {},
+ [&](const std::shared_ptr& q,
+ dht::ValueCallback vcb,
+ dht::SyncCallback scb) -> size_t {
+ // Simulate node added and synced
+ scb(dht::ListenSyncStatus::ADDED);
+ // Inject values
+ vcb({v1, v2}, false);
+ // Mark as synced
+ scb(dht::ListenSyncStatus::SYNCED);
+ return 1;
+ });
+ CPPUNIT_ASSERT(listenToken != 0);
+
+ // Now perform a get on the synced cache
+ bool result = searchCache.get(
+ {},
+ query,
+ [&](const std::vector>& vals) {
+ getCallCount++;
+ CPPUNIT_ASSERT_EQUAL((size_t) 2, vals.size());
+ return true;
+ },
+ [&](bool success, const std::vector>&) {
+ doneCalled = true;
+ doneSuccess = success;
+ });
+
+ // get should be served from cache
+ CPPUNIT_ASSERT_MESSAGE("get() should return true when cache is synced", result);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("get callback should be called exactly once", 1, getCallCount);
+ CPPUNIT_ASSERT_MESSAGE("done callback should be called", doneCalled);
+ CPPUNIT_ASSERT_MESSAGE("done callback should indicate success", doneSuccess);
+}
+
+void
+OpCacheTester::testGetWhileNotSynced()
+{
+ // Test that when a get() is performed on a non-synced cache, values
+ // are not delivered twice: once from the cache and once from the
+ // simulated network get that Search::get would queue.
+ dht::SearchCache searchCache;
+
+ // Track every value delivery with (value_id, source) pairs
+ std::vector> deliveries;
+
+ auto v1 = std::make_shared();
+ v1->id = 200;
+ v1->data = {'A'};
+
+ auto query = std::make_shared();
+
+ // Start a listen but DON'T mark as synced
+ dht::ValueCallback storedVcb;
+ dht::SyncCallback storedScb;
+ size_t listenToken = searchCache.listen([](const std::vector>&, bool) { return true; },
+ query,
+ {},
+ [&](const std::shared_ptr& q,
+ dht::ValueCallback vcb,
+ dht::SyncCallback scb) -> size_t {
+ storedVcb = vcb;
+ storedScb = scb;
+ // Node added but NOT synced
+ scb(dht::ListenSyncStatus::ADDED);
+ // Inject a value
+ vcb({v1}, false);
+ return 1;
+ });
+ CPPUNIT_ASSERT(listenToken != 0);
+
+ // Simulate what Search::get does: try cache first, if false, queue network get
+ bool doneCalled = false;
+ auto gcb = [&](const std::vector>& vals) {
+ for (const auto& v : vals)
+ deliveries.emplace_back(v->id, "get");
+ return true;
+ };
+ auto dcb = [&](bool, const std::vector>&) {
+ doneCalled = true;
+ };
+
+ bool servedFromCache = searchCache.get({}, query, gcb, dcb);
+
+ // Cache is not synced so get should not be served from cache
+ CPPUNIT_ASSERT_MESSAGE("get() should return false when cache is not synced", !servedFromCache);
+
+ // Simulate the network get completing with the same value
+ // (this is what onGetValuesDone does — calls gcb for each matching get)
+ gcb({v1});
+
+ // Exactly one delivery expected — from the network get only
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should be delivered exactly once (from network get only)",
+ (size_t) 1,
+ deliveries.size());
+ CPPUNIT_ASSERT_EQUAL(v1->id, deliveries[0].first);
+ CPPUNIT_ASSERT_EQUAL(std::string("get"), deliveries[0].second);
+
+ // Now sync the listen and perform another get — this time it should be served from cache
+ storedScb(dht::ListenSyncStatus::SYNCED);
+
+ deliveries.clear();
+ doneCalled = false;
+ bool servedFromCache2 = searchCache.get({}, query, gcb, dcb);
+ CPPUNIT_ASSERT_MESSAGE("get() should return true once cache is synced", servedFromCache2);
+ CPPUNIT_ASSERT_MESSAGE("done callback should be called when served from synced cache", doneCalled);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache get should deliver the value exactly once",
+ (size_t) 1,
+ deliveries.size());
+}
+
+void
+OpCacheTester::testGetEmptySynced()
+{
+ // Test that get() on a synced but empty cache calls dcb but not gcb.
+ dht::SearchCache searchCache;
+
+ int getCallCount = 0;
+ bool doneCalled = false;
+ bool doneSuccess = false;
+
+ auto query = std::make_shared();
+
+ // Start a listen, inject no values, mark as synced
+ size_t listenToken = searchCache.listen([](const std::vector>&, bool) { return true; },
+ query,
+ {},
+ [&](const std::shared_ptr& q,
+ dht::ValueCallback vcb,
+ dht::SyncCallback scb) -> size_t {
+ scb(dht::ListenSyncStatus::ADDED);
+ scb(dht::ListenSyncStatus::SYNCED);
+ return 1;
+ });
+ CPPUNIT_ASSERT(listenToken != 0);
+
+ bool result = searchCache.get(
+ {},
+ query,
+ [&](const std::vector>& vals) {
+ getCallCount++;
+ return true;
+ },
+ [&](bool success, const std::vector>&) {
+ doneCalled = true;
+ doneSuccess = success;
+ });
+
+ // Synced + empty: get should succeed but gcb should not be called
+ CPPUNIT_ASSERT_MESSAGE("get() should return true when cache is synced (even if empty)", result);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("get callback should NOT be called (no values)", 0, getCallCount);
+ CPPUNIT_ASSERT_MESSAGE("done callback should be called", doneCalled);
+ CPPUNIT_ASSERT_MESSAGE("done callback should indicate success", doneSuccess);
+}
+
+void
+OpCacheTester::testValueExpirationDuringListen()
+{
+ // Test that values correctly expire via refCount during listen with multiple sources.
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 300;
+ v1->data = {'D'};
+
+ // Simulate two nodes reporting the same value
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+
+ cache.onValuesAdded({v1}); // source 1
+ cache.onValuesAdded({v1}); // source 2 (no refCount increment)
+
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+ CPPUNIT_ASSERT(cache.get(300) != nullptr);
+
+ // Single expire removes the value (refCount = 1)
+ cache.onValuesExpired({v1});
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+ CPPUNIT_ASSERT(cache.get(300) == nullptr);
+}
+
+void
+OpCacheTester::testTimestampOrdering()
+{
+ // Test that expired values with timestamps older than the last update are ignored.
+ dht::OpValueCache cache([](const std::vector>&, bool) { return true; });
+
+ auto v1 = std::make_shared();
+ v1->id = 400;
+ v1->data = {'T'};
+
+ auto t1 = std::chrono::system_clock::now();
+ auto t2 = t1 + std::chrono::seconds(10);
+
+ // Add value at time t2
+ cache.onValue({v1}, false, t2);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // Try to expire at time t1 (older) — should be ignored
+ cache.onValue({v1}, true, t1);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Stale expiration should be ignored", (size_t) 1, cache.size());
+ CPPUNIT_ASSERT(cache.get(400) != nullptr);
+
+ // Expire at time t2 — should succeed
+ cache.onValue({v1}, true, t2);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+OpCacheTester::testShortExpirationPhantomExpiry()
+{
+ // End-to-end test: demonstrates phantom expire / re-add when multiple
+ // per-node ValueCaches feed into a single OpValueCache and a short-lived
+ // value expires from all per-node caches before the listen refresh.
+ //
+ // The fixed behavior (ValueCache does not time-expire while synced)
+ // prevents the phantom. Here we verify the OpValueCache stays consistent.
+
+ using namespace std::chrono;
+ int addCount = 0;
+ int expireCount = 0;
+
+ dht::OpValueCache opCache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ return true;
+ });
+
+ auto v = std::make_shared();
+ v->id = 900;
+ v->data = {'Z'};
+
+ // Simulate 3 nodes each adding the same value
+ // refCount stays at 1 (no increment on duplicate adds)
+ opCache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ opCache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ opCache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ opCache.onValuesAdded({v});
+ opCache.onValuesAdded({v});
+ opCache.onValuesAdded({v});
+ CPPUNIT_ASSERT_EQUAL(1, addCount); // first add fires callback
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, opCache.size());
+
+ // Single expire removes the value
+ opCache.onValuesExpired({v});
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, opCache.size());
+
+ // New nodes report the same value again (re-listen completed)
+ opCache.onValuesAdded({v});
+ opCache.onValuesAdded({v});
+ CPPUNIT_ASSERT_EQUAL(2, addCount); // value re-appeared
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, opCache.size());
+
+ // Single expire removes
+ opCache.onValuesExpired({v});
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, opCache.size());
+ CPPUNIT_ASSERT_EQUAL(2, expireCount);
+}
+
+void
+OpCacheTester::testHighChurnRefCountConsistency()
+{
+ // Rapid node churn: nodes join and leave quickly, each reporting the same
+ // value. Verify that the refCount never goes negative and the value is
+ // correctly expired only when all sources are gone.
+
+ bool callbackFailed = false;
+ dht::OpValueCache cache([&](const std::vector>&, bool) { return true; });
+
+ auto v = std::make_shared();
+ v->id = 800;
+ v->data = {'C', 'H', 'U', 'R', 'N'};
+
+ // Simulate 50 rapid churn cycles:
+ // - node joins, adds V (first add sets refCount=1)
+ // - node leaves, expires V (refCount→0, removed)
+ for (int i = 0; i < 50; i++) {
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ cache.onValuesAdded({v});
+ cache.onValuesExpired({v});
+ cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
+ }
+
+ // After all churn, value should be completely gone.
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("All sources gone — value must be removed", (size_t) 0, cache.size());
+
+ // Now simulate overlapping churn: add N, add N+1, expire N, expire N+1
+ // Since refCount stays at 1, the first expire removes the value.
+ // The second expire is a no-op.
+ for (int i = 0; i < 20; i++) {
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ cache.onValuesAdded({v});
+
+ cache.onNodeChanged(dht::ListenSyncStatus::ADDED);
+ cache.onValuesAdded({v}); // no increment
+
+ // First node leaves — refCount 1→0, value removed
+ cache.onValuesExpired({v});
+ cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
+
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value removed after single expire", (size_t) 0, cache.size());
+
+ // Second expire is a no-op (value already gone)
+ cache.onValuesExpired({v});
+ cache.onNodeChanged(dht::ListenSyncStatus::REMOVED);
+ }
+
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+OpCacheTester::testValueUpdateSingleSourcePhantom()
+{
+ // Verify that when a value is edited (same id, higher seq), the user
+ // sees only the new version as an add — no phantom expiration of the
+ // old version.
+
+ std::vector> events; // (id, expired?)
+
+ dht::OpValueCache cache([&](const std::vector>& vals, bool expired) {
+ for (const auto& v : vals)
+ events.emplace_back(v->id, expired);
+ return true;
+ });
+
+ auto v1 = std::make_shared();
+ v1->id = 700;
+ v1->seq = 1;
+ v1->data = {'O', 'L', 'D'};
+
+ auto v2 = std::make_shared();
+ v2->id = 700;
+ v2->seq = 2;
+ v2->data = {'N', 'E', 'W'};
+
+ // Single source adds v1
+ cache.onValuesAdded({v1});
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+ events.clear();
+
+ // ValueCache::addValues now only emits add(new) for edits,
+ // so OpValueCache updates in place via onValuesAdded.
+ cache.onValuesAdded({v2});
+
+ // User should see exactly one event: the updated value as an add.
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Expected 1 event (update add)", (size_t) 1, events.size());
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Event should be an addition", false, events[0].second);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Event should carry the new value id", v2->id, events[0].first);
+
+ // Final state: value exists with updated data
+ auto stored = cache.get(700);
+ CPPUNIT_ASSERT(stored != nullptr);
+ CPPUNIT_ASSERT_EQUAL(v2->seq, stored->seq);
+}
+
+} // namespace test
diff --git a/tests/test_opcache.h b/tests/test_opcache.h
new file mode 100644
index 00000000..5adf1a22
--- /dev/null
+++ b/tests/test_opcache.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2025 Savoir-faire Linux Inc.
+ *
+ * Author: GitHub Copilot
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace test {
+
+class OpCacheTester : public CppUnit::TestFixture
+{
+ CPPUNIT_TEST_SUITE(OpCacheTester);
+ CPPUNIT_TEST(testUpdateRefCount);
+ CPPUNIT_TEST(testBasicAddExpire);
+ CPPUNIT_TEST(testMultipleSources);
+ CPPUNIT_TEST(testUpdateSequence);
+ CPPUNIT_TEST(testCallbacks);
+ CPPUNIT_TEST(testFilters);
+ CPPUNIT_TEST(testSyncStatus);
+ CPPUNIT_TEST(testGetWhileSynced);
+ CPPUNIT_TEST(testGetWhileNotSynced);
+ CPPUNIT_TEST(testGetEmptySynced);
+ CPPUNIT_TEST(testValueExpirationDuringListen);
+ CPPUNIT_TEST(testTimestampOrdering);
+ CPPUNIT_TEST(testShortExpirationPhantomExpiry);
+ CPPUNIT_TEST(testHighChurnRefCountConsistency);
+ CPPUNIT_TEST(testValueUpdateSingleSourcePhantom);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void setUp();
+ void tearDown();
+
+ void testUpdateRefCount();
+ void testBasicAddExpire();
+ void testMultipleSources();
+ void testUpdateSequence();
+ void testCallbacks();
+ void testFilters();
+ void testSyncStatus();
+ void testGetWhileSynced();
+ void testGetWhileNotSynced();
+ void testGetEmptySynced();
+ void testValueExpirationDuringListen();
+ void testTimestampOrdering();
+ void testShortExpirationPhantomExpiry();
+ void testHighChurnRefCountConsistency();
+ void testValueUpdateSingleSourcePhantom();
+};
+
+} // namespace test
diff --git a/tests/test_valuecache.cpp b/tests/test_valuecache.cpp
new file mode 100644
index 00000000..bb9881ec
--- /dev/null
+++ b/tests/test_valuecache.cpp
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2025 Savoir-faire Linux Inc.
+ *
+ * Author: GitHub Copilot
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "test_valuecache.h"
+#include "../src/value_cache.h"
+
+namespace test {
+CPPUNIT_TEST_SUITE_REGISTRATION(ValueCacheTester);
+
+void
+ValueCacheTester::setUp()
+{}
+
+void
+ValueCacheTester::tearDown()
+{}
+
+void
+ValueCacheTester::testUpdate()
+{
+ int addCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (!expired)
+ addCount += vals.size();
+ });
+
+ dht::TypeStore types;
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->seq = 1;
+ v1->data = {'A'};
+
+ auto v2 = std::make_shared();
+ v2->id = 1;
+ v2->seq = 2;
+ v2->data = {'B'};
+
+ auto now = std::chrono::steady_clock::now();
+
+ // Add v1
+ cache.onValues({v1}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL(1, addCount);
+
+ // Update v1 -> v2
+ cache.onValues({v2}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Callback should be called for update", 2, addCount);
+}
+
+void
+ValueCacheTester::testUpdateExpiration()
+{
+ int addCount = 0;
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ });
+
+ dht::TypeStore types;
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->seq = 1;
+ v1->data = {'A'};
+
+ auto v2 = std::make_shared();
+ v2->id = 1;
+ v2->seq = 2;
+ v2->data = {'B'};
+
+ auto now = std::chrono::steady_clock::now();
+
+ // Add v1
+ cache.onValues({v1}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL(1, addCount);
+ CPPUNIT_ASSERT_EQUAL(0, expireCount);
+
+ // Update v1 -> v2: only the new value is emitted as an add,
+ // no expire callback for the old value.
+ cache.onValues({v2}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL(2, addCount);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("No expire callback on edit", 0, expireCount);
+}
+
+void
+ValueCacheTester::testExpiration()
+{
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ auto v1 = std::make_shared();
+ v1->id = 10;
+ v1->data = {'A'};
+
+ // Default expiration is 10 minutes
+ auto now = std::chrono::steady_clock::now();
+ cache.onValues({v1}, {}, {}, types, now);
+
+ // Advance time past expiration
+ auto future = now + std::chrono::minutes(11);
+
+ // expireValues returns next expiration time
+ cache.expireValues(future);
+
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testRefresh()
+{
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ auto v1 = std::make_shared();
+ v1->id = 20;
+ v1->data = {'B'};
+
+ auto now = std::chrono::steady_clock::now();
+ cache.onValues({v1}, {}, {}, types, now);
+
+ // Refresh at +5 mins
+ auto later = now + std::chrono::minutes(5);
+ cache.onValues({}, {v1->id}, {}, types, later);
+
+ // Should not expire at +11 mins (original expiration was +10, so +10 from start)
+ // But refresh should extend it to +10 from 'later' = +15 from start.
+
+ auto checkTime = now + std::chrono::minutes(11);
+ cache.expireValues(checkTime);
+
+ CPPUNIT_ASSERT_EQUAL(0, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // Should expire at +16 mins
+ auto future = now + std::chrono::minutes(16);
+ cache.expireValues(future);
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+}
+
+void
+ValueCacheTester::testExplicitExpiration()
+{
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ auto v1 = std::make_shared();
+ v1->id = 30;
+ v1->data = {'C'};
+
+ auto now = std::chrono::steady_clock::now();
+ cache.onValues({v1}, {}, {}, types, now);
+
+ cache.onValues({}, {}, {v1->id}, types, now);
+
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testClear()
+{
+ dht::ValueCache cache([](const std::vector>&, bool) {});
+ dht::TypeStore types;
+ auto now = std::chrono::steady_clock::now();
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ auto v2 = std::make_shared();
+ v2->id = 2;
+
+ cache.onValues({v1, v2}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL((size_t) 2, cache.size());
+
+ auto cbs = cache.clear();
+ // Execute callbacks
+ for (auto& cb : cbs)
+ cb();
+
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testSyncStatus()
+{
+ std::vector statuses;
+ {
+ dht::ValueCache cache([](const std::vector>&, bool) {},
+ [&](dht::ListenSyncStatus status) { statuses.push_back(status); });
+
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, statuses.size());
+ CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::ADDED);
+
+ cache.onSynced(true);
+ CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
+ CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
+
+ // Calling onSynced(true) again should not trigger the callback
+ cache.onSynced(true);
+ CPPUNIT_ASSERT_EQUAL((size_t) 2, statuses.size());
+
+ cache.onSynced(false);
+ CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
+ CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::UNSYNCED);
+
+ // Calling onSynced(false) again should not trigger the callback
+ cache.onSynced(false);
+ CPPUNIT_ASSERT_EQUAL((size_t) 3, statuses.size());
+
+ // Set to synced before destruction to test UNSYNCED on destruction
+ cache.onSynced(true);
+ CPPUNIT_ASSERT_EQUAL((size_t) 4, statuses.size());
+ CPPUNIT_ASSERT(statuses.back() == dht::ListenSyncStatus::SYNCED);
+ }
+
+ // Destructor should have been called
+ CPPUNIT_ASSERT_EQUAL((size_t) 6, statuses.size());
+ CPPUNIT_ASSERT(statuses[4] == dht::ListenSyncStatus::UNSYNCED);
+ CPPUNIT_ASSERT(statuses[5] == dht::ListenSyncStatus::REMOVED);
+}
+
+void
+ValueCacheTester::testMaxValues()
+{
+ dht::ValueCache cache([](const std::vector>&, bool) {});
+ dht::TypeStore types;
+ auto now = std::chrono::steady_clock::now();
+
+ // MAX_VALUES is 4096
+ for (int i = 0; i < 4100; ++i) {
+ auto v = std::make_shared();
+ v->id = (dht::Value::Id) i + 1;
+ cache.onValues({v}, {}, {}, types, now + std::chrono::milliseconds(i));
+ }
+
+ // Should be capped at 4096
+ CPPUNIT_ASSERT_EQUAL((size_t) 4096, cache.size());
+}
+
+void
+ValueCacheTester::testUpdateTypeExpiration()
+{
+ // Test that when a value is updated with a different type, the expiration
+ // uses the new type's expiration period, not the old one.
+ int addCount = 0;
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ });
+
+ // Register a custom type with a short expiration (2 minutes)
+ dht::TypeStore types;
+ dht::ValueType shortType(42, "short", std::chrono::minutes(2));
+ types.registerType(shortType);
+
+ // Register a custom type with a long expiration (30 minutes)
+ dht::ValueType longType(99, "long", std::chrono::minutes(30));
+ types.registerType(longType);
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->type = 42; // short type: 2 min expiration
+ v1->data = {'A'};
+
+ auto now = std::chrono::steady_clock::now();
+
+ // Add v1 with short type
+ cache.onValues({v1}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL(1, addCount);
+
+ // Update with new type (long expiration)
+ auto v2 = std::make_shared();
+ v2->id = 1;
+ v2->type = 99; // long type: 30 min expiration
+ v2->data = {'B'};
+
+ cache.onValues({v2}, {}, {}, types, now);
+ CPPUNIT_ASSERT_EQUAL(2, addCount);
+
+ // At +3 minutes: should NOT be expired (long type has 30 min expiration)
+ // Before the fix, the old type (2 min) would be used, causing expiration here
+ expireCount = 0;
+ auto at3min = now + std::chrono::minutes(3);
+ cache.expireValues(at3min);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value updated to long-expiry type should NOT expire at 3 min", 0, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // At +31 minutes: should be expired
+ auto at31min = now + std::chrono::minutes(31);
+ cache.expireValues(at31min);
+ CPPUNIT_ASSERT_EQUAL(1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testShortExpirationRefreshLost()
+{
+ // Demonstrates that a value with a short expiration type survives
+ // past its nominal expiration when the listen is synced, thanks to
+ // a proportional grace period (= typeExpiration).
+ //
+ // For a 5s type: effective expiration when synced = expiration + 5s = 10s
+ //
+ // Scenario:
+ // t=0 : Value V added → nominal expiration t+5s, effective t+10s
+ // t=6 : Past nominal but within grace → alive
+ // t=8 : Refresh arrives → V refreshed → expiration reset to t8+5s, effective t8+10s=t18s
+ // t=14: Still alive (within refreshed effective range)
+ // t=20: No more refreshes → past t8+10s=t18s → expired
+
+ int addCount = 0;
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ });
+
+ // Register a type with a very short expiration (5 seconds)
+ dht::TypeStore types;
+ dht::ValueType shortType(100, "short_lived", std::chrono::seconds(5));
+ types.registerType(shortType);
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->type = 100;
+ v1->data = {'V'};
+
+ auto t0 = std::chrono::steady_clock::now();
+
+ // t=0: Listen response arrives with V
+ cache.onValues({v1}, {}, {}, types, t0);
+ CPPUNIT_ASSERT_EQUAL(1, addCount);
+ CPPUNIT_ASSERT_EQUAL(0, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // Mark cache as synced (listen is active)
+ cache.onSynced(true);
+
+ // t=6: Past nominal 5s expiration, but grace (5s) gives effective 10s
+ auto t6 = t0 + std::chrono::seconds(6);
+ cache.expireValues(t6);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past nominal expiration thanks to proportional grace",
+ 0,
+ expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // t=8: Value refresh arrives → expiration reset to t8+5s=t13s, effective t8+10s=t18s
+ auto t8 = t0 + std::chrono::seconds(8);
+ cache.onValues({}, {v1->id}, {}, types, t8);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // t=14: Within refreshed effective range (t18s) → alive
+ auto t14 = t0 + std::chrono::seconds(14);
+ cache.expireValues(t14);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value should survive past short expiration after refresh", 0, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // t=20: No more refreshes → past effective t18s → must expire
+ auto t20 = t0 + std::chrono::seconds(20);
+ cache.expireValues(t20);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Value must eventually expire even while synced if server stops refreshing",
+ 1,
+ expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testShortExpirationHighChurn()
+{
+ // Simulates high churn: multiple rapid add/expire/refresh cycles with
+ // short-lived values, verifying the cache stays consistent.
+
+ int addCount = 0;
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ else
+ addCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ dht::ValueType shortType(101, "fast_churn", std::chrono::seconds(3));
+ types.registerType(shortType);
+
+ auto now = std::chrono::steady_clock::now();
+
+ // Simulate 10 churn cycles: add value, advance time, refresh, advance, expire
+ for (int cycle = 0; cycle < 10; cycle++) {
+ auto v = std::make_shared();
+ v->id = static_cast(500 + cycle);
+ v->type = 101;
+ v->data = {static_cast('A' + cycle)};
+
+ auto t = now + std::chrono::seconds(cycle * 10);
+
+ // Add the value
+ cache.onValues({v}, {}, {}, types, t);
+
+ // Refresh halfway through
+ auto tRefresh = t + std::chrono::seconds(2);
+ cache.onValues({}, {v->id}, {}, types, tRefresh);
+
+ // Explicit expire from server
+ auto tExpire = t + std::chrono::seconds(8);
+ cache.onValues({}, {}, {v->id}, types, tExpire);
+ }
+
+ // All 10 values were added and explicitly expired
+ CPPUNIT_ASSERT_EQUAL(10, addCount);
+ CPPUNIT_ASSERT_EQUAL(10, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testNoTimeExpirationWhileSynced()
+{
+ // Verify that when the listen is synced, the proportional grace period
+ // (= typeExpiration) delays time-based expiration but doesn't prevent
+ // it entirely. For a 2s type: effective expiration = 2s + 2s = 4s.
+
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ dht::ValueType shortType(110, "short", std::chrono::seconds(2));
+ types.registerType(shortType);
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->type = 110;
+ v1->data = {'X'};
+
+ auto t0 = std::chrono::steady_clock::now();
+ cache.onValues({v1}, {}, {}, types, t0);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // Mark as synced
+ cache.onSynced(true);
+
+ // t=3: Past nominal 2s, within effective 4s → grace protects
+ auto t3 = t0 + std::chrono::seconds(3);
+ cache.expireValues(t3);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: grace period should prevent early expiry", 0, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 1, cache.size());
+
+ // t=5: Past effective 4s (2s + 2s grace) → must expire
+ auto t5 = t0 + std::chrono::seconds(5);
+ cache.expireValues(t5);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Synced cache: value must eventually expire after grace period", 1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+void
+ValueCacheTester::testUnsyncExpiresImmediately()
+{
+ // When the cache goes from synced to unsynced, the grace period is
+ // removed and already-past-due values expire on the next check.
+ // Type expiration = 5s. Grace when synced = 5s → effective = 10s.
+
+ int expireCount = 0;
+ dht::ValueCache cache([&](const std::vector>& vals, bool expired) {
+ if (expired)
+ expireCount += vals.size();
+ });
+
+ dht::TypeStore types;
+ dht::ValueType shortType(120, "short", std::chrono::seconds(5));
+ types.registerType(shortType);
+
+ auto v1 = std::make_shared();
+ v1->id = 1;
+ v1->type = 120;
+ v1->data = {'U'};
+
+ auto t0 = std::chrono::steady_clock::now();
+ cache.onValues({v1}, {}, {}, types, t0);
+ cache.onSynced(true);
+
+ // t=7: past nominal 5s, within effective 10s while synced → alive
+ auto t7 = t0 + std::chrono::seconds(7);
+ cache.expireValues(t7);
+ CPPUNIT_ASSERT_EQUAL(0, expireCount);
+
+ // Now unsync (node went away / connectivity loss)
+ cache.onSynced(false);
+
+ // Same time point t=7: past nominal 5s, no grace → expires
+ cache.expireValues(t7);
+ CPPUNIT_ASSERT_EQUAL_MESSAGE("Unsynced cache should immediately expire past-due values", 1, expireCount);
+ CPPUNIT_ASSERT_EQUAL((size_t) 0, cache.size());
+}
+
+} // namespace test
diff --git a/tests/test_valuecache.h b/tests/test_valuecache.h
new file mode 100644
index 00000000..aa1f1c41
--- /dev/null
+++ b/tests/test_valuecache.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 Savoir-faire Linux Inc.
+ *
+ * Author: GitHub Copilot
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace test {
+
+class ValueCacheTester : public CppUnit::TestFixture
+{
+ CPPUNIT_TEST_SUITE(ValueCacheTester);
+ CPPUNIT_TEST(testUpdate);
+ CPPUNIT_TEST(testUpdateExpiration);
+ CPPUNIT_TEST(testExpiration);
+ CPPUNIT_TEST(testRefresh);
+ CPPUNIT_TEST(testExplicitExpiration);
+ CPPUNIT_TEST(testClear);
+ CPPUNIT_TEST(testSyncStatus);
+ CPPUNIT_TEST(testMaxValues);
+ CPPUNIT_TEST(testUpdateTypeExpiration);
+ CPPUNIT_TEST(testShortExpirationRefreshLost);
+ CPPUNIT_TEST(testShortExpirationHighChurn);
+ CPPUNIT_TEST(testNoTimeExpirationWhileSynced);
+ CPPUNIT_TEST(testUnsyncExpiresImmediately);
+ CPPUNIT_TEST_SUITE_END();
+
+public:
+ void setUp();
+ void tearDown();
+
+ void testUpdate();
+ void testUpdateExpiration();
+ void testExpiration();
+ void testRefresh();
+ void testExplicitExpiration();
+ void testClear();
+ void testSyncStatus();
+ void testMaxValues();
+ void testUpdateTypeExpiration();
+ void testShortExpirationRefreshLost();
+ void testShortExpirationHighChurn();
+ void testNoTimeExpirationWhileSynced();
+ void testUnsyncExpiresImmediately();
+};
+
+} // namespace test