blockchain/tests/unit_tests/pod_array_file_container.cpp

364 lines
10 KiB
C++
Raw Normal View History

// Copyright (c) 2025 Zano Project
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "epee/include/include_base_utils.h"
#include "crypto/crypto.h"
#include "crypto/crypto-sugar.h"
#include "gtest/gtest.h"
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <cstdint>
#include <vector>
#include "common/pod_array_file_container.h"
// helper: returns a unique temp file path
static boost::filesystem::path make_temp_file()
{
return boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("pod_test_%%%%-%%%%.bin");
}
//======================================================================
// typed test fixture for pod_array_file_container<T>
// container = alias for pod_array_file_container<T>
// tmp_path = temp file for test
// SetUp() = generate temp path (SetUp from ::testing::Test)
// TearDown() = remove temp file (TearDown from ::testing::Test)
//======================================================================
template <typename T>
class pod_array_file_typed_test : public ::testing::Test
{
protected:
using container = tools::pod_array_file_container<T>;
boost::filesystem::path tmp_path;
void SetUp() override
{
tmp_path = make_temp_file();
}
void TearDown() override
{
if (boost::filesystem::exists(tmp_path))
boost::filesystem::remove(tmp_path);
}
};
// list of integral types to test
using integral_types = ::testing::Types<
int8_t, uint8_t,
int16_t, uint16_t,
int32_t, uint32_t,
int64_t, uint64_t
>;
// register typed tests
TYPED_TEST_CASE(pod_array_file_typed_test, integral_types);
// push back and get items:
// write values [-1,0,1] and verify they are read back correctly
TYPED_TEST(pod_array_file_typed_test, push_back_and_get_items)
{
typename TestFixture::container c;
ASSERT_TRUE(c.open(this->tmp_path.wstring(), true));
std::vector<TypeParam> values =
{
static_cast<TypeParam>(-1),
0,
static_cast<TypeParam>(1)
};
for (auto v : values)
ASSERT_TRUE(c.push_back(v));
EXPECT_EQ(c.size(), values.size());
TypeParam read_value;
for (size_t i = 0; i < values.size(); ++i)
{
ASSERT_TRUE(c.get_item(i, read_value));
EXPECT_EQ(read_value, values[i]);
}
}
// ensure get_item returns false for index >= size
TYPED_TEST(pod_array_file_typed_test, get_item_out_of_range)
{
typename TestFixture::container c;
ASSERT_TRUE(c.open(this->tmp_path.wstring(), true));
TypeParam dummy;
EXPECT_FALSE(c.get_item(0, dummy));
EXPECT_FALSE(c.get_item(100, dummy));
}
// --------------------------------------------------------------------
typedef uint32_t pod_t;
typedef tools::pod_array_file_container<pod_t> pod_container;
// open fails if not exist without create:
// open() false when file missing and create_if_not_exist=false
TEST(pod_array_file_container, open_fails_if_not_exist_without_create)
{
auto path = make_temp_file();
pod_container c;
std::string reason;
bool opened = c.open(path.wstring(), false, nullptr, &reason);
EXPECT_FALSE(opened);
EXPECT_TRUE(reason.find("not exist") != std::string::npos);
}
// open creates file when not exist:
// open() with create flag creates empty file and size=0
TEST(pod_array_file_container, open_creates_file_when_not_exist)
{
auto path = make_temp_file();
pod_container c;
bool corrupted = true;
ASSERT_TRUE(c.open(path.wstring(), true, &corrupted, nullptr));
EXPECT_FALSE(corrupted);
EXPECT_EQ(c.size(), 0u);
c.close();
EXPECT_TRUE(boost::filesystem::exists(path));
}
// --------------------------------------------------------------------
// POD struct with fixed-size fields
struct test_struct
{
crypto::public_key pubkey;
crypto::key_image key;
};
typedef tools::pod_array_file_container<test_struct> struct_container;
// push back and get items struct:
// write two test_struct and verify memory equality
TEST(pod_array_file_container_struct, push_back_and_get_items_struct)
{
auto path = make_temp_file();
struct_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
test_struct a1{}, a2{};
// use random values for pubkey and key
a1.pubkey = (crypto::scalar_t::random() * crypto::c_point_G).to_public_key();
a1.key = (crypto::scalar_t::random() * crypto::c_point_G).to_key_image();
a2.pubkey = (crypto::scalar_t::random() * crypto::c_point_G).to_public_key();
a2.key = (crypto::scalar_t::random() * crypto::c_point_G).to_key_image();
ASSERT_TRUE(c.push_back(a1));
ASSERT_TRUE(c.push_back(a2));
EXPECT_EQ(c.size(), 2u);
test_struct got;
ASSERT_TRUE(c.get_item(0, got));
EXPECT_EQ(got.pubkey, a1.pubkey);
EXPECT_EQ(got.key, a1.key);
ASSERT_TRUE(c.get_item(1, got));
EXPECT_EQ(got.pubkey, a2.pubkey);
EXPECT_EQ(got.key, a2.key);
}
// get item out of range struct:
// get_item false when no items written
TEST(pod_array_file_container_struct, get_item_out_of_range_struct)
{
auto path = make_temp_file();
struct_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
test_struct dummy;
EXPECT_FALSE(c.get_item(0, dummy));
}
// corrupted file truncation struct:
// simulate corrupted file tail and check truncation flag and size
TEST(pod_array_file_container_struct, corrupted_file_truncation_struct)
{
auto path = make_temp_file();
{
// write a valid test_struct followed by garbage data
boost::filesystem::ofstream out(path, std::ios::binary | std::ios::out);
test_struct tmp{};
std::fill(std::begin(tmp.pubkey.data), std::end(tmp.pubkey.data), 'X');
std::fill(std::begin(tmp.key.data), std::end(tmp.key.data), 'Y');
out.write(reinterpret_cast<char*>(&tmp), sizeof(tmp));
const char garbage[5] = {1,2,3,4,5};
out.write(garbage, sizeof(garbage));
}
struct_container c;
bool corrupted = false;
std::string reason;
ASSERT_TRUE(c.open(path.wstring(), false, &corrupted, &reason));
EXPECT_TRUE(corrupted);
EXPECT_EQ(c.size(), 1u);
test_struct got;
ASSERT_TRUE(c.get_item(0, got));
for (size_t i = 0; i < sizeof(got.pubkey.data); ++i)
EXPECT_EQ(got.pubkey.data[i], 'X');
for (size_t i = 0; i < sizeof(got.key.data); ++i)
EXPECT_EQ(got.key.data[i], 'Y');
EXPECT_TRUE(reason.find("truncated") != std::string::npos);
}
// persistence between opens struct:
// write multiple structs, reopen file, verify data persists
TEST(pod_array_file_container_struct, persistence_between_opens_struct)
{
auto path = make_temp_file();
{
// write 3 test_struct with different pubkey and key values
struct_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
for (int i = 0; i < 3; ++i)
{
test_struct tmp{};
tmp.pubkey.data[0] = '0' + i;
tmp.key.data[0] = 'A' + i;
ASSERT_TRUE(c.push_back(tmp));
}
EXPECT_EQ(c.size(), 3u);
}
// reopen and verify data
struct_container c2;
ASSERT_TRUE(c2.open(path.wstring(), false));
EXPECT_EQ(c2.size(), 3u);
test_struct got;
for (int i = 0; i < 3; ++i)
{
ASSERT_TRUE(c2.get_item(i, got));
EXPECT_EQ(got.pubkey.data[0], '0' + i);
EXPECT_EQ(got.key.data[0], 'A' + i);
}
}
// size bytes and size:
// check size_bytes() matches raw byte count and size() element count
TEST(pod_array_file_container, size_bytes_and_size)
{
auto path = make_temp_file();
pod_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
EXPECT_EQ(c.size_bytes(), 0u);
EXPECT_EQ(c.size(), 0u);
// push one element
pod_t value = 42;
ASSERT_TRUE(c.push_back(value));
EXPECT_EQ(c.size_bytes(), sizeof(pod_t));
EXPECT_EQ(c.size(), 1u);
}
// operations after close:
// ensure push_back and get_item fail after close()
TEST(pod_array_file_container, operations_after_close)
{
auto path = make_temp_file();
pod_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
ASSERT_TRUE(c.push_back(123u));
c.close();
// after close, operations should return false
EXPECT_FALSE(c.push_back(456u));
pod_t dummy = 0;
EXPECT_FALSE(c.get_item(0, dummy));
}
// open fails if cannot open (directory):
// attempt to open a directory path should fail with "file could not be opened"
TEST(pod_array_file_container, open_fails_if_cannot_open)
{
// create a directory instead of a file
auto dir_path = make_temp_file();
boost::filesystem::create_directory(dir_path);
pod_container c;
std::string reason;
bool opened = c.open(dir_path.wstring(), true, nullptr, &reason);
EXPECT_FALSE(opened);
EXPECT_TRUE(reason.find("could not be opened") != std::string::npos);
boost::filesystem::remove(dir_path);
}
// corrupted file truncation uint32:
// simulate corrupted file tail on uint32_t and check truncation
TEST(pod_array_file_container, corrupted_file_truncation_uint32)
{
auto path = make_temp_file();
{
boost::filesystem::ofstream out(path, std::ios::binary | std::ios::out);
pod_t v = 0x12345678u;
out.write(reinterpret_cast<char*>(&v), sizeof(v));
const char junk[3] = {9,8,7};
out.write(junk, sizeof(junk));
}
pod_container c;
bool corrupted = false;
std::string reason;
ASSERT_TRUE(c.open(path.wstring(), false, &corrupted, &reason));
EXPECT_TRUE(corrupted);
EXPECT_EQ(c.size(), 1u);
pod_t read_v;
ASSERT_TRUE(c.get_item(0, read_v));
EXPECT_EQ(read_v, 0x12345678u);
EXPECT_TRUE(reason.find("truncated") != std::string::npos);
}
// operations without open:
// ensure push_back/get_item/size_bytes/size behave when container never opened
TEST(pod_array_file_container, operations_without_open)
{
pod_container c;
EXPECT_FALSE(c.push_back(1u));
pod_t dummy;
EXPECT_FALSE(c.get_item(0, dummy));
EXPECT_EQ(c.size_bytes(), 0u);
EXPECT_EQ(c.size(), 0u);
}
// checks stream state transitions
TEST(pod_array_file_container, is_opened_and_in_good_state)
{
auto path = make_temp_file();
pod_container c;
// not opened yet
EXPECT_FALSE(c.is_opened_and_in_good_state());
// open for create
ASSERT_TRUE(c.open(path.wstring(), true));
EXPECT_TRUE(c.is_opened_and_in_good_state());
// after close
c.close();
EXPECT_FALSE(c.is_opened_and_in_good_state());
}
// wipes file contents and resets size
TEST(pod_array_file_container, clear_resets_file)
{
auto path = make_temp_file();
pod_container c;
ASSERT_TRUE(c.open(path.wstring(), true));
// add some elements
ASSERT_TRUE(c.push_back(123u));
ASSERT_TRUE(c.push_back(456u));
EXPECT_EQ(c.size(), 2u);
// clear the container
ASSERT_TRUE(c.clear());
EXPECT_EQ(c.size(), 0u);
// file should still be usable
ASSERT_TRUE(c.push_back(789u));
EXPECT_EQ(c.size(), 1u);
}