diff --git a/source/centipede/reader/CMakeLists.txt b/source/centipede/reader/CMakeLists.txt index e69de29..4a49077 100644 --- a/source/centipede/reader/CMakeLists.txt +++ b/source/centipede/reader/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( + core + PRIVATE binary.cpp + PUBLIC FILE_SET publicHeaders TYPE HEADERS FILES binary.hpp +) diff --git a/source/centipede/reader/binary.cpp b/source/centipede/reader/binary.cpp new file mode 100644 index 0000000..b5aa65d --- /dev/null +++ b/source/centipede/reader/binary.cpp @@ -0,0 +1,252 @@ +#include "binary.hpp" +#include "centipede/data/entry.hpp" +#include "centipede/util/error_types.hpp" +#include "centipede/util/return_types.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace centipede::reader +{ + namespace srs = std::ranges; + namespace svs = std::views; + namespace + { + template + requires(sizeof(T) == sizeof(uint32_t) and std::is_trivially_copyable_v) + auto read_from_file(std::ifstream& input_file, T& data) -> EnumError + { + const auto read_size = sizeof(data); + // NOLINTBEGIN (cppcoreguidelines-pro-type-reinterpret-cast) + input_file.read(reinterpret_cast(&data), static_cast(read_size)); + // NOLINTEND (cppcoreguidelines-pro-type-reinterpret-cast) + if (input_file.gcount() != static_cast(read_size)) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_read }; + } + return read_size; + } + + template + requires(sizeof(T) == sizeof(uint32_t) and std::is_trivially_copyable_v) + auto read_from_file(std::ifstream& input_file, std::vector& data) -> EnumError + { + assert(!data.empty()); + const auto read_size = data.size() * sizeof(T); + // NOLINTBEGIN (cppcoreguidelines-pro-type-reinterpret-cast) + input_file.read(reinterpret_cast(data.data()), static_cast(read_size)); + // NOLINTEND (cppcoreguidelines-pro-type-reinterpret-cast) + if (input_file.gcount() != static_cast(read_size)) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_read }; + } + return read_size; + } + + template + struct ChunkPointer + { + IterCursor iter; + IterEnd end; + EntryPoint<>* entrypoint; + std::size_t& current_size; + }; + + auto chunk_check_size_one(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + return (srs::size(*(chunk_ptr.iter)) == 1U) ? std::optional{ chunk_ptr } : std::nullopt; + } + + auto chunk_not_end_and_increment(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + return (++(chunk_ptr.iter) != chunk_ptr.end) ? std::optional{ chunk_ptr } : std::nullopt; + } + + auto chunk_handle_measurement(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + (chunk_ptr.entrypoint)->set_measurement(std::get<1>(*(*(chunk_ptr.iter)).begin())); + return chunk_ptr; + } + + auto chunk_handle_globals(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + for (const auto& global : *(chunk_ptr.iter)) + { + chunk_ptr.entrypoint->add_global(std::get<0>(global), std::get<1>(global)); + } + return chunk_ptr; + } + + auto chunk_handle_sigma(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + chunk_ptr.entrypoint->set_sigma(std::get<1>(*(*(chunk_ptr.iter)).begin())); + return chunk_ptr; + } + + auto chunk_handle_locals(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + for (const auto& local : *(chunk_ptr.iter) | svs::values) + { + chunk_ptr.entrypoint->add_local(local); + } + ++(chunk_ptr.current_size); + return chunk_ptr; + } + + auto chunk_end_after_increment(auto chunk_ptr) + { + assert(chunk_ptr.entrypoint != nullptr); + return (++(chunk_ptr.iter) == chunk_ptr.end) ? std::optional{ chunk_ptr } : std::nullopt; + } + + auto parse_entry_points(const Binary::RawBufferType& input, Binary::BufferType& output) + -> EnumError + { + if (input.first.at(0) != 0U) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_read }; + } + constexpr auto chunk_size{ 4 }; + auto size = std::size_t{}; + auto zipped = svs::zip(input.first, input.second) | svs::drop(1) | + svs::chunk_by([](const auto& current, const auto& next) -> auto + { return std::get<0>(current) != 0U and std::get<0>(next) != 0U; }); + if (zipped.begin() == zipped.end()) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_read }; + } + // TODO: Use chunk_view after libc++ supports it. + auto chunks = + svs::zip(svs::iota(0), zipped) | + svs::chunk_by([](const auto& current, const auto& next) -> auto + { return std::get<0>(current) / chunk_size == std::get<0>(next) / chunk_size; }) | + svs::transform([](auto&& chunk) -> auto { return chunk | svs::values; }); + + auto is_ok = srs::all_of(svs::zip_transform( + [&size](auto&& chunk, auto&& entrypoint) -> bool + { + auto chunk_ptr = ChunkPointer{ .iter = chunk.begin(), + .end = chunk.end(), + .entrypoint = &entrypoint, + .current_size = size }; + using ChunkPtrType = decltype(chunk_ptr); + return chunk_check_size_one(chunk_ptr) + .transform(chunk_handle_measurement) + .and_then(chunk_not_end_and_increment) + .transform(chunk_handle_globals) + .and_then(chunk_not_end_and_increment) + .and_then(chunk_check_size_one) + .transform(chunk_handle_sigma) + .and_then(chunk_not_end_and_increment) + .transform(chunk_handle_locals) + .and_then(chunk_end_after_increment) + .has_value(); + }, + chunks, + output), + std::identity{}); + if (not is_ok) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_read }; + } + return size; + } + } // namespace + + auto Binary::init() -> EnumError<> + { + if (config_.in_filename.empty()) + { + return std::unexpected{ ErrorCode::reader_invalid_filename }; + } + entry_buffer_.resize(config_.max_bufferpoint_size); + raw_entry_buffer_.first.reserve(config_.max_bufferpoint_size); + raw_entry_buffer_.second.reserve(config_.max_bufferpoint_size); + input_file_.open(config_.in_filename, std::ios::binary | std::ios::in); + if (!input_file_.is_open()) + { + return std::unexpected{ ErrorCode::reader_file_fail_to_open }; + } + n_entries_ = 0Z; + end_of_file_ = false; + return {}; + } + + auto Binary::read_one_entry() -> EnumError + { + if (entry_buffer_.empty()) + { + return std::unexpected{ ErrorCode::reader_uninitialized }; + } + reset(); + auto read_size = uint32_t{}; + if (auto result = read_from_file(input_file_, read_size); !result) + { + if (input_file_.eof() and input_file_.gcount() == 0) + { + end_of_file_ = true; + return 0U; + } + return std::unexpected{ result.error() }; + } + if (auto result = read_entry_to_buffer(read_size); !result) + { + return std::unexpected{ result.error() }; + } + if (read_size > config_.max_bufferpoint_size) + { + entry_buffer_.resize(read_size); + } + auto size = parse_entry_points(raw_entry_buffer_, entry_buffer_); + if (not size) + { + return std::unexpected{ size.error() }; + } + ++n_entries_; + size_ = size.value(); + return size.value(); + } + + void Binary::reset() + { + for (auto& entrypoint : entry_buffer_) + { + entrypoint.reset(); + } + raw_entry_buffer_.first.clear(); + raw_entry_buffer_.second.clear(); + size_ = 0U; + } + + auto Binary::read_entry_to_buffer(uint32_t read_size) -> EnumError<> + { + raw_entry_buffer_.first.resize(read_size / 2U); + raw_entry_buffer_.second.resize(read_size / 2U); + if (auto result = read_from_file(input_file_, raw_entry_buffer_.second); !result) + { + return std::unexpected{ result.error() }; + } + if (auto result = read_from_file(input_file_, raw_entry_buffer_.first); !result) + { + return std::unexpected{ result.error() }; + } + return {}; + } +} // namespace centipede::reader diff --git a/source/centipede/reader/binary.hpp b/source/centipede/reader/binary.hpp new file mode 100644 index 0000000..2873318 --- /dev/null +++ b/source/centipede/reader/binary.hpp @@ -0,0 +1,333 @@ +#pragma once + +#include "centipede/data/entry.hpp" +#include "centipede/util/common_definitions.hpp" +#include "centipede/util/error_types.hpp" +#include "centipede/util/return_types.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace centipede::reader +{ + /** + * @class Binary + * @brief Class for reading binary files entry-wise. + * + * Data is read from the binary file entry-wise. Each entry contains multiple + * entrypoints of type #centipede::EntryPoint. Before reading, + * #centipede::reader::Binary::init() must be called to open the file and + * initialize the internal buffers. + * + * The reader can be used as an input range. Each iteration reads one entry + * from the file and returns an `EntrySpan` referencing the current entry stored + * in the internal buffers. + * + * Iteration stops automatically once end-of-file is reached or a read/parsing + * error occurs. The final reader state can be queried afterwards via + * #centipede::reader::Binary::get_status(). + * + * Note that manual reading via + * #centipede::reader::Binary::read_one_entry() and + * #centipede::reader::Binary::get_current_entry() is also supported. + * + * Configuration of the class is done via Binary::Config. + * + * #### Example usage + * + * ```cpp + * auto reader = centipede::reader::Binary{ + * centipede::reader::Binary::Config{ .in_filename = "output.bin" } + * }; + * + * auto init_err = reader.init(); + * if (not init_err.has_value()) + * { + * std::println(stderr, "Error: {}", init_err.error()); + * return EXIT_FAILURE; + * } + * + * for (const auto& entry : reader) + * { + * for (const auto& entrypoint : entry) + * { + * // handle entrypoint + * } + * } + * + * if (not reader.is_ok()) + * { + * std::println(stderr, "Error: {}", reader.get_status()); + * } + * + * std::println("N Entries: {}", reader.get_n_entries()); + * ``` + */ + class Binary + { + public: + /** + * @class Config + * @brief Class for configuring the binary writer class (#Binary) + * + * #### Example usage + * + * ```cpp + * auto reader = + * Binary{ Binary::Config{ .in_filename = "output.bin", .max_bufferpoint_size = 1000 } }; + * ``` + * + */ + struct Config + { + std::string in_filename; //!< Input binary filename. + uint32_t max_bufferpoint_size = common::DEFAULT_BUFFER_SIZE; //!< maximum bufferpoint for an entry. + }; + using RawBufferType = std::pair, std::vector>; //!< Type of #raw_entry_buffer_ + using BufferType = std::vector>; //!< Type of #entry_buffer_ + + /** + * @brief Default constructor + */ + Binary() = default; + + /** + * @brief Constructor takes an argument for the configuration. + * + * The config argument will be moved (`std::move`) to its member variable `config_` + * @param config Configuration struct. + * @see Config + */ + constexpr explicit Binary(Config config) + : config_{ std::move(config) } + { + } + + /** + * @brief Initialization. + * + * The initialization function must be called before calling the #read_one_entry() method. When calling this + * function, #raw_entry_buffer_ and #entry_buffer_ get resized and a file is opened with the specified name in + * #Config. + * @return Returns ErrorCode::reader_file_fail_to_open if the file cannot be opened with the name specified by + * Config::in_filename. + * @see Config + */ + [[nodiscard]] auto init() -> EnumError<>; + + /** + * @brief Manually close the input file handler. + * + * This function will be called automatically when the destructor is called. + */ + void close() { input_file_.close(); } + + /** + * @brief Reads one entry from file into the internal buffers. + * + * Reading an entry follows this crude sequence: + * 1. if #init() is not called once after instanciating, returns an error + * 2. Reads one entry to #raw_entry_buffer_, returns if read operation fails. + * 3. Parses entry and stores individual entrypoints in #entry_buffer_, returns if file format is corrupted. + * 4. Increases #n_entries_ for one entry is read and sets #size_ corresponding to the number of 32 Bit values + * (global and local derivs, sigmas and measurements) contained in current entry. + * 5. returns #size_ + * + * @return + * - ErrorCode::reader_uninitialized if #init() is not called before reading + * - ErrorCode::reader_buffer_overflow if buffer size is too small. + * - ErrorCode::reader_file_fail_to_read if the file stream is broken or file format is corrupted. + * - #size_ on success + */ + [[maybe_unused]] auto read_one_entry() -> EnumError; + + /** + * @brief Getter of #entry_buffer_. + * + * @return Returns a std::span of #entry_buffer_ + **/ + [[nodiscard]] auto get_current_entry() const -> auto { return std::span{ entry_buffer_.begin(), size_ }; } + + /** + * @brief Getter of the configuration. + * + * @return Returns a const reference to the member variable #config_. + * @see ref + */ + [[nodiscard]] constexpr auto get_config() const -> const Config& { return config_; } + + /** + * @brief Getter of n_entries_ + * + * @return Total number of entries read by the current instance + */ + [[nodiscard]] constexpr auto get_n_entries() const -> std::size_t { return n_entries_; } + + /** + * @brief Checks if last read operation reached end of file. + * @return Returns true if end of file is reached. + */ + [[nodiscard]] auto is_end_of_file() const -> bool { return end_of_file_; } + + /** + * @brief Returns the current reader status. + * + * The status is updated during iteration and after manual read operations. + * + * @return + * - ErrorCode::invalid while iteration/reading is in progress. + * - ErrorCode::success if iteration finished successfully. + * - Any other ErrorCode if a read or parsing error occurred. + */ + [[nodiscard]] auto get_status() const -> ErrorCode { return status_; } + + /** + * @brief Returns true if the last read was successful. + * + * Note that this while return false on read error as well as incompleted read operation. + */ + [[nodiscard]] auto is_ok() const -> bool { return get_status() == ErrorCode::success; } + + using EntrySpan = std::span; + + /** + * @brief Sentinel type marking the end of a Binary reader range. + * + * The sentinel does not store state itself. End detection is handled by + * Binary::Iterator, which stops once EOF or an empty read is reached. + */ + class Sentinel + { + }; + + /** + * @brief Input iterator for entry-wise reading of a binary file. + * + * The iterator reads entries from the associated Binary reader. On + * construction, the first entry is read. Each increment reads the next entry. + * + * Dereferencing returns an EntrySpan referencing the current entry stored in + * the reader's internal buffer. + * + * Iteration stops automatically once end-of-file is reached or a read error + * occurs. The final reader state can be queried afterwards via + * #Binary::get_status(). + * + * The returned span is valid only until the iterator is incremented, because + * incrementing reads the next entry and resets/reuses the internal buffers. + */ + class Iterator + { + public: + /** + * @brief Constructs an iterator for the given Binary reader. + * + * Construction performs the first read operation. + * + * @param reader_ptr Pointer to the associated Binary reader instance. + */ + explicit Iterator(Binary* reader_ptr) + : reader_{ reader_ptr } + { + reader_->status_ = ErrorCode::invalid; + ++(*this); + } + + using iterator_category = std::input_iterator_tag; //!< Iterator category type. + using difference_type = std::ptrdiff_t; //!< Difference type. + using value_type = EntrySpan; //!< Dereferenced value type. + using reference = const EntrySpan&; //!< Dereference reference type. + + /** + * @brief Dereferences the iterator. + * + * @return Returns the current EntrySpan. + */ + auto operator*() const -> const EntrySpan& { return current_; } + + /** + * @brief Advances the iterator to the next entry. + * + * Reads the next entry from the underlying Binary reader and updates the + * internal state accordingly. + * + * @return Reference to the incremented iterator. + */ + auto operator++() -> Iterator& + { + auto result = reader_->read_one_entry(); + + if (not result.has_value()) + { + reader_->status_ = result.error(); + return *this; + } + + if (reader_->is_end_of_file() or result.value() == 0U) + { + reader_->status_ = ErrorCode::success; + return *this; + } + + current_ = reader_->get_current_entry(); + reader_->status_ = ErrorCode::invalid; + return *this; + } + /** + * @brief Post-increment operator. + * + * @return Copy of the iterator before incrementing. + */ + auto operator++(int) -> Iterator + { + auto tmp = *this; + ++(*this); + return tmp; + } + + /** + * @brief Compares iterator against the end sentinel. + * + * @return Returns true while iteration is not finished. + */ + auto operator!=(const Sentinel&) const -> bool { return reader_->status_ == ErrorCode::invalid; } + + private: + Binary* reader_{}; //!< Associated Binary reader instance. + EntrySpan current_{}; //!< Current iterator value. + }; + + /** + * @brief Returns an iterator positioned at the first readable entry. + * + * @return Iterator initialized with the first entry read from file. + */ + auto begin() -> Iterator { return Iterator{ this }; } + + /** + * @brief Returns the end sentinel of the Binary reader range. + * + * @return Sentinel representing the end of the range. + */ + auto end() const -> Sentinel { return Sentinel{}; } + + private: + BufferType entry_buffer_; //!< A vector containing all entrypoints of the current entry. + RawBufferType raw_entry_buffer_; //!< A buffer to store raw data coming from file stream. + Config config_; //!< Member variable for the configuration. + std::ifstream input_file_; //!< Input file handler + std::size_t size_{}; //!< Number of Entrypoints in the current entry + std::size_t n_entries_{}; //!< Total number of entries read by this instance + bool end_of_file_{ false }; //!< Indicates if end of file is reached. Gets updated on read. + ErrorCode status_{ ErrorCode::invalid }; + + void reset(); + auto read_entry_to_buffer(uint32_t read_size) -> EnumError<>; + }; +} // namespace centipede::reader diff --git a/source/centipede/util/common_definitions.hpp b/source/centipede/util/common_definitions.hpp new file mode 100644 index 0000000..1854c25 --- /dev/null +++ b/source/centipede/util/common_definitions.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace centipede::common +{ + constexpr auto DEFAULT_BUFFER_SIZE = + std::size_t{ 10000 }; //!< Default maximum buffer size for binary readers/writers. +} // namespace centipede::common diff --git a/source/centipede/util/error_types.hpp b/source/centipede/util/error_types.hpp index 7b4c746..8101044 100644 --- a/source/centipede/util/error_types.hpp +++ b/source/centipede/util/error_types.hpp @@ -28,6 +28,11 @@ namespace centipede analysis_global_negative_definite, analysis_factor_matrix_zero, //!< Global factor matrix is zero matrix. analysis_rhs_vector_zero, //!< Global right-hand-side vector is zero vector. + reader_file_fail_to_open, //!< Input file failed to be open. + reader_file_fail_to_read, //!< Input file failed to read + reader_uninitialized, //!< Reader is not initialized. + reader_buffer_overflow, //!< Buffer size is too small for a new entry occurs. See @ref reader::Binary. + reader_invalid_filename, //!< Filename is invalid or empty }; } // namespace centipede @@ -82,6 +87,16 @@ struct std::formatter return std::format_to(ctx.out(), "Global factor matrix is zero matrix."); case analysis_rhs_vector_zero: return std::format_to(ctx.out(), "Global right-hand-side vector is zero vector."); + case reader_file_fail_to_open: + return std::format_to(ctx.out(), "Reader: Failed to open the file."); + case reader_uninitialized: + return std::format_to(ctx.out(), "Reader: Must be initialized beforehand!"); + case reader_file_fail_to_read: + return std::format_to(ctx.out(), "Reader: Failed to read the file."); + case reader_buffer_overflow: + return std::format_to(ctx.out(), "Reader: Cannot read the file. Buffer size will be exceeded!"); + case reader_invalid_filename: + return std::format_to(ctx.out(), "Reader: Filename is either empty or invalid!"); case invalid: return std::format_to(ctx.out(), "Error due to no evaluation!"); default: diff --git a/source/centipede/util/namespaces.hpp b/source/centipede/util/namespaces.hpp index 56f037a..945787b 100644 --- a/source/centipede/util/namespaces.hpp +++ b/source/centipede/util/namespaces.hpp @@ -29,3 +29,17 @@ namespace centipede::core::engine namespace centipede::writer { } + +/** + * @brief Components of data input from files + */ +namespace centipede::reader +{ +} + +/** + * @brief Common constants and shared utility definitions. + */ +namespace centipede::common +{ +} diff --git a/source/centipede/writer/binary.hpp b/source/centipede/writer/binary.hpp index 002919a..d5550c8 100644 --- a/source/centipede/writer/binary.hpp +++ b/source/centipede/writer/binary.hpp @@ -1,6 +1,7 @@ #pragma once #include "centipede/data/entry.hpp" +#include "centipede/util/common_definitions.hpp" #include "centipede/util/error_types.hpp" #include "centipede/util/return_types.hpp" @@ -24,7 +25,8 @@ namespace centipede::writer * handler is opened and internal buffer resetted, ready for the next data input. When adding each entrypoint, the * writer doesn't write the corresponding data to the binary file, but rather pushes the data to its internal buffer * (see @ref Binary::data_buffer_). Data is only written to the binary file after calling - * #Binary::write_current_entry(). All entrypoints added before this call are grouped into the same entry. + * #centipede::writer::Binary::write_current_entry(). All entrypoints added before this call are grouped into the + * same entry. * * Configuration of the class is done via the Binary::Config struct. * @@ -76,10 +78,8 @@ namespace centipede::writer */ struct Config { - static constexpr auto DEFAULT_BUFFER_SIZE = - std::size_t{ 10000 }; //!< Default vector capacities for both vectors in #Binary::BufferType. - std::string out_filename = "output.bin"; //!< Output binary filename. - uint32_t max_bufferpoint_size = DEFAULT_BUFFER_SIZE; //!< maximum bufferpoint for an entry. + std::string out_filename = "output.bin"; //!< Output binary filename. + uint32_t max_bufferpoint_size = common::DEFAULT_BUFFER_SIZE; //!< maximum bufferpoint for an entry. }; using BufferType = std::pair, std::vector>; //!< Type of the #data_buffer_. diff --git a/test/integration_tests/CMakeLists.txt b/test/integration_tests/CMakeLists.txt index 782365a..b67ea9e 100644 --- a/test/integration_tests/CMakeLists.txt +++ b/test/integration_tests/CMakeLists.txt @@ -33,3 +33,16 @@ set_tests_properties( integration_test_writer_res PROPERTIES DEPENDS integration_test_writer_data_gen ) + +add_executable(integration_test_reader test_reader.cpp) + +add_test(NAME integration_test_reader_data_read COMMAND integration_test_reader) + +target_link_libraries(integration_test_reader PRIVATE centipede::centipede) + +target_compile_options(integration_test_reader PRIVATE ${COMPILE_OPTIONS}) + +set_tests_properties( + integration_test_reader_data_read + PROPERTIES DEPENDS integration_test_writer_data_gen +) diff --git a/test/integration_tests/test_reader.cpp b/test/integration_tests/test_reader.cpp new file mode 100644 index 0000000..e3ba3fd --- /dev/null +++ b/test/integration_tests/test_reader.cpp @@ -0,0 +1,32 @@ +#include "centipede/reader/binary.hpp" +#include +#include + +auto main() -> int +{ + auto reader = centipede::reader::Binary{ centipede::reader::Binary::Config{ .in_filename = "output.bin" } }; + auto init_err = reader.init(); + if (not init_err.has_value()) + { + std::println(stderr, "Error: {}", init_err.error()); + return EXIT_FAILURE; + } + + for ([[maybe_unused]] const auto& entry : reader) + { + } + + if (not reader.is_ok()) + { + std::println(stderr, "Error: {}", reader.get_status()); + return EXIT_FAILURE; + } + + if (reader.get_n_entries() == 0U) + { + std::println(stderr, "Error: no entries read"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt index 60cf565..8687b9f 100644 --- a/test/unit_tests/CMakeLists.txt +++ b/test/unit_tests/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources( test_binary_writer.cpp test_formatter.cpp test_eigen_engine.cpp + test_binary_reader.cpp test_entry.cpp test_handler.cpp test_master_engine.cpp diff --git a/test/unit_tests/test_binary_reader.cpp b/test/unit_tests/test_binary_reader.cpp new file mode 100644 index 0000000..8b61763 --- /dev/null +++ b/test/unit_tests/test_binary_reader.cpp @@ -0,0 +1,436 @@ +#include "centipede/centipede.hpp" +#include "centipede/reader/binary.hpp" +#include "centipede/util/error_types.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using centipede::reader::Binary; +using Config = centipede::reader::Binary::Config; +namespace fs = std::filesystem; + +namespace centipede::test +{ + + TEST(reader, constructor) + { + auto reader = Binary{ Config{ .in_filename = "binary_reader_constructor.bin" } }; + EXPECT_FALSE(fs::exists(reader.get_config().in_filename)); + } + + TEST(reader, init) + { + auto file_name = std::string{ "binary_reader_init.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto error = reader.init(); + EXPECT_TRUE(error.has_value()); + } + + TEST(reader, init_empty_file_name_error) + { + auto reader = Binary{ Config{ .in_filename = "" } }; + auto error = reader.init(); + EXPECT_TRUE(not error.has_value()); + EXPECT_EQ(error.error(), ErrorCode::reader_invalid_filename); + reader = Binary{ Config{ .in_filename = "nonexistent.bin" } }; + error = reader.init(); + EXPECT_EQ(error.error(), ErrorCode::reader_file_fail_to_open); + } + + TEST(reader, init_nonexisting_file_error) + { + auto reader = Binary{ Config{ .in_filename = "nonexistent.bin" } }; + auto error = reader.init(); + EXPECT_TRUE(not error.has_value()); + EXPECT_EQ(error.error(), ErrorCode::reader_file_fail_to_open); + } + + TEST(reader, not_initialized) + { + auto file_name = std::string{ "not_init.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto read_err = reader.read_one_entry(); + EXPECT_FALSE(read_err); + EXPECT_EQ(read_err.error(), ErrorCode::reader_uninitialized); + } + + TEST(reader, file_invalid_idx_size) + { + auto file_name = std::string{ "file_invalid_idx_size.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + constexpr auto declared_entry_size = uint32_t{ 2 }; + constexpr auto dummy_data = uint32_t{ 1 }; + // NOLINTBEGIN (cppcoreguidelines-pro-type-reinterpret-cast) + file.write(reinterpret_cast(&declared_entry_size), sizeof(declared_entry_size)); + file.write(reinterpret_cast(&dummy_data), sizeof(dummy_data)); + // NOLINTEND (cppcoreguidelines-pro-type-reinterpret-cast) + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + auto read_err = reader.read_one_entry(); + EXPECT_FALSE(read_err); + EXPECT_EQ(read_err.error(), ErrorCode::reader_file_fail_to_read); + } + + TEST(reader, file_invalid_val_size) + { + auto file_name = std::string{ "file_invalid_val_size.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + constexpr auto declared_entry_size = uint32_t{ 2 }; + // NOLINTBEGIN (cppcoreguidelines-pro-type-reinterpret-cast) + file.write(reinterpret_cast(&declared_entry_size), sizeof(declared_entry_size)); + // NOLINTEND (cppcoreguidelines-pro-type-reinterpret-cast) + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + auto read_err = reader.read_one_entry(); + EXPECT_FALSE(read_err); + EXPECT_EQ(read_err.error(), ErrorCode::reader_file_fail_to_read); + } + + namespace + { + // NOLINTBEGIN + // (cppcoreguidelines-avoid-magic-numbers) + auto valid_measurement = float{ 1. }; + auto valid_sigma = float{ 1. }; + auto valid_locals_data = Binary::RawBufferType{ { 1, 2, 3 }, { 1.F, 2.F, 3.F } }; + auto valid_globals_data = Binary::RawBufferType{ { 3, 4, 5 }, { 3.F, 4.F, 5.F } }; + // NOLINTEND + // (cppcoreguidelines-avoid-magic-numbers) + + auto fill_buffer(Binary::RawBufferType& output, + const float measurement, + const Binary::RawBufferType& locals_data, + const float sigma, + const Binary::RawBufferType& globals_data) + { + output.first.push_back(uint32_t{ 0 }); + output.second.push_back(measurement); + std::ranges::copy(globals_data.first, std::back_inserter(output.first)); + std::ranges::copy(globals_data.second, std::back_inserter(output.second)); + + output.first.push_back(uint32_t{ 0 }); + output.second.push_back(sigma); + std::ranges::copy(locals_data.first, std::back_inserter(output.first)); + std::ranges::copy(locals_data.second, std::back_inserter(output.second)); + } + + auto write_to_file(std::ofstream& file, const Binary::RawBufferType& buffer) + { + auto entry_size = static_cast(buffer.first.size() + buffer.second.size()); + // NOLINTBEGIN (cppcoreguidelines-pro-type-reinterpret-cast) + file.write(reinterpret_cast(&entry_size), sizeof(entry_size)); + file.write(reinterpret_cast(buffer.second.data()), buffer.second.size() * sizeof(float)); + file.write(reinterpret_cast(buffer.first.data()), buffer.first.size() * sizeof(uint32_t)); + // NOLINTEND (cppcoreguidelines-pro-type-reinterpret-cast) + } + } // namespace + + TEST(reader, valid_single_entry) + { + // NOLINTBEGIN(readability-function-cognitive-complexity) + auto file_name = std::string{ "valid_single_entry.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for (const auto& entry : reader) + { + ASSERT_FALSE(entry.empty()); + for (const auto& entrypoint : entry) + { + EXPECT_EQ(valid_locals_data.second, entrypoint.get_locals()); + auto expected_globals = std::views::zip_transform([](const auto& index, const auto& value) -> auto + { return std::pair{ index, value }; }, + valid_globals_data.first, + valid_globals_data.second) | + std::ranges::to(); + EXPECT_EQ(expected_globals, entrypoint.get_globals()); + EXPECT_EQ(entrypoint.get_measurement(), valid_measurement); + EXPECT_EQ(entrypoint.get_sigma(), valid_sigma); + } + } + EXPECT_TRUE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::success); + reader.close(); + // NOLINTEND(readability-function-cognitive-complexity) + } + + TEST(reader, reset) + { + // NOLINTBEGIN(readability-function-cognitive-complexity) + auto file_name = std::string{ "reader_reset.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + auto read_err = reader.read_one_entry(); + EXPECT_TRUE(read_err); + + for (const auto& entry : reader) + { + for (const auto& entrypoint : entry) + { + EXPECT_TRUE(entrypoint.get_globals().empty()); + EXPECT_TRUE(entrypoint.get_locals().empty()); + EXPECT_EQ(entrypoint.get_measurement(), 0U); + EXPECT_EQ(entrypoint.get_sigma(), 0U); + } + } + EXPECT_TRUE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::success); + reader.close(); + // NOLINTEND(readability-function-cognitive-complexity) + } + + TEST(reader, invalid_zero_len_entry) + { + + auto file_name = std::string{ "reader_invalid_zero_len_entry.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_one_len_entry) + { + + auto file_name = std::string{ "reader_invalid_one_len_entry.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_two_len_entry) + { + + auto file_name = std::string{ "reader_invalid_two_len_entry.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_three_len_entry) + { + + auto file_name = std::string{ "reader_invalid_three_len_entry.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_file_begin) + { + auto file_name = std::string{ "reader_invalid_file_begin.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 1 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_measurement_chunk) + { + + auto file_name = std::string{ "invalid_measurement.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + output_buffer.first.at(1) = 1U; + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, invalid_sigma_chunk) + { + auto file_name = std::string{ "invalid_sigma.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + // NOLINTBEGIN + // (cppcoreguidelines-avoid-magic-numbers) + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 1 }); + + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 2 }); + + output_buffer.first.push_back(uint32_t{ 1 }); + output_buffer.second.push_back(float{ 3 }); + + output_buffer.first.push_back(uint32_t{ 2 }); + output_buffer.second.push_back(float{ 4 }); + + output_buffer.first.push_back(uint32_t{ 0 }); + output_buffer.second.push_back(float{ 5 }); + // NOLINTEND + // (cppcoreguidelines-avoid-magic-numbers) + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } + + TEST(reader, file_resize_buffer) + { + auto file_name = std::string{ "file_resize_buffer.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name, .max_bufferpoint_size = 1U } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + auto read_err = reader.read_one_entry(); + EXPECT_TRUE(read_err); + } + + TEST(reader, end_of_file) + { + auto file_name = std::string{ "reader_end_of_file.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + auto output_buffer = Binary::RawBufferType{ { uint32_t{ 0 } }, { 0.F } }; + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + fill_buffer(output_buffer, valid_measurement, valid_locals_data, valid_sigma, valid_globals_data); + write_to_file(file, output_buffer); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + EXPECT_TRUE(init_err); + auto read_err = reader.read_one_entry(); + EXPECT_TRUE(read_err); + read_err = reader.read_one_entry(); + EXPECT_TRUE(read_err); + EXPECT_TRUE(reader.is_end_of_file()); + EXPECT_EQ(read_err.value(), 0U); + } + + TEST(reader, incomplete_header_error) + { + auto file_name = std::string{ "reader_incomplete_header_error.bin" }; + auto file = std::ofstream{ file_name, std::ios::out | std::ios::binary | std::ios::trunc }; + + constexpr auto invalid_header = char{ 42 }; + file.write(&invalid_header, 1); + file.close(); + auto reader = Binary{ Config{ .in_filename = file_name } }; + auto init_err = reader.init(); + ASSERT_TRUE(init_err); + for ([[maybe_unused]] const auto& entry : reader) + { + } + EXPECT_EQ(reader.get_n_entries(), 0U); + EXPECT_FALSE(reader.is_ok()); + EXPECT_EQ(reader.get_status(), ErrorCode::reader_file_fail_to_read); + reader.close(); + } +} // namespace centipede::test