mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-15 17:14:07 +00:00
feat(cdclient): content-addressed copies of the client's cdclient.fdb
FdbSnapshot names copies cdclient-<hash>.fdb / CDServer-<hash>.sqlite, writes them under temporary names and renames them in, keeps a pointer file naming the current pair, removes old copies, and decides when a changed file has settled enough to hash. Nothing in it logs, so it can run on a worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
170
tests/dCommonTests/FdbSnapshotTests.cpp
Normal file
170
tests/dCommonTests/FdbSnapshotTests.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "FdbSnapshot.h"
|
||||
#include "../FdbTestWriter.h"
|
||||
|
||||
namespace {
|
||||
using FdbTestWriter::Value;
|
||||
|
||||
std::vector<FdbTestWriter::Table> Tables(int32_t extraRows, const std::string& name = "first") {
|
||||
FdbTestWriter::Table objects{ "Objects", { { "id", eSqliteDataType::INT32 }, { "name", eSqliteDataType::TEXT_4 } }, 4, {} };
|
||||
objects.rows.push_back({ Value::Int(1), Value::Text(name) });
|
||||
for (int32_t i = 0; i < extraRows; i++) objects.rows.push_back({ Value::Int(100 + i), Value::Text("x") });
|
||||
FdbTestWriter::Table other{ "Other", { { "id", eSqliteDataType::INT32 } }, 2, { { Value::Int(5) } } };
|
||||
return { objects, other };
|
||||
}
|
||||
|
||||
class FdbSnapshotTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_Dir = std::filesystem::temp_directory_path() / ("dlu_fdb_snapshot_" + std::string(::testing::UnitTest::GetInstance()->current_test_info()->name()));
|
||||
std::filesystem::remove_all(m_Dir);
|
||||
std::filesystem::create_directories(m_Dir / "res");
|
||||
std::filesystem::create_directories(m_Dir / "resServer");
|
||||
}
|
||||
void TearDown() override { std::filesystem::remove_all(m_Dir); }
|
||||
|
||||
std::filesystem::path Client() const { return m_Dir / "res" / "cdclient.fdb"; }
|
||||
std::filesystem::path Server() const { return m_Dir / "resServer"; }
|
||||
|
||||
std::filesystem::path m_Dir;
|
||||
};
|
||||
}
|
||||
|
||||
TEST(FdbSnapshotNames, HashIsFnv1a64) {
|
||||
EXPECT_EQ(FdbSnapshot::Hash(nullptr, 0), 14695981039346656037ULL);
|
||||
const std::string a = "a";
|
||||
EXPECT_EQ(FdbSnapshot::Hash(reinterpret_cast<const uint8_t*>(a.data()), a.size()), 0xaf63dc4c8601ec8cULL);
|
||||
}
|
||||
|
||||
TEST(FdbSnapshotNames, NamesRoundTrip) {
|
||||
const uint64_t hash = 0x0123456789abcdefULL;
|
||||
EXPECT_EQ(FdbSnapshot::FdbName(hash), "cdclient-0123456789abcdef.fdb");
|
||||
EXPECT_EQ(FdbSnapshot::SqliteName(hash), "CDServer-0123456789abcdef.sqlite");
|
||||
EXPECT_EQ(FdbSnapshot::ParseName(FdbSnapshot::FdbName(hash)), hash);
|
||||
EXPECT_EQ(FdbSnapshot::ParseName(FdbSnapshot::SqliteName(hash)), hash);
|
||||
EXPECT_FALSE(FdbSnapshot::ParseName("cdclient.fdb"));
|
||||
EXPECT_FALSE(FdbSnapshot::ParseName("CDServer.sqlite"));
|
||||
EXPECT_FALSE(FdbSnapshot::ParseName("cdclient-0123456789ABCDEF.fdb"));
|
||||
EXPECT_FALSE(FdbSnapshot::ParseName("cdclient-0123.fdb"));
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, CopyIsNamedByItsContent) {
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(0)));
|
||||
std::string error;
|
||||
const auto hash = FdbSnapshot::MakeCopy(Client(), Server(), error);
|
||||
ASSERT_TRUE(hash) << error;
|
||||
EXPECT_EQ(hash, FdbSnapshot::HashFile(Client()));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / FdbSnapshot::FdbName(*hash)));
|
||||
|
||||
// The same bytes again keep the one copy
|
||||
EXPECT_EQ(FdbSnapshot::MakeCopy(Client(), Server(), error), hash);
|
||||
|
||||
// New bytes, new name; the old copy stays until it is removed
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(1)));
|
||||
const auto next = FdbSnapshot::MakeCopy(Client(), Server(), error);
|
||||
ASSERT_TRUE(next);
|
||||
EXPECT_NE(*next, *hash);
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / FdbSnapshot::FdbName(*hash)));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / FdbSnapshot::FdbName(*next)));
|
||||
|
||||
uint32_t files = 0;
|
||||
for ([[maybe_unused]] const auto& entry : std::filesystem::directory_iterator(Server())) files++;
|
||||
EXPECT_EQ(files, 2u) << "no temporary copies left behind";
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, MissingSourceFails) {
|
||||
std::string error;
|
||||
EXPECT_FALSE(FdbSnapshot::MakeCopy(Client(), Server(), error));
|
||||
EXPECT_FALSE(error.empty());
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, CurrentRoundTrip) {
|
||||
EXPECT_FALSE(FdbSnapshot::ReadCurrent(Server()));
|
||||
const FdbSnapshot::Current current{ FdbSnapshot::FdbName(1), FdbSnapshot::SqliteName(1) };
|
||||
ASSERT_TRUE(FdbSnapshot::WriteCurrent(Server(), current));
|
||||
EXPECT_EQ(FdbSnapshot::ReadCurrent(Server()), current);
|
||||
const FdbSnapshot::Current next{ FdbSnapshot::FdbName(2), FdbSnapshot::DEFAULT_SQLITE };
|
||||
ASSERT_TRUE(FdbSnapshot::WriteCurrent(Server(), next));
|
||||
EXPECT_EQ(FdbSnapshot::ReadCurrent(Server()), next);
|
||||
|
||||
// A pointer file naming something outside resServer is ignored
|
||||
ASSERT_TRUE(FdbSnapshot::WriteCurrent(Server(), { "../cdclient.fdb", "CDServer.sqlite" }));
|
||||
EXPECT_FALSE(FdbSnapshot::ReadCurrent(Server()));
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, RemoveOldKeepsCurrentAndUnrelatedFiles) {
|
||||
for (const uint64_t hash : { 1, 2, 3 }) {
|
||||
FdbTestWriter::WriteFile(Server() / FdbSnapshot::FdbName(hash), { 1 });
|
||||
FdbTestWriter::WriteFile(Server() / FdbSnapshot::SqliteName(hash), { 1 });
|
||||
}
|
||||
FdbTestWriter::WriteFile(Server() / "CDServer.sqlite", { 1 });
|
||||
FdbTestWriter::WriteFile(Server() / "cdclient-.abc.tmp", { 1 });
|
||||
FdbTestWriter::WriteFile(Server() / "notes.txt", { 1 });
|
||||
|
||||
const auto removed = FdbSnapshot::RemoveOld(Server(), { 2, 3 });
|
||||
EXPECT_EQ(removed.size(), 3u);
|
||||
EXPECT_FALSE(std::filesystem::exists(Server() / FdbSnapshot::FdbName(1)));
|
||||
EXPECT_FALSE(std::filesystem::exists(Server() / FdbSnapshot::SqliteName(1)));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / FdbSnapshot::FdbName(2)));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / FdbSnapshot::SqliteName(3)));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / "CDServer.sqlite"));
|
||||
EXPECT_TRUE(std::filesystem::exists(Server() / "notes.txt"));
|
||||
}
|
||||
|
||||
TEST(FdbSnapshotWatcher, TriggersOnceASettledChangeIsSeen) {
|
||||
FdbSnapshot::Watcher watcher;
|
||||
const FdbSnapshot::Stamp first{ 100, 1, true };
|
||||
watcher.Accept(first);
|
||||
EXPECT_FALSE(watcher.Poll(first));
|
||||
|
||||
// Being written: changes between polls, so wait
|
||||
const FdbSnapshot::Stamp writing{ 50, 2, true };
|
||||
const FdbSnapshot::Stamp written{ 120, 3, true };
|
||||
EXPECT_FALSE(watcher.Poll(writing));
|
||||
EXPECT_FALSE(watcher.Poll(written));
|
||||
EXPECT_TRUE(watcher.Poll(written));
|
||||
// Until it is accepted, it keeps asking (a failed reload is retried)
|
||||
EXPECT_TRUE(watcher.Poll(written));
|
||||
watcher.Accept(written);
|
||||
EXPECT_FALSE(watcher.Poll(written));
|
||||
|
||||
// A missing file (mid-replace) never triggers
|
||||
const FdbSnapshot::Stamp missing{};
|
||||
EXPECT_FALSE(watcher.Poll(missing));
|
||||
EXPECT_FALSE(watcher.Poll(missing));
|
||||
// And a file put back as it was isn't a change
|
||||
EXPECT_FALSE(watcher.Poll(written));
|
||||
EXPECT_FALSE(watcher.Poll(written));
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, StampFollowsTheFile) {
|
||||
EXPECT_FALSE(FdbSnapshot::StampOf(Client()).exists);
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(0)));
|
||||
const auto stamp = FdbSnapshot::StampOf(Client());
|
||||
EXPECT_TRUE(stamp.exists);
|
||||
EXPECT_EQ(stamp.size, std::filesystem::file_size(Client()));
|
||||
}
|
||||
|
||||
TEST_F(FdbSnapshotTest, DescribeChangesNamesChangedTables) {
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(0)));
|
||||
const auto before = FdbSnapshot::Summarize(Client());
|
||||
ASSERT_EQ(before.size(), 2u);
|
||||
EXPECT_EQ(before.at("Objects").rows, 1u);
|
||||
|
||||
EXPECT_TRUE(FdbSnapshot::DescribeChanges(before, before).empty());
|
||||
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(2)));
|
||||
auto lines = FdbSnapshot::DescribeChanges(before, FdbSnapshot::Summarize(Client()));
|
||||
ASSERT_EQ(lines.size(), 1u);
|
||||
EXPECT_EQ(lines[0], "Objects: 1 -> 3 rows");
|
||||
|
||||
FdbTestWriter::WriteFile(Client(), FdbTestWriter::Write(Tables(0, "renamed")));
|
||||
lines = FdbSnapshot::DescribeChanges(before, FdbSnapshot::Summarize(Client()));
|
||||
ASSERT_EQ(lines.size(), 1u);
|
||||
EXPECT_EQ(lines[0], "Objects: 1 -> 1 rows (values changed)");
|
||||
}
|
||||
Reference in New Issue
Block a user