mirror of
https://github.com/XRPLF/clio.git
synced 2025-12-06 17:27:58 +00:00
196
tests/unit/data/impl/InputFileTests.cpp
Normal file
196
tests/unit/data/impl/InputFileTests.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/impl/InputFile.hpp"
|
||||
#include "util/Shasum.hpp"
|
||||
#include "util/TmpFile.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace data::impl;
|
||||
|
||||
struct InputFileTest : ::testing::Test {};
|
||||
|
||||
TEST_F(InputFileTest, ConstructorWithValidFile)
|
||||
{
|
||||
auto const tmpFile = TmpFile{"Hello, World!"};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
EXPECT_TRUE(inputFile.isOpen());
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ConstructorWithInvalidFile)
|
||||
{
|
||||
InputFile inputFile("/nonexistent/path/file.txt");
|
||||
|
||||
EXPECT_FALSE(inputFile.isOpen());
|
||||
|
||||
char i = 0;
|
||||
EXPECT_FALSE(inputFile.read(i));
|
||||
EXPECT_FALSE(inputFile.readRaw(&i, 1));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadRawFromFile)
|
||||
{
|
||||
std::string const content = "Test content for reading";
|
||||
auto tmpFile = TmpFile{content};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::vector<char> buffer(content.size());
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
EXPECT_EQ(std::string(buffer.data(), buffer.size()), content);
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadRawFromFilePartial)
|
||||
{
|
||||
std::string content = "Hello, World!";
|
||||
auto tmpFile = TmpFile{content};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::vector<char> buffer(3);
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
EXPECT_EQ(std::string(buffer.data(), buffer.size()), "Hel"); // codespell:ignore
|
||||
|
||||
buffer.resize(6);
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
EXPECT_EQ(std::string(buffer.data(), buffer.size()), "lo, Wo");
|
||||
|
||||
buffer.resize(4);
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
EXPECT_EQ(std::string(buffer.data(), buffer.size()), "rld!");
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadRawAfterEnd)
|
||||
{
|
||||
std::string content = "Test";
|
||||
auto tmpFile = TmpFile{content};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::vector<char> buffer(content.size());
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
|
||||
char extraByte = 0;
|
||||
EXPECT_FALSE(inputFile.readRaw(&extraByte, 1));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadRawFromFileExceedsSize)
|
||||
{
|
||||
auto tmpFile = TmpFile{"Test"};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::vector<char> buffer(10); // Larger than file content
|
||||
EXPECT_FALSE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadTemplateMethod)
|
||||
{
|
||||
auto tmpFile = TmpFile{"\x01\x02\x03\x04"};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::uint32_t value{0};
|
||||
bool success = inputFile.read(value);
|
||||
|
||||
EXPECT_TRUE(success);
|
||||
// Note: The actual value depends on endianness
|
||||
EXPECT_NE(value, 0u);
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadTemplateMethodFailure)
|
||||
{
|
||||
auto tmpFile = TmpFile{"Hi"}; // Only 2 bytes
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
std::uint64_t value{0}; // Trying to read 8 bytes
|
||||
EXPECT_FALSE(inputFile.read(value));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, ReadFromEmptyFile)
|
||||
{
|
||||
auto tmpFile = TmpFile::empty();
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
char byte = 0;
|
||||
EXPECT_FALSE(inputFile.readRaw(&byte, 1));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, HashOfEmptyFile)
|
||||
{
|
||||
auto tmpFile = TmpFile::empty();
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum(""));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, HashAfterReading)
|
||||
{
|
||||
std::string const content = "Hello, World!";
|
||||
auto tmpFile = TmpFile{content};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum(""));
|
||||
|
||||
std::vector<char> buffer(content.size());
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer.data(), buffer.size()));
|
||||
EXPECT_EQ(std::string(buffer.data(), buffer.size()), content);
|
||||
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum(content));
|
||||
}
|
||||
|
||||
TEST_F(InputFileTest, HashProgressesWithReading)
|
||||
{
|
||||
std::string const content = "Hello, World!";
|
||||
auto tmpFile = TmpFile{content};
|
||||
InputFile inputFile(tmpFile.path);
|
||||
|
||||
ASSERT_TRUE(inputFile.isOpen());
|
||||
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum(""));
|
||||
|
||||
// Read first part
|
||||
std::vector<char> buffer1(5);
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer1.data(), buffer1.size()));
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum("Hello"));
|
||||
|
||||
// Read second part
|
||||
std::vector<char> buffer2(8);
|
||||
EXPECT_TRUE(inputFile.readRaw(buffer2.data(), buffer2.size()));
|
||||
EXPECT_EQ(inputFile.hash(), util::sha256sum(content));
|
||||
}
|
||||
723
tests/unit/data/impl/LedgerCacheFileTests.cpp
Normal file
723
tests/unit/data/impl/LedgerCacheFileTests.cpp
Normal file
@@ -0,0 +1,723 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 corruptByte = static_cast<char>(0xFF);
|
||||
file.write(&corruptByte, 1);
|
||||
}
|
||||
break;
|
||||
case CorruptionType::CorruptedSeparator2:
|
||||
file.seekp(offsets.separator2Offset);
|
||||
{
|
||||
char corruptByte = static_cast<char>(0xFF);
|
||||
file.write(&corruptByte, 1);
|
||||
}
|
||||
break;
|
||||
case CorruptionType::CorruptedSeparator3:
|
||||
file.seekp(offsets.separator3Offset);
|
||||
{
|
||||
char corruptByte = static_cast<char>(0xFF);
|
||||
file.write(&corruptByte, 1);
|
||||
}
|
||||
break;
|
||||
case CorruptionType::MapKeyCorrupted:
|
||||
if (!offsets.mapEntries.empty()) {
|
||||
file.seekp(offsets.mapEntries[0].keyOffset);
|
||||
char 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 corruptByte = static_cast<char>(0xFF);
|
||||
file.write(&corruptByte, 1);
|
||||
}
|
||||
break;
|
||||
case CorruptionType::DeletedKeyCorrupted:
|
||||
if (!offsets.deletedEntries.empty()) {
|
||||
file.seekp(offsets.deletedEntries[0].keyOffset);
|
||||
char 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 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"));
|
||||
}
|
||||
}
|
||||
169
tests/unit/data/impl/OutputFileTests.cpp
Normal file
169
tests/unit/data/impl/OutputFileTests.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/impl/OutputFile.hpp"
|
||||
#include "util/Shasum.hpp"
|
||||
#include "util/TmpFile.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <numbers>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace data::impl;
|
||||
|
||||
struct OutputFileTest : ::testing::Test {
|
||||
TmpFile tmpFile = TmpFile::empty();
|
||||
|
||||
std::string
|
||||
readFileContents() const
|
||||
{
|
||||
std::ifstream ifs(tmpFile.path, std::ios::binary);
|
||||
return std::string{std::istreambuf_iterator<char>{ifs}, std::istreambuf_iterator<char>{}};
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(OutputFileTest, ConstructorOpensFile)
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
EXPECT_TRUE(file.isOpen());
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, NonExistingFile)
|
||||
{
|
||||
std::string const invalidPath = "/invalid/nonexistent/directory/file.dat";
|
||||
OutputFile file(invalidPath);
|
||||
EXPECT_FALSE(file.isOpen());
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, WriteBasicTypes)
|
||||
{
|
||||
uint32_t const intValue = 0x12345678;
|
||||
double const doubleValue = std::numbers::pi;
|
||||
char const charValue = 'A';
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
|
||||
file.write(intValue);
|
||||
file.write(doubleValue);
|
||||
file.write(charValue);
|
||||
}
|
||||
|
||||
std::string contents = readFileContents();
|
||||
EXPECT_EQ(contents.size(), sizeof(intValue) + sizeof(doubleValue) + sizeof(charValue));
|
||||
|
||||
auto* data = reinterpret_cast<char const*>(contents.data());
|
||||
EXPECT_EQ(*reinterpret_cast<uint32_t const*>(data), intValue);
|
||||
EXPECT_EQ(*reinterpret_cast<double const*>(data + sizeof(intValue)), doubleValue);
|
||||
EXPECT_EQ(*(data + sizeof(intValue) + sizeof(doubleValue)), charValue);
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, WriteArray)
|
||||
{
|
||||
std::vector<uint32_t> const data = {0x11111111, 0x22222222, 0x33333333, 0x44444444};
|
||||
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
file.write(data.data(), data.size() * sizeof(uint32_t));
|
||||
}
|
||||
|
||||
std::string contents = readFileContents();
|
||||
EXPECT_EQ(contents.size(), data.size() * sizeof(uint32_t));
|
||||
|
||||
auto* readData = reinterpret_cast<uint32_t const*>(contents.data());
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
EXPECT_EQ(readData[i], data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, WriteRawData)
|
||||
{
|
||||
std::string const testData = "Hello, World!";
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
file.writeRaw(testData.data(), testData.size());
|
||||
}
|
||||
|
||||
std::string contents = readFileContents();
|
||||
EXPECT_EQ(contents, testData);
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, WriteMultipleChunks)
|
||||
{
|
||||
std::string chunk1 = "First chunk";
|
||||
std::string chunk2 = "Second chunk";
|
||||
std::string chunk3 = "Third chunk";
|
||||
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
|
||||
file.writeRaw(chunk1.data(), chunk1.size());
|
||||
file.writeRaw(chunk2.data(), chunk2.size());
|
||||
file.writeRaw(chunk3.data(), chunk3.size());
|
||||
}
|
||||
|
||||
std::string contents = readFileContents();
|
||||
EXPECT_EQ(contents, chunk1 + chunk2 + chunk3);
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, HashOfEmptyFile)
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
ASSERT_TRUE(file.isOpen());
|
||||
|
||||
// Hash of empty file should match SHA256 of empty string
|
||||
EXPECT_EQ(file.hash(), util::sha256sum(""));
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, HashAfterWriting)
|
||||
{
|
||||
std::string const testData = "Hello, World!";
|
||||
{
|
||||
OutputFile file(tmpFile.path);
|
||||
file.writeRaw(testData.data(), testData.size());
|
||||
|
||||
// Hash should match SHA256 of the written data
|
||||
EXPECT_EQ(file.hash(), util::sha256sum(testData));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(OutputFileTest, HashProgressesWithWrites)
|
||||
{
|
||||
std::string const part1 = "Hello, ";
|
||||
std::string const part2 = "World!";
|
||||
std::string const combined = part1 + part2;
|
||||
|
||||
OutputFile file(tmpFile.path);
|
||||
ASSERT_TRUE(file.isOpen());
|
||||
|
||||
EXPECT_EQ(file.hash(), util::sha256sum(""));
|
||||
|
||||
file.writeRaw(part1.data(), part1.size());
|
||||
EXPECT_EQ(file.hash(), util::sha256sum(part1));
|
||||
|
||||
file.writeRaw(part2.data(), part2.size());
|
||||
EXPECT_EQ(file.hash(), util::sha256sum(combined));
|
||||
}
|
||||
Reference in New Issue
Block a user