Files
clio/tests/unit/data/impl/LedgerCacheFileTests.cpp
2026-03-24 15:25:32 +00:00

733 lines
26 KiB
C++

#include "data/LedgerCache.hpp"
#include "data/impl/LedgerCacheFile.hpp"
#include "util/NameGenerator.hpp"
#include "util/TmpFile.hpp"
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <xrpl/basics/base_uint.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <ios>
#include <limits>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
using namespace data::impl;
struct LedgerCacheFileTestBase : ::testing::Test {
struct DataSizeParams {
size_t mapEntries;
size_t deletedEntries;
size_t blobSize;
std::string description;
};
enum class CorruptionType {
InvalidVersion,
CorruptedSeparator1, // After header
CorruptedSeparator2, // After map hash
CorruptedSeparator3, // After deleted hash
MapKeyCorrupted,
MapSeqCorrupted,
MapBlobSizeCorrupted,
MapBlobDataCorrupted,
DeletedKeyCorrupted,
DeletedSeqCorrupted,
DeletedBlobSizeCorrupted,
DeletedBlobDataCorrupted,
HeaderLatestSeqCorrupted
};
struct CorruptionParams {
CorruptionType type;
std::string description;
};
struct EntryOffsets {
size_t keyOffset;
size_t seqOffset;
size_t blobSizeOffset;
size_t blobDataOffset;
};
struct FileOffsets {
size_t headerOffset;
size_t separator1Offset;
size_t mapStartOffset;
std::vector<EntryOffsets> mapEntries;
size_t separator2Offset;
size_t deletedStartOffset;
std::vector<EntryOffsets> deletedEntries;
size_t separator3Offset;
size_t hashOffset;
static FileOffsets
calculate(LedgerCacheFile::DataView const& dataView)
{
FileOffsets offsets{};
size_t currentOffset = 0;
offsets.headerOffset = currentOffset;
currentOffset += sizeof(LedgerCacheFile::Header);
offsets.separator1Offset = currentOffset;
currentOffset += 16;
// Map entries
offsets.mapStartOffset = currentOffset;
for (auto const& [key, entry] : dataView.map) {
EntryOffsets entryOffsets{};
entryOffsets.keyOffset = currentOffset;
entryOffsets.seqOffset = currentOffset + 32; // uint256 size
entryOffsets.blobSizeOffset = currentOffset + 32 + 4; // + uint32 size
entryOffsets.blobDataOffset = currentOffset + 32 + 4 + 8; // + size_t size
offsets.mapEntries.push_back(entryOffsets);
currentOffset += 32 + 4 + 8 + entry.blob.size(); // key + seq + size + blob
}
// Separator 2 (after map entries)
offsets.separator2Offset = currentOffset;
currentOffset += 16;
// Deleted entries
offsets.deletedStartOffset = currentOffset;
for (auto const& [key, entry] : dataView.deleted) {
EntryOffsets entryOffsets{};
entryOffsets.keyOffset = currentOffset;
entryOffsets.seqOffset = currentOffset + 32;
entryOffsets.blobSizeOffset = currentOffset + 32 + 4;
entryOffsets.blobDataOffset = currentOffset + 32 + 4 + 8;
offsets.deletedEntries.push_back(entryOffsets);
currentOffset += 32 + 4 + 8 + entry.blob.size();
}
// Separator 3 (after deleted entries)
offsets.separator3Offset = currentOffset;
currentOffset += 16;
// Overall file hash
offsets.hashOffset = currentOffset;
return offsets;
}
};
~LedgerCacheFileTestBase() override
{
auto const pathWithNewPrefix = fmt::format("{}.new", tmpFile.path);
if (std::filesystem::exists(pathWithNewPrefix))
std::filesystem::remove(pathWithNewPrefix);
}
static std::vector<DataSizeParams> const kDATA_SIZE_PARAMS;
static std::vector<CorruptionParams> const kCORRUPTION_PARAMS;
TmpFile tmpFile = TmpFile::empty();
static uint32_t constexpr kLATEST_SEQUENCE = 12345;
static LedgerCacheFile::Data
createTestData(size_t mapSize, size_t deletedSize, size_t blobSize)
{
LedgerCacheFile::Data data;
data.latestSeq = kLATEST_SEQUENCE;
for (size_t i = 0; i < mapSize; ++i) {
ripple::uint256 key;
std::memset(key.data(), static_cast<int>(i), ripple::uint256::size());
data::LedgerCache::CacheEntry entry;
entry.seq = static_cast<uint32_t>(1000 + i);
entry.blob.resize(blobSize);
std::memset(entry.blob.data(), static_cast<int>(i + 100), blobSize);
data.map.emplace(key, std::move(entry));
}
for (size_t i = 0; i < deletedSize; ++i) {
ripple::uint256 key;
std::memset(key.data(), static_cast<int>(i + 200), ripple::uint256::size());
data::LedgerCache::CacheEntry entry;
entry.seq = static_cast<uint32_t>(2000 + i);
entry.blob.resize(blobSize);
std::memset(entry.blob.data(), static_cast<int>(i + 250), blobSize);
data.deleted.emplace(key, std::move(entry));
}
return data;
}
static LedgerCacheFile::DataView
toDataView(LedgerCacheFile::Data const& data)
{
return LedgerCacheFile::DataView{
.latestSeq = data.latestSeq, .map = data.map, .deleted = data.deleted
};
}
void
corruptFile(CorruptionType type, LedgerCacheFile::DataView const& dataView) const
{
std::fstream file(tmpFile.path, std::ios::in | std::ios::out | std::ios::binary);
ASSERT_TRUE(file.is_open());
auto const offsets = FileOffsets::calculate(dataView);
switch (type) {
case CorruptionType::InvalidVersion:
file.seekp(offsets.headerOffset);
{
uint32_t invalidVersion = 999;
file.write(
reinterpret_cast<char const*>(&invalidVersion), sizeof(invalidVersion)
);
}
break;
case CorruptionType::CorruptedSeparator1:
file.seekp(offsets.separator1Offset);
{
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::CorruptedSeparator2:
file.seekp(offsets.separator2Offset);
{
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::CorruptedSeparator3:
file.seekp(offsets.separator3Offset);
{
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::MapKeyCorrupted:
if (!offsets.mapEntries.empty()) {
file.seekp(offsets.mapEntries[0].keyOffset);
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::MapSeqCorrupted:
if (!offsets.mapEntries.empty()) {
file.seekp(offsets.mapEntries[0].seqOffset);
uint32_t const corruptSeq = std::numeric_limits<uint32_t>::max();
file.write(reinterpret_cast<char const*>(&corruptSeq), sizeof(corruptSeq));
}
break;
case CorruptionType::MapBlobSizeCorrupted:
if (!offsets.mapEntries.empty()) {
file.seekp(offsets.mapEntries[0].blobSizeOffset);
size_t const corruptSize = std::numeric_limits<size_t>::max();
file.write(reinterpret_cast<char const*>(&corruptSize), sizeof(corruptSize));
}
break;
case CorruptionType::MapBlobDataCorrupted:
if (!offsets.mapEntries.empty() && !dataView.map.begin()->second.blob.empty()) {
file.seekp(offsets.mapEntries[0].blobDataOffset);
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::DeletedKeyCorrupted:
if (!offsets.deletedEntries.empty()) {
file.seekp(offsets.deletedEntries[0].keyOffset);
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::DeletedSeqCorrupted:
if (!offsets.deletedEntries.empty()) {
file.seekp(offsets.deletedEntries[0].seqOffset);
uint32_t const corruptSeq = std::numeric_limits<uint32_t>::max();
file.write(reinterpret_cast<char const*>(&corruptSeq), sizeof(corruptSeq));
}
break;
case CorruptionType::DeletedBlobSizeCorrupted:
if (!offsets.deletedEntries.empty()) {
file.seekp(offsets.deletedEntries[0].blobSizeOffset);
size_t const corruptSize = std::numeric_limits<size_t>::max();
file.write(reinterpret_cast<char const*>(&corruptSize), sizeof(corruptSize));
}
break;
case CorruptionType::DeletedBlobDataCorrupted:
if (!offsets.deletedEntries.empty() &&
!dataView.deleted.begin()->second.blob.empty()) {
file.seekp(offsets.deletedEntries[0].blobDataOffset);
char const corruptByte = static_cast<char>(0xFF);
file.write(&corruptByte, 1);
}
break;
case CorruptionType::HeaderLatestSeqCorrupted:
file.seekp(offsets.headerOffset + sizeof(uint32_t)); // skip version
{
uint32_t corruptSeq =
0; // set to 0 to fail validation if minLatestSequence > 0
file.write(reinterpret_cast<char const*>(&corruptSeq), sizeof(corruptSeq));
}
break;
}
}
static void
verifyDataEquals(LedgerCacheFile::Data const& expected, LedgerCacheFile::Data const& actual)
{
EXPECT_EQ(expected.latestSeq, actual.latestSeq);
EXPECT_EQ(expected.map.size(), actual.map.size());
EXPECT_EQ(expected.deleted.size(), actual.deleted.size());
for (auto const& [key, entry] : expected.map) {
auto it = actual.map.find(key);
ASSERT_NE(it, actual.map.end()) << "Key not found in actual map";
EXPECT_EQ(entry.seq, it->second.seq);
EXPECT_EQ(entry.blob, it->second.blob);
}
for (auto const& [key, entry] : expected.deleted) {
auto it = actual.deleted.find(key);
ASSERT_NE(it, actual.deleted.end()) << "Key not found in actual deleted";
EXPECT_EQ(entry.seq, it->second.seq);
EXPECT_EQ(entry.blob, it->second.blob);
}
}
};
std::vector<
LedgerCacheFileTestBase::DataSizeParams> const LedgerCacheFileTestBase::kDATA_SIZE_PARAMS = {
{.mapEntries = 0, .deletedEntries = 0, .blobSize = 0, .description = "empty"},
{.mapEntries = 1, .deletedEntries = 0, .blobSize = 10, .description = "single_map_small_blob"},
{.mapEntries = 0,
.deletedEntries = 1,
.blobSize = 100,
.description = "single_deleted_medium_blob"},
{.mapEntries = 5,
.deletedEntries = 3,
.blobSize = 1000,
.description = "multiple_entries_large_blob"},
{.mapEntries = 10,
.deletedEntries = 10,
.blobSize = 50000,
.description = "many_entries_huge_blob"}
};
std::vector<LedgerCacheFileTestBase::CorruptionParams> const
LedgerCacheFileTestBase::kCORRUPTION_PARAMS = {
{.type = CorruptionType::InvalidVersion, .description = "invalid_version"},
{.type = CorruptionType::CorruptedSeparator1, .description = "corrupted_separator1"},
{.type = CorruptionType::CorruptedSeparator2, .description = "corrupted_separator2"},
{.type = CorruptionType::CorruptedSeparator3, .description = "corrupted_separator3"},
{.type = CorruptionType::MapKeyCorrupted, .description = "map_key_corrupted"},
{.type = CorruptionType::MapSeqCorrupted, .description = "map_seq_corrupted"},
{.type = CorruptionType::MapBlobSizeCorrupted, .description = "map_blob_size_corrupted"},
{.type = CorruptionType::MapBlobDataCorrupted, .description = "map_blob_data_corrupted"},
{.type = CorruptionType::DeletedKeyCorrupted, .description = "deleted_key_corrupted"},
{.type = CorruptionType::DeletedSeqCorrupted, .description = "deleted_seq_corrupted"},
{.type = CorruptionType::DeletedBlobSizeCorrupted,
.description = "deleted_blob_size_corrupted"},
{.type = CorruptionType::DeletedBlobDataCorrupted,
.description = "deleted_blob_data_corrupted"},
{.type = CorruptionType::HeaderLatestSeqCorrupted,
.description = "header_latest_seq_corrupted"}
};
struct LedgerCacheFileTest
: LedgerCacheFileTestBase,
::testing::WithParamInterface<LedgerCacheFileTestBase::DataSizeParams> {
static std::string
roundTripParamName(::testing::TestParamInfo<DataSizeParams> const& info)
{
return info.param.description;
}
};
INSTANTIATE_TEST_SUITE_P(
AllDataSizes,
LedgerCacheFileTest,
::testing::ValuesIn(LedgerCacheFileTestBase::kDATA_SIZE_PARAMS),
LedgerCacheFileTest::roundTripParamName
);
TEST_P(LedgerCacheFileTest, WriteAndReadData)
{
auto dataParams = GetParam();
LedgerCacheFile cacheFile(tmpFile.path);
auto testData =
createTestData(dataParams.mapEntries, dataParams.deletedEntries, dataParams.blobSize);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value()) << "Failed to write: " << writeResult.error();
EXPECT_TRUE(std::filesystem::exists(tmpFile.path));
EXPECT_GT(std::filesystem::file_size(tmpFile.path), 0u);
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value()) << "Failed to read: " << readResult.error();
verifyDataEquals(testData, readResult.value());
}
struct LedgerCacheFileCorruptionTest
: LedgerCacheFileTestBase,
::testing::WithParamInterface<LedgerCacheFileTestBase::CorruptionParams> {
static std::string
corruptionParamName(::testing::TestParamInfo<CorruptionParams> const& info)
{
return info.param.description;
}
};
INSTANTIATE_TEST_SUITE_P(
AllCorruptions,
LedgerCacheFileCorruptionTest,
::testing::ValuesIn(LedgerCacheFileTestBase::kCORRUPTION_PARAMS),
LedgerCacheFileCorruptionTest::corruptionParamName
);
TEST_P(LedgerCacheFileCorruptionTest, HandleCorruption)
{
auto corruptionParams = GetParam();
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(3, 2, 100);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value()) << "Failed to write: " << writeResult.error();
corruptFile(corruptionParams.type, dataView);
auto readResult = cacheFile.read(0);
EXPECT_FALSE(readResult.has_value()) << "Should have failed to read corrupted file";
std::string const& error = readResult.error();
switch (corruptionParams.type) {
case CorruptionType::InvalidVersion:
EXPECT_THAT(error, ::testing::HasSubstr("wrong version"));
break;
case CorruptionType::CorruptedSeparator1:
case CorruptionType::CorruptedSeparator2:
case CorruptionType::CorruptedSeparator3:
EXPECT_THAT(error, ::testing::HasSubstr("Separator verification failed"));
break;
case CorruptionType::MapKeyCorrupted:
case CorruptionType::MapSeqCorrupted:
EXPECT_FALSE(error.empty());
break;
case CorruptionType::MapBlobSizeCorrupted:
EXPECT_THAT(
error,
::testing::AnyOf(
::testing::HasSubstr("Error reading cache file"),
::testing::HasSubstr("Failed to read blob"),
::testing::HasSubstr("Hash file corruption detected")
)
);
break;
case CorruptionType::MapBlobDataCorrupted:
EXPECT_THAT(
error,
::testing::AnyOf(
::testing::HasSubstr("Hash file corruption detected"),
::testing::HasSubstr("Error reading cache file")
)
);
break;
case CorruptionType::DeletedKeyCorrupted:
case CorruptionType::DeletedSeqCorrupted:
EXPECT_FALSE(error.empty());
break;
case CorruptionType::DeletedBlobSizeCorrupted:
EXPECT_THAT(
error,
::testing::AnyOf(
::testing::HasSubstr("Error reading cache file"),
::testing::HasSubstr("Failed to read blob"),
::testing::HasSubstr("Hash file corruption detected")
)
);
break;
case CorruptionType::DeletedBlobDataCorrupted:
EXPECT_THAT(
error,
::testing::AnyOf(
::testing::HasSubstr("Hash file corruption detected"),
::testing::HasSubstr("Error reading cache file")
)
);
break;
case CorruptionType::HeaderLatestSeqCorrupted:
EXPECT_THAT(error, ::testing::HasSubstr("Hash file corruption detected"));
break;
}
}
struct LedgerCacheFileEdgeCaseTest : LedgerCacheFileTestBase {};
TEST_F(LedgerCacheFileEdgeCaseTest, NonExistingFile)
{
LedgerCacheFile invalidPathFile("/invalid/path/file.cache");
auto testData = createTestData(1, 1, 10);
auto dataView = toDataView(testData);
auto writeResult = invalidPathFile.write(dataView);
EXPECT_FALSE(writeResult.has_value());
EXPECT_THAT(writeResult.error(), ::testing::HasSubstr("Couldn't open file"));
auto readResult = invalidPathFile.read(0);
EXPECT_FALSE(readResult.has_value());
EXPECT_THAT(readResult.error(), ::testing::HasSubstr("Couldn't open file"));
}
TEST_F(LedgerCacheFileEdgeCaseTest, MaxSequenceNumber)
{
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(1, 1, 10);
testData.latestSeq = std::numeric_limits<uint32_t>::max();
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, ZeroSizedBlobs)
{
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(3, 2, 0);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, SpecialKeyPatterns)
{
LedgerCacheFile cacheFile(tmpFile.path);
LedgerCacheFile::Data testData;
testData.latestSeq = 100;
ripple::uint256 zeroKey;
std::memset(zeroKey.data(), 0, ripple::uint256::size());
testData.map.emplace(zeroKey, data::LedgerCache::CacheEntry{.seq = 1, .blob = {1, 2, 3}});
ripple::uint256 onesKey;
std::memset(onesKey.data(), 0xFF, ripple::uint256::size());
testData.map.emplace(onesKey, data::LedgerCache::CacheEntry{.seq = 2, .blob = {4, 5, 6}});
ripple::uint256 altKey;
for (size_t i = 0; i < ripple::uint256::size(); ++i) {
altKey.data()[i] = static_cast<unsigned char>(((i % 2) != 0u) ? 0xAA : 0x55);
}
testData.deleted.emplace(altKey, data::LedgerCache::CacheEntry{.seq = 3, .blob = {7, 8, 9}});
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, LargeBlobs)
{
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(1, 1, 1024 * 1024);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, SequenceNumber)
{
LedgerCacheFile cacheFile(tmpFile.path);
LedgerCacheFile::Data testData;
testData.latestSeq = 0;
ripple::uint256 key1, key2, key3;
std::memset(key1.data(), 1, ripple::uint256::size());
std::memset(key2.data(), 2, ripple::uint256::size());
std::memset(key3.data(), 3, ripple::uint256::size());
testData.map.emplace(key1, data::LedgerCache::CacheEntry{.seq = 0, .blob = {1}});
testData.map.emplace(
key2,
data::LedgerCache::CacheEntry{.seq = std::numeric_limits<uint32_t>::max(), .blob = {2}}
);
testData.deleted.emplace(
key3,
data::LedgerCache::CacheEntry{.seq = std::numeric_limits<uint32_t>::max() / 2, .blob = {3}}
);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, OnlyMapEntries)
{
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(5, 0, 100);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, OnlyDeletedEntries)
{
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(0, 5, 100);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(0);
ASSERT_TRUE(readResult.has_value());
verifyDataEquals(testData, readResult.value());
}
TEST_F(LedgerCacheFileEdgeCaseTest, WriteCreatesFileWithSuffixNew)
{
// The test causes failure of rename operation by creating destination as directory
std::filesystem::remove(tmpFile.path);
std::filesystem::create_directory(tmpFile.path);
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(1, 1, 10);
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
EXPECT_FALSE(writeResult.has_value());
auto newFilePath = fmt::format("{}.new", tmpFile.path);
EXPECT_THAT(writeResult.error(), ::testing::HasSubstr(newFilePath));
EXPECT_TRUE(std::filesystem::exists(newFilePath));
EXPECT_TRUE(std::filesystem::is_regular_file(newFilePath));
}
struct LedgerCacheFileMinSequenceValidationParams {
uint32_t latestSeq;
uint32_t minLatestSeq;
bool shouldSucceed;
std::string testName;
};
struct LedgerCacheFileMinSequenceValidationTest
: LedgerCacheFileTestBase,
::testing::WithParamInterface<LedgerCacheFileMinSequenceValidationParams> {};
INSTANTIATE_TEST_SUITE_P(
LedgerCacheFileMinSequenceValidationTests,
LedgerCacheFileMinSequenceValidationTest,
::testing::Values(
LedgerCacheFileMinSequenceValidationParams{
.latestSeq = 1000u,
.minLatestSeq = 500u,
.shouldSucceed = true,
.testName = "accept_when_min_less_than_latest"
},
LedgerCacheFileMinSequenceValidationParams{
.latestSeq = 1000u,
.minLatestSeq = 2000u,
.shouldSucceed = false,
.testName = "reject_when_min_greater_than_latest"
},
LedgerCacheFileMinSequenceValidationParams{
.latestSeq = 1000u,
.minLatestSeq = 1000u,
.shouldSucceed = true,
.testName = "accept_when_min_equals_latest"
},
LedgerCacheFileMinSequenceValidationParams{
.latestSeq = 0u,
.minLatestSeq = 0u,
.shouldSucceed = true,
.testName = "accept_zero_sequence"
}
),
tests::util::kNAME_GENERATOR
);
TEST_P(LedgerCacheFileMinSequenceValidationTest, ValidateMinSequence)
{
auto const params = GetParam();
auto const latestSeq = params.latestSeq;
auto const minLatestSeq = params.minLatestSeq;
auto const shouldSucceed = params.shouldSucceed;
LedgerCacheFile cacheFile(tmpFile.path);
auto testData = createTestData(3, 2, 100);
testData.latestSeq = latestSeq;
auto dataView = toDataView(testData);
auto writeResult = cacheFile.write(dataView);
ASSERT_TRUE(writeResult.has_value());
auto readResult = cacheFile.read(minLatestSeq);
if (shouldSucceed) {
ASSERT_TRUE(readResult.has_value())
<< "Expected read to succeed but got error: " << readResult.error();
EXPECT_EQ(readResult.value().latestSeq, latestSeq);
} else {
EXPECT_FALSE(readResult.has_value()) << "Expected read to fail but it succeeded";
EXPECT_THAT(readResult.error(), ::testing::HasSubstr("too low"));
}
}