mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-27 23:25:53 +00:00
724 lines
26 KiB
C++
724 lines
26 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of clio: https://github.com/XRPLF/clio
|
|
Copyright (c) 2025, the clio developers.
|
|
|
|
Permission to use, copy, modify, and distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#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"));
|
|
}
|
|
}
|