/*
* 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 "opcachetester.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 v1
// Note: onValuesExpired uses ID to find value.
cache.onValuesExpired({v1});
// If refCount was incremented on update, we should still have the value (refCount=1).
// If not, it will be removed (refCount=0).
// Check if value exists
stored = cache.get(1);
// This assertion will fail if the bug exists
CPPUNIT_ASSERT_MESSAGE("Value should still exist after expiring one source", stored != nullptr);
if (stored) {
CPPUNIT_ASSERT_EQUAL(v2->seq, stored->seq);
}
// Expire v2
cache.onValuesExpired({v2});
stored = cache.get(1);
CPPUNIT_ASSERT_MESSAGE("Value should be removed after expiring all sources", 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)
cache.onValuesAdded({v1});
cache.onValuesAdded({v1});
CPPUNIT_ASSERT_EQUAL((size_t)1, cache.size());
// Expire once
cache.onValuesExpired({v1});
CPPUNIT_ASSERT_EQUAL((size_t)1, cache.size());
CPPUNIT_ASSERT(cache.get(20));
// Expire twice
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 but increment refCount)
cache.onValuesAdded({v3});
stored = cache.get(30);
CPPUNIT_ASSERT_EQUAL((uint16_t)20, stored->seq);
// We added 3 times. Should need 3 expires to remove.
cache.onValuesExpired({v1}); // 2 left
CPPUNIT_ASSERT(cache.get(30));
cache.onValuesExpired({v2}); // 1 left
CPPUNIT_ASSERT(cache.get(30));
cache.onValuesExpired({v3}); // 0 left
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)
cache.onValuesExpired({v1});
CPPUNIT_ASSERT_EQUAL(0, expireCount); // RefCount dec, not removed yet
cache.onValuesExpired({v1});
CPPUNIT_ASSERT_EQUAL(0, expireCount); // RefCount dec, not removed yet
cache.onValuesExpired({v1});
CPPUNIT_ASSERT_EQUAL(1, expireCount); // Removed now
}
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());
}
} // namespace test