mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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>
171 lines
7.1 KiB
C++
171 lines
7.1 KiB
C++
#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)");
|
|
}
|