From 18dfa2d0d731f9869cd4c904e083ef96d9ad8f57 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 18 Mar 2026 12:05:57 -0300 Subject: [PATCH 01/10] Add compare_files implementation --- include/quiver/binary/binary.h | 13 +++ include/quiver/binary/binary_metadata.h | 2 + include/quiver/binary/dimension.h | 2 + include/quiver/binary/time_properties.h | 2 + src/binary/binary.cpp | 139 ++++++++++++++++++++++++ 5 files changed, 158 insertions(+) diff --git a/include/quiver/binary/binary.h b/include/quiver/binary/binary.h index b20a19d0..b477b976 100644 --- a/include/quiver/binary/binary.h +++ b/include/quiver/binary/binary.h @@ -14,6 +14,13 @@ namespace quiver { +enum class CompareStatus { FileMatch, MetadataMismatch, DataMismatch }; + +struct CompareResult { + CompareStatus status; + std::string report; +}; + class QUIVER_API Binary { public: explicit Binary(const std::string& file_path, const BinaryMetadata& metadata, std::unique_ptr io); @@ -30,6 +37,8 @@ class QUIVER_API Binary { // File handling static Binary open_file(const std::string& file_path, char mode, const std::optional& metadata = {}); + static CompareResult + compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report = true); // Data handling std::vector read(const std::unordered_map& dims, bool allow_nulls = false); @@ -48,6 +57,10 @@ class QUIVER_API Binary { void go_to_position(int64_t position, char mode); void fill_file_with_nulls(); + // Reporting + static std::string build_data_report(const std::string& file_path1, const std::string& file_path2, Binary& binary1, + Binary& binary2); + // Validations void validate_file_is_open() const; void validate_dimension_values(const std::unordered_map& dims); diff --git a/include/quiver/binary/binary_metadata.h b/include/quiver/binary/binary_metadata.h index f1adc823..0c4367b5 100644 --- a/include/quiver/binary/binary_metadata.h +++ b/include/quiver/binary/binary_metadata.h @@ -21,6 +21,8 @@ struct QUIVER_API BinaryMetadata { // int64_t number_of_time_dimensions = 0; + bool operator==(const BinaryMetadata& other) const = default; + BinaryMetadata(); ~BinaryMetadata(); diff --git a/include/quiver/binary/dimension.h b/include/quiver/binary/dimension.h index e3349d71..884015ac 100644 --- a/include/quiver/binary/dimension.h +++ b/include/quiver/binary/dimension.h @@ -16,6 +16,8 @@ struct Dimension { int64_t size; std::optional time; + bool operator==(const Dimension& other) const = default; + bool is_time_dimension() const { return time.has_value(); } }; diff --git a/include/quiver/binary/time_properties.h b/include/quiver/binary/time_properties.h index e4dd2b3e..5394e88c 100644 --- a/include/quiver/binary/time_properties.h +++ b/include/quiver/binary/time_properties.h @@ -26,6 +26,8 @@ struct QUIVER_API TimeProperties { void set_initial_value(int64_t initial_value); void set_parent_dimension_index(int64_t parent_dimension_index); + bool operator==(const TimeProperties& other) const = default; + int64_t datetime_to_int(std::chrono::system_clock::time_point datetime) const; std::chrono::system_clock::time_point add_offset_from_int(std::chrono::system_clock::time_point base_datetime, int64_t value) const; diff --git a/src/binary/binary.cpp b/src/binary/binary.cpp index 0296d7a3..98aeaae5 100644 --- a/src/binary/binary.cpp +++ b/src/binary/binary.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -349,6 +350,144 @@ void Binary::fill_file_with_nulls() { } } +std::string Binary::build_data_report(const std::string& file_path1, + const std::string& file_path2, + Binary& binary1, + Binary& binary2) { + constexpr int MAX_REPORT_LINES = 5; + + const auto& metadata = binary1.get_metadata(); + const auto& dimensions = metadata.dimensions; + + // Get initial dimension values + std::vector initial_dimensions; + for (const auto& dim : dimensions) { + if (dim.is_time_dimension()) { + initial_dimensions.push_back(dim.time->initial_value); + } else { + initial_dimensions.push_back(1); + } + } + + // Calculate maximum number of lines + int64_t max_lines = 1; + for (const auto& dim : dimensions) { + max_lines *= dim.size; + } + + std::string report = "Data mismatch between '" + file_path1 + "' and '" + file_path2 + "':\n"; + int report_lines = 0; + auto current_dims = initial_dimensions; + for (int64_t i = 0; i < max_lines; ++i) { + std::unordered_map dims; + for (size_t j = 0; j < dimensions.size(); ++j) { + dims[dimensions[j].name] = current_dims[j]; + } + + auto data1 = binary1.read(dims, true); + auto data2 = binary2.read(dims, true); + + for (size_t k = 0; k < data1.size(); ++k) { + bool nan1 = std::isnan(data1[k]); + bool nan2 = std::isnan(data2[k]); + if (nan1 != nan2 || (!nan1 && data1[k] != data2[k])) { + // Build dimension string + std::string dim_str; + for (size_t j = 0; j < dimensions.size(); ++j) { + if (!dim_str.empty()) + dim_str += ", "; + dim_str += dimensions[j].name + "=" + std::to_string(current_dims[j]); + } + + // Build data string + std::string data_str = "'" + metadata.labels[k] + "': "; + if (nan1) { + data_str += "NaN"; + } else { + data_str += std::to_string(data1[k]); + } + data_str += " vs "; + if (nan2) { + data_str += "NaN"; + } else { + data_str += std::to_string(data2[k]); + } + + // Append to report + report += " {" + dim_str + "}, " + data_str + "\n"; + report_lines++; + if (report_lines >= MAX_REPORT_LINES) { + report += " more ...\n"; + return report; + } + } + } + + current_dims = binary1.next_dimensions(current_dims); + if (current_dims == initial_dimensions) + break; + } + + return report; +} + +CompareResult +Binary::compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report) { + auto binary1 = open_file(file_path1, 'r'); + auto binary2 = open_file(file_path2, 'r'); + std::string report; + + const auto& metadata1 = binary1.get_metadata(); + const auto& metadata2 = binary2.get_metadata(); + + if (metadata1 != metadata2) { + if (detailed_report) { + report = "Metadata mismatch between files '" + file_path1 + "' and '" + file_path2 + "'.\n"; + } + return {CompareStatus::MetadataMismatch, report}; + } + + // Fast comparison via raw binary data + auto& io1 = binary1.get_io(); + auto& io2 = binary2.get_io(); + io1.seekg(0); + io2.seekg(0); + + CompareStatus status = CompareStatus::FileMatch; + constexpr size_t BUFFER_SIZE = 8192; // 8 KB chunks for raw binary comparison + char buf1[BUFFER_SIZE]; + char buf2[BUFFER_SIZE]; + + while (io1.good() && io2.good()) { + io1.read(buf1, BUFFER_SIZE); + io2.read(buf2, BUFFER_SIZE); + + auto bytes_read1 = io1.gcount(); + auto bytes_read2 = io2.gcount(); + + if (bytes_read1 != bytes_read2 || std::memcmp(buf1, buf2, bytes_read1) != 0) { + status = CompareStatus::DataMismatch; + break; + } + + if (bytes_read1 == 0) + break; + } + + if (detailed_report) { + if (status == CompareStatus::DataMismatch) { + // Clear stream state flags (eofbit/failbit) set by the memcmp loop + io1.clear(); + io2.clear(); + report = build_data_report(file_path1, file_path2, binary1, binary2); + } else if (status == CompareStatus::FileMatch) { + report = "Files '" + file_path1 + "' and '" + file_path2 + "' match."; + } + } + + return {status, report}; +} + const BinaryMetadata& Binary::get_metadata() const { return impl_->metadata; } From 3ce9677e316bdd97fb0ccbbd64dfad0ffc081b8b Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 18 Mar 2026 13:24:29 -0300 Subject: [PATCH 02/10] Add tests --- include/quiver/binary/binary.h | 6 +- src/binary/binary.cpp | 2 +- tests/test_binary.cpp | 331 +++++++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+), 4 deletions(-) diff --git a/include/quiver/binary/binary.h b/include/quiver/binary/binary.h index b477b976..5856cdf2 100644 --- a/include/quiver/binary/binary.h +++ b/include/quiver/binary/binary.h @@ -38,7 +38,7 @@ class QUIVER_API Binary { static Binary open_file(const std::string& file_path, char mode, const std::optional& metadata = {}); static CompareResult - compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report = true); + compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report = false); // Data handling std::vector read(const std::unordered_map& dims, bool allow_nulls = false); @@ -58,8 +58,8 @@ class QUIVER_API Binary { void fill_file_with_nulls(); // Reporting - static std::string build_data_report(const std::string& file_path1, const std::string& file_path2, Binary& binary1, - Binary& binary2); + static std::string + build_data_report(const std::string& file_path1, const std::string& file_path2, Binary& binary1, Binary& binary2); // Validations void validate_file_is_open() const; diff --git a/src/binary/binary.cpp b/src/binary/binary.cpp index 98aeaae5..fa6fc5c9 100644 --- a/src/binary/binary.cpp +++ b/src/binary/binary.cpp @@ -354,7 +354,7 @@ std::string Binary::build_data_report(const std::string& file_path1, const std::string& file_path2, Binary& binary1, Binary& binary2) { - constexpr int MAX_REPORT_LINES = 5; + constexpr int MAX_REPORT_LINES = 100; const auto& metadata = binary1.get_metadata(); const auto& dimensions = metadata.dimensions; diff --git a/tests/test_binary.cpp b/tests/test_binary.cpp index 93569610..be78241a 100644 --- a/tests/test_binary.cpp +++ b/tests/test_binary.cpp @@ -429,3 +429,334 @@ TEST_F(BinaryTempFileFixture, SingleTimeDimensionSkipsConsistencyCheck) { auto binary = Binary::open_file(path, 'w', md); EXPECT_NO_THROW(binary.write({1.0}, {{"month", 12}, {"scenario", 3}})); } + +// ============================================================================ +// Fixture for CompareFiles +// ============================================================================ + +class BinaryCompareFixture : public ::testing::Test { +protected: + void SetUp() override { + path1 = (fs::temp_directory_path() / "quiver_compare_test1").string(); + path2 = (fs::temp_directory_path() / "quiver_compare_test2").string(); + } + + void TearDown() override { + for (const auto& p : {path1, path2}) { + for (auto ext : {".qvr", ".toml"}) { + auto full = p + ext; + if (fs::exists(full)) + fs::remove(full); + } + } + } + + std::string path1; + std::string path2; + + static BinaryMetadata make_metadata() { + BinaryMetadata md; + md.version = "1"; + md.add_dimension("scenario", 2); + md.add_dimension("block", 3); + md.unit = "MW"; + md.labels = {"plant_1", "plant_2"}; + return md; + } +}; + +// ============================================================================ +// CompareFiles — Status +// ============================================================================ + +TEST_F(BinaryCompareFixture, IdenticalFilesReturnFileMatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, BothFilesUnwrittenReturnFileMatch) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, DifferentDataReturnsDataMismatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, WrittenVsUnwrittenReturnsDataMismatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentMetadataReturnsMetadataMismatch) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + auto bin = Binary::open_file(path1, 'w', md1); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md2); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentDimensionSizesReturnsMetadataMismatch) { + auto md1 = make_metadata(); + BinaryMetadata md2; + md2.version = "1"; + md2.add_dimension("scenario", 4); + md2.add_dimension("block", 3); + md2.unit = "MW"; + md2.labels = {"plant_1", "plant_2"}; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentLabelsReturnsMetadataMismatch) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.labels = {"gen_1", "gen_2"}; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +// ============================================================================ +// CompareFiles — Report with detailed_report=true +// ============================================================================ + +TEST_F(BinaryCompareFixture, FileMatchReportIsNotEmpty) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::FileMatch); + EXPECT_FALSE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportContainsDimensionInfo) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 2}}); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("scenario=1"), std::string::npos); + EXPECT_NE(result.report.find("block=2"), std::string::npos); + EXPECT_NE(result.report.find("plant_1"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportContainsValues) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_NE(result.report.find("10.0"), std::string::npos); + EXPECT_NE(result.report.find("99.0"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportShowsNaN) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + // leave position unwritten (NaN) + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({5.0, 6.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("NaN"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportOnlyShowsDifferingPositions) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); // same + bin.write({99.0, 40.0}, {{"scenario", 1}, {"block", 2}}); // different + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + // block=2 should appear (mismatch), block=1 should not (match) + EXPECT_NE(result.report.find("block=2"), std::string::npos); + EXPECT_EQ(result.report.find("block=1"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportTruncatesAtMaxLines) { + // Use larger dimensions to exceed MAX_REPORT_LINES (100) + // 10 scenarios x 11 blocks x 1 label = 110 mismatch lines + BinaryMetadata md; + md.version = "1"; + md.add_dimension("scenario", 10); + md.add_dimension("block", 11); + md.unit = "MW"; + md.labels = {"val"}; + + { + auto bin = Binary::open_file(path1, 'w', md); + for (int64_t s = 1; s <= 10; ++s) + for (int64_t b = 1; b <= 11; ++b) + bin.write({1.0}, {{"scenario", s}, {"block", b}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + for (int64_t s = 1; s <= 10; ++s) + for (int64_t b = 1; b <= 11; ++b) + bin.write({99.0}, {{"scenario", s}, {"block", b}}); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("more ..."), std::string::npos); +} + +TEST_F(BinaryCompareFixture, MetadataMismatchReportIsNotEmpty) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = Binary::compare_files(path1, path2, true); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); + EXPECT_FALSE(result.report.empty()); +} + +// ============================================================================ +// CompareFiles — Report with detailed_report=false (default) +// ============================================================================ + +TEST_F(BinaryCompareFixture, FileMatchNoReportIsEmpty) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); + EXPECT_TRUE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, DataMismatchNoReportIsEmpty) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_TRUE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, MetadataMismatchNoReportIsEmpty) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = Binary::compare_files(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); + EXPECT_TRUE(result.report.empty()); +} From 542998f95a6873895384e7bf9c49b56eb467f9b4 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 18 Mar 2026 14:37:05 -0300 Subject: [PATCH 03/10] Add julia bindings and C API --- bindings/julia/src/Quiver.jl | 6 + bindings/julia/src/binary/binary_file.jl | 25 ++++ bindings/julia/src/c_api.jl | 10 ++ bindings/julia/test/test_binary.jl | 136 +++++++++++++++++++ include/quiver/c/binary/binary.h | 14 ++ src/c/binary/binary.cpp | 37 ++++++ tests/test_c_api_binary.cpp | 162 +++++++++++++++++++++++ 7 files changed, 390 insertions(+) diff --git a/bindings/julia/src/Quiver.jl b/bindings/julia/src/Quiver.jl index 3a195bd0..8801e672 100644 --- a/bindings/julia/src/Quiver.jl +++ b/bindings/julia/src/Quiver.jl @@ -26,10 +26,16 @@ include("binary/Binary.jl") export Element, Database, LuaRunner, DatabaseException export ScalarMetadata, GroupMetadata export QUIVER_DATA_TYPE_INTEGER, QUIVER_DATA_TYPE_FLOAT, QUIVER_DATA_TYPE_STRING +export QUIVER_COMPARE_FILE_MATCH, QUIVER_COMPARE_METADATA_MISMATCH, QUIVER_COMPARE_DATA_MISMATCH # Re-export C enum constants for data types const QUIVER_DATA_TYPE_INTEGER = C.QUIVER_DATA_TYPE_INTEGER const QUIVER_DATA_TYPE_FLOAT = C.QUIVER_DATA_TYPE_FLOAT const QUIVER_DATA_TYPE_STRING = C.QUIVER_DATA_TYPE_STRING +# Re-export C enum constants for compare status +const QUIVER_COMPARE_FILE_MATCH = C.QUIVER_COMPARE_FILE_MATCH +const QUIVER_COMPARE_METADATA_MISMATCH = C.QUIVER_COMPARE_METADATA_MISMATCH +const QUIVER_COMPARE_DATA_MISMATCH = C.QUIVER_COMPARE_DATA_MISMATCH + end diff --git a/bindings/julia/src/binary/binary_file.jl b/bindings/julia/src/binary/binary_file.jl index db4f9e52..3608fcaa 100644 --- a/bindings/julia/src/binary/binary_file.jl +++ b/bindings/julia/src/binary/binary_file.jl @@ -77,6 +77,31 @@ function get_metadata(binary::Binary) return Metadata(out_md[]) end +function compare_files(path1::String, path2::String; detailed_report::Bool = false) + out_status = Ref{C.quiver_compare_status_t}(C.QUIVER_COMPARE_FILE_MATCH) + out_report = Ref{Ptr{Cchar}}(C_NULL) + + check(C.quiver_binary_compare_files(path1, path2, detailed_report ? Cint(1) : Cint(0), out_status, out_report)) + + status = if out_status[] == C.QUIVER_COMPARE_FILE_MATCH + C.QUIVER_COMPARE_FILE_MATCH + elseif out_status[] == C.QUIVER_COMPARE_METADATA_MISMATCH + C.QUIVER_COMPARE_METADATA_MISMATCH + else + C.QUIVER_COMPARE_DATA_MISMATCH + end + + report = if out_report[] != C_NULL + r = unsafe_string(out_report[]) + C.quiver_binary_free_string(out_report[]) + r + else + nothing + end + + return (; status, report) +end + function get_file_path(binary::Binary) out = Ref{Ptr{Cchar}}(C_NULL) check(C.quiver_binary_get_file_path(binary.ptr, out)) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index fa83186c..3100f87a 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -624,6 +624,16 @@ function quiver_binary_get_file_path(binary, out) @ccall libquiver_c.quiver_binary_get_file_path(binary::Ptr{quiver_binary_t}, out::Ptr{Ptr{Cchar}})::quiver_error_t end +@cenum quiver_compare_status_t::UInt32 begin + QUIVER_COMPARE_FILE_MATCH = 0 + QUIVER_COMPARE_METADATA_MISMATCH = 1 + QUIVER_COMPARE_DATA_MISMATCH = 2 +end + +function quiver_binary_compare_files(path1, path2, detailed_report, out_status, out_report) + @ccall libquiver_c.quiver_binary_compare_files(path1::Ptr{Cchar}, path2::Ptr{Cchar}, detailed_report::Cint, out_status::Ptr{quiver_compare_status_t}, out_report::Ptr{Ptr{Cchar}})::quiver_error_t +end + function quiver_binary_free_string(str) @ccall libquiver_c.quiver_binary_free_string(str::Ptr{Cchar})::quiver_error_t end diff --git a/bindings/julia/test/test_binary.jl b/bindings/julia/test/test_binary.jl index 3ee7f974..4672374c 100644 --- a/bindings/julia/test/test_binary.jl +++ b/bindings/julia/test/test_binary.jl @@ -511,6 +511,142 @@ end end end + # ========================================================================== + # Compare files + # ========================================================================== + + @testset "compare_files returns file_match for identical files" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files detects data mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files detects metadata mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md1 = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md1) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + md2 = Quiver.Binary.Metadata(; + initial_datetime = "2025-01-01T00:00:00", + unit = "GWh", + labels = ["val1", "val2"], + dimensions = ["row", "col"], + dimension_sizes = Int64[3, 2], + ) + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md2) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_METADATA_MISMATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with detailed report on match" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + @test result.report !== nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with detailed report on data mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + @test result.report !== nothing + @test occursin("val1", result.report) + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files non-existent file" begin + path1 = make_binary_path() * "_cmp1" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + @test_throws Quiver.DatabaseException Quiver.Binary.compare_files(path1, "nonexistent") + finally + cleanup_binary(path1) + end + end + # ========================================================================== # Time dimension validation # ========================================================================== diff --git a/include/quiver/c/binary/binary.h b/include/quiver/c/binary/binary.h index 15518362..aa04bdf5 100644 --- a/include/quiver/c/binary/binary.h +++ b/include/quiver/c/binary/binary.h @@ -11,6 +11,13 @@ extern "C" { // Opaque handle type typedef struct quiver_binary quiver_binary_t; +// Compare status codes +typedef enum { + QUIVER_COMPARE_FILE_MATCH = 0, + QUIVER_COMPARE_METADATA_MISMATCH = 1, + QUIVER_COMPARE_DATA_MISMATCH = 2, +} quiver_compare_status_t; + // Open/close QUIVER_C_API quiver_error_t quiver_binary_open_read(const char* path, quiver_binary_t** out); QUIVER_C_API quiver_error_t quiver_binary_open_write(const char* path, @@ -37,6 +44,13 @@ QUIVER_C_API quiver_error_t quiver_binary_write(quiver_binary_t* binary, QUIVER_C_API quiver_error_t quiver_binary_get_metadata(quiver_binary_t* binary, quiver_binary_metadata_t** out); QUIVER_C_API quiver_error_t quiver_binary_get_file_path(quiver_binary_t* binary, char** out); +// Compare +QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, + const char* path2, + int detailed_report, + quiver_compare_status_t* out_status, + char** out_report); + // Free QUIVER_C_API quiver_error_t quiver_binary_free_string(char* str); QUIVER_C_API quiver_error_t quiver_binary_free_float_array(double* data); diff --git a/src/c/binary/binary.cpp b/src/c/binary/binary.cpp index dc597387..ba66e100 100644 --- a/src/c/binary/binary.cpp +++ b/src/c/binary/binary.cpp @@ -133,6 +133,43 @@ QUIVER_C_API quiver_error_t quiver_binary_get_file_path(quiver_binary_t* binary, return QUIVER_OK; } +// Compare + +QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, + const char* path2, + int detailed_report, + quiver_compare_status_t* out_status, + char** out_report) { + QUIVER_REQUIRE(path1, path2, out_status, out_report); + + try { + auto result = quiver::Binary::compare_files(path1, path2, detailed_report != 0); + + switch (result.status) { + case quiver::CompareStatus::FileMatch: + *out_status = QUIVER_COMPARE_FILE_MATCH; + break; + case quiver::CompareStatus::MetadataMismatch: + *out_status = QUIVER_COMPARE_METADATA_MISMATCH; + break; + case quiver::CompareStatus::DataMismatch: + *out_status = QUIVER_COMPARE_DATA_MISMATCH; + break; + } + + if (result.report.empty()) { + *out_report = nullptr; + } else { + *out_report = quiver::string::new_c_str(result.report); + } + + return QUIVER_OK; + } catch (const std::exception& e) { + quiver_set_last_error(e.what()); + return QUIVER_ERROR; + } +} + // Free QUIVER_C_API quiver_error_t quiver_binary_free_string(char* str) { diff --git a/tests/test_c_api_binary.cpp b/tests/test_c_api_binary.cpp index 15fbaff0..46b905a3 100644 --- a/tests/test_c_api_binary.cpp +++ b/tests/test_c_api_binary.cpp @@ -460,3 +460,165 @@ TEST_F(BinaryCApiFixture, ReadAllowNulls) { quiver_binary_close(binary); } + +// ============================================================================ +// Compare files +// ============================================================================ + +class BinaryCApiCompareFixture : public ::testing::Test { +protected: + void SetUp() override { + path1 = (fs::temp_directory_path() / "quiver_c_compare_test_1").string(); + path2 = (fs::temp_directory_path() / "quiver_c_compare_test_2").string(); + } + + void TearDown() override { + for (auto* p : {&path1, &path2}) { + for (auto ext : {".qvr", ".toml"}) { + auto full = *p + ext; + if (fs::exists(full)) + fs::remove(full); + } + } + } + + std::string path1; + std::string path2; + + quiver_binary_metadata_t* make_metadata() { + quiver_element_t* el = nullptr; + quiver_element_create(&el); + quiver_element_set_string(el, "version", "1"); + quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); + quiver_element_set_string(el, "unit", "MW"); + + const char* dims[] = {"scenario", "block"}; + quiver_element_set_array_string(el, "dimensions", dims, 2); + int64_t sizes[] = {2, 3}; + quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); + const char* labels[] = {"plant_1", "plant_2"}; + quiver_element_set_array_string(el, "labels", labels, 2); + + quiver_binary_metadata_t* md = nullptr; + quiver_binary_metadata_from_element(el, &md); + quiver_element_destroy(el); + return md; + } + + void write_file(const std::string& path, quiver_binary_metadata_t* md, double val1, double val2) { + quiver_binary_t* binary = nullptr; + quiver_binary_open_write(path.c_str(), md, &binary); + + const char* dim_names[] = {"scenario", "block"}; + int64_t dim_values[] = {1, 1}; + double data[] = {val1, val2}; + quiver_binary_write(binary, dim_names, dim_values, 2, data, 2); + quiver_binary_close(binary); + } +}; + +TEST_F(BinaryCApiCompareFixture, FileMatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, DataMismatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 99.0, 20.0); + quiver_binary_metadata_free(md); + + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, MetadataMismatch) { + auto* md1 = make_metadata(); + write_file(path1, md1, 10.0, 20.0); + quiver_binary_metadata_free(md1); + + // Create second file with different unit + quiver_element_t* el = nullptr; + quiver_element_create(&el); + quiver_element_set_string(el, "version", "1"); + quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); + quiver_element_set_string(el, "unit", "GWh"); + const char* dims[] = {"scenario", "block"}; + quiver_element_set_array_string(el, "dimensions", dims, 2); + int64_t sizes[] = {2, 3}; + quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); + const char* labels[] = {"plant_1", "plant_2"}; + quiver_element_set_array_string(el, "labels", labels, 2); + + quiver_binary_metadata_t* md2 = nullptr; + quiver_binary_metadata_from_element(el, &md2); + quiver_element_destroy(el); + + write_file(path2, md2, 10.0, 20.0); + quiver_binary_metadata_free(md2); + + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_METADATA_MISMATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, DetailedReportOnMatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 1, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); + ASSERT_NE(report, nullptr); + quiver_binary_free_string(report); +} + +TEST_F(BinaryCApiCompareFixture, DetailedReportOnDataMismatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 99.0, 20.0); + quiver_binary_metadata_free(md); + + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 1, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); + ASSERT_NE(report, nullptr); + + std::string report_str(report); + EXPECT_NE(report_str.find("plant_1"), std::string::npos); + quiver_binary_free_string(report); +} + +TEST_F(BinaryCApiCompareFixture, NullArgs) { + quiver_compare_status_t status; + char* report = nullptr; + EXPECT_EQ(quiver_binary_compare_files(nullptr, nullptr, 0, &status, &report), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: path1"); +} + +TEST_F(BinaryCApiCompareFixture, NonExistentFile) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + quiver_compare_status_t status; + char* report = nullptr; + EXPECT_EQ(quiver_binary_compare_files(path1.c_str(), "nonexistent", 0, &status, &report), QUIVER_ERROR); +} From ca541e0e0ba6791c668f2f41531d58e508a64338 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 18 Mar 2026 14:49:25 -0300 Subject: [PATCH 04/10] format --- src/c/binary/binary.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/c/binary/binary.cpp b/src/c/binary/binary.cpp index ba66e100..8c761800 100644 --- a/src/c/binary/binary.cpp +++ b/src/c/binary/binary.cpp @@ -146,15 +146,15 @@ QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, auto result = quiver::Binary::compare_files(path1, path2, detailed_report != 0); switch (result.status) { - case quiver::CompareStatus::FileMatch: - *out_status = QUIVER_COMPARE_FILE_MATCH; - break; - case quiver::CompareStatus::MetadataMismatch: - *out_status = QUIVER_COMPARE_METADATA_MISMATCH; - break; - case quiver::CompareStatus::DataMismatch: - *out_status = QUIVER_COMPARE_DATA_MISMATCH; - break; + case quiver::CompareStatus::FileMatch: + *out_status = QUIVER_COMPARE_FILE_MATCH; + break; + case quiver::CompareStatus::MetadataMismatch: + *out_status = QUIVER_COMPARE_METADATA_MISMATCH; + break; + case quiver::CompareStatus::DataMismatch: + *out_status = QUIVER_COMPARE_DATA_MISMATCH; + break; } if (result.report.empty()) { From 43ae69553b57935b6b0eb44c0b70aa780e0ef532 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 18 Mar 2026 18:27:08 -0300 Subject: [PATCH 05/10] Add tolerances and move comparator to a separate class --- bindings/julia/src/binary/Binary.jl | 1 + bindings/julia/src/binary/binary_file.jl | 25 -- bindings/julia/src/binary/comparator.jl | 44 ++ bindings/julia/src/c_api.jl | 19 +- bindings/julia/test/test_binary.jl | 136 ------ bindings/julia/test/test_binary_comparator.jl | 249 +++++++++++ include/quiver/binary/binary.h | 21 +- include/quiver/binary/binary_comparator.h | 49 +++ include/quiver/c/binary/binary.h | 14 - include/quiver/c/binary/binary_comparator.h | 41 ++ include/quiver/quiver.h | 1 + src/CMakeLists.txt | 2 + src/binary/binary.cpp | 139 ------ src/binary/binary_comparator.cpp | 152 +++++++ src/c/binary/binary.cpp | 37 -- src/c/binary/binary_comparator.cpp | 62 +++ tests/CMakeLists.txt | 2 + tests/test_binary.cpp | 331 -------------- tests/test_binary_comparator.cpp | 416 ++++++++++++++++++ tests/test_c_api_binary.cpp | 162 ------- tests/test_c_api_binary_comparator.cpp | 185 ++++++++ 21 files changed, 1225 insertions(+), 863 deletions(-) create mode 100644 bindings/julia/src/binary/comparator.jl create mode 100644 bindings/julia/test/test_binary_comparator.jl create mode 100644 include/quiver/binary/binary_comparator.h create mode 100644 include/quiver/c/binary/binary_comparator.h create mode 100644 src/binary/binary_comparator.cpp create mode 100644 src/c/binary/binary_comparator.cpp create mode 100644 tests/test_binary_comparator.cpp create mode 100644 tests/test_c_api_binary_comparator.cpp diff --git a/bindings/julia/src/binary/Binary.jl b/bindings/julia/src/binary/Binary.jl index 65607c7d..0a317c75 100644 --- a/bindings/julia/src/binary/Binary.jl +++ b/bindings/julia/src/binary/Binary.jl @@ -4,6 +4,7 @@ using ..Quiver: C, check, Element include("metadata.jl") include("binary_file.jl") +include("comparator.jl") include("csv_converter.jl") end diff --git a/bindings/julia/src/binary/binary_file.jl b/bindings/julia/src/binary/binary_file.jl index 3608fcaa..db4f9e52 100644 --- a/bindings/julia/src/binary/binary_file.jl +++ b/bindings/julia/src/binary/binary_file.jl @@ -77,31 +77,6 @@ function get_metadata(binary::Binary) return Metadata(out_md[]) end -function compare_files(path1::String, path2::String; detailed_report::Bool = false) - out_status = Ref{C.quiver_compare_status_t}(C.QUIVER_COMPARE_FILE_MATCH) - out_report = Ref{Ptr{Cchar}}(C_NULL) - - check(C.quiver_binary_compare_files(path1, path2, detailed_report ? Cint(1) : Cint(0), out_status, out_report)) - - status = if out_status[] == C.QUIVER_COMPARE_FILE_MATCH - C.QUIVER_COMPARE_FILE_MATCH - elseif out_status[] == C.QUIVER_COMPARE_METADATA_MISMATCH - C.QUIVER_COMPARE_METADATA_MISMATCH - else - C.QUIVER_COMPARE_DATA_MISMATCH - end - - report = if out_report[] != C_NULL - r = unsafe_string(out_report[]) - C.quiver_binary_free_string(out_report[]) - r - else - nothing - end - - return (; status, report) -end - function get_file_path(binary::Binary) out = Ref{Ptr{Cchar}}(C_NULL) check(C.quiver_binary_get_file_path(binary.ptr, out)) diff --git a/bindings/julia/src/binary/comparator.jl b/bindings/julia/src/binary/comparator.jl new file mode 100644 index 00000000..9b0cd6e2 --- /dev/null +++ b/bindings/julia/src/binary/comparator.jl @@ -0,0 +1,44 @@ +function compare_files( + path1::String, path2::String; + absolute_tolerance::Union{Float64, Nothing} = nothing, + relative_tolerance::Union{Float64, Nothing} = nothing, + detailed_report::Union{Bool, Nothing} = nothing, + max_report_lines::Union{Int, Nothing} = nothing, +) + options = C.quiver_compare_options_default() + if absolute_tolerance !== nothing + options.absolute_tolerance = absolute_tolerance + end + if relative_tolerance !== nothing + options.relative_tolerance = relative_tolerance + end + if detailed_report !== nothing + options.detailed_report = detailed_report ? Cint(1) : Cint(0) + end + if max_report_lines !== nothing + options.max_report_lines = Cint(max_report_lines) + end + + out_status = Ref{C.quiver_compare_status_t}(C.QUIVER_COMPARE_FILE_MATCH) + out_report = Ref{Ptr{Cchar}}(C_NULL) + + check(C.quiver_binary_compare_files(path1, path2, Ref(options), out_status, out_report)) + + status = if out_status[] == C.QUIVER_COMPARE_FILE_MATCH + C.QUIVER_COMPARE_FILE_MATCH + elseif out_status[] == C.QUIVER_COMPARE_METADATA_MISMATCH + C.QUIVER_COMPARE_METADATA_MISMATCH + else + C.QUIVER_COMPARE_DATA_MISMATCH + end + + report = if out_report[] != C_NULL + r = unsafe_string(out_report[]) + C.quiver_binary_comparator_free_string(out_report[]) + r + else + nothing + end + + return (; status, report) +end diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index 3100f87a..e743e14f 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -630,8 +630,23 @@ end QUIVER_COMPARE_DATA_MISMATCH = 2 end -function quiver_binary_compare_files(path1, path2, detailed_report, out_status, out_report) - @ccall libquiver_c.quiver_binary_compare_files(path1::Ptr{Cchar}, path2::Ptr{Cchar}, detailed_report::Cint, out_status::Ptr{quiver_compare_status_t}, out_report::Ptr{Ptr{Cchar}})::quiver_error_t +mutable struct quiver_compare_options_t + absolute_tolerance::Cdouble + relative_tolerance::Cdouble + detailed_report::Cint + max_report_lines::Cint +end + +function quiver_compare_options_default() + @ccall libquiver_c.quiver_compare_options_default()::quiver_compare_options_t +end + +function quiver_binary_compare_files(path1, path2, options, out_status, out_report) + @ccall libquiver_c.quiver_binary_compare_files(path1::Ptr{Cchar}, path2::Ptr{Cchar}, options::Ptr{quiver_compare_options_t}, out_status::Ptr{quiver_compare_status_t}, out_report::Ptr{Ptr{Cchar}})::quiver_error_t +end + +function quiver_binary_comparator_free_string(str) + @ccall libquiver_c.quiver_binary_comparator_free_string(str::Ptr{Cchar})::quiver_error_t end function quiver_binary_free_string(str) diff --git a/bindings/julia/test/test_binary.jl b/bindings/julia/test/test_binary.jl index 4672374c..3ee7f974 100644 --- a/bindings/julia/test/test_binary.jl +++ b/bindings/julia/test/test_binary.jl @@ -511,142 +511,6 @@ end end end - # ========================================================================== - # Compare files - # ========================================================================== - - @testset "compare_files returns file_match for identical files" begin - path1 = make_binary_path() * "_cmp1" - path2 = make_binary_path() * "_cmp2" - try - md = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) - Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b2) - - result = Quiver.Binary.compare_files(path1, path2) - @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH - @test result.report === nothing - finally - cleanup_binary(path1) - cleanup_binary(path2) - end - end - - @testset "compare_files detects data mismatch" begin - path1 = make_binary_path() * "_cmp1" - path2 = make_binary_path() * "_cmp2" - try - md = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) - Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b2) - - result = Quiver.Binary.compare_files(path1, path2) - @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH - @test result.report === nothing - finally - cleanup_binary(path1) - cleanup_binary(path2) - end - end - - @testset "compare_files detects metadata mismatch" begin - path1 = make_binary_path() * "_cmp1" - path2 = make_binary_path() * "_cmp2" - try - md1 = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md1) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - md2 = Quiver.Binary.Metadata(; - initial_datetime = "2025-01-01T00:00:00", - unit = "GWh", - labels = ["val1", "val2"], - dimensions = ["row", "col"], - dimension_sizes = Int64[3, 2], - ) - b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md2) - Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b2) - - result = Quiver.Binary.compare_files(path1, path2) - @test result.status == Quiver.QUIVER_COMPARE_METADATA_MISMATCH - @test result.report === nothing - finally - cleanup_binary(path1) - cleanup_binary(path2) - end - end - - @testset "compare_files with detailed report on match" begin - path1 = make_binary_path() * "_cmp1" - path2 = make_binary_path() * "_cmp2" - try - md = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) - Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b2) - - result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) - @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH - @test result.report !== nothing - finally - cleanup_binary(path1) - cleanup_binary(path2) - end - end - - @testset "compare_files with detailed report on data mismatch" begin - path1 = make_binary_path() * "_cmp1" - path2 = make_binary_path() * "_cmp2" - try - md = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) - Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b2) - - result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) - @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH - @test result.report !== nothing - @test occursin("val1", result.report) - finally - cleanup_binary(path1) - cleanup_binary(path2) - end - end - - @testset "compare_files non-existent file" begin - path1 = make_binary_path() * "_cmp1" - try - md = make_simple_metadata() - b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) - Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) - Quiver.Binary.close!(b1) - - @test_throws Quiver.DatabaseException Quiver.Binary.compare_files(path1, "nonexistent") - finally - cleanup_binary(path1) - end - end - # ========================================================================== # Time dimension validation # ========================================================================== diff --git a/bindings/julia/test/test_binary_comparator.jl b/bindings/julia/test/test_binary_comparator.jl new file mode 100644 index 00000000..5a745db1 --- /dev/null +++ b/bindings/julia/test/test_binary_comparator.jl @@ -0,0 +1,249 @@ +module TestBinaryComparator + +using Quiver +using Test + +include("fixture.jl") + +function make_binary_path() + return joinpath(tempdir(), "quiver_julia_comparator_test") +end + +function cleanup_binary(path) + for ext in [".qvr", ".toml", ".csv"] + f = path * ext + isfile(f) && rm(f) + end +end + +function make_simple_metadata() + return Quiver.Binary.Metadata(; + initial_datetime = "2025-01-01T00:00:00", + unit = "MW", + labels = ["val1", "val2"], + dimensions = ["row", "col"], + dimension_sizes = Int64[3, 2], + ) +end + +@testset "Binary Comparator" begin + @testset "compare_files returns file_match for identical files" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files detects data mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files detects metadata mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md1 = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md1) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + md2 = Quiver.Binary.Metadata(; + initial_datetime = "2025-01-01T00:00:00", + unit = "GWh", + labels = ["val1", "val2"], + dimensions = ["row", "col"], + dimension_sizes = Int64[3, 2], + ) + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md2) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2) + @test result.status == Quiver.QUIVER_COMPARE_METADATA_MISMATCH + @test result.report === nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with detailed report on match" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + @test result.report !== nothing + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with detailed report on data mismatch" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [99.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; detailed_report = true) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + @test result.report !== nothing + @test occursin("val1", result.report) + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files non-existent file" begin + path1 = make_binary_path() * "_cmp1" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [1.0, 2.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + @test_throws Quiver.DatabaseException Quiver.Binary.compare_files(path1, "nonexistent") + finally + cleanup_binary(path1) + end + end + + @testset "compare_files with absolute tolerance matches within threshold" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [10.0, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [10.05, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; absolute_tolerance = 0.1) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with absolute tolerance fails outside threshold" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [10.0, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [10.05, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + result = Quiver.Binary.compare_files(path1, path2; absolute_tolerance = 0.01) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with relative tolerance matches within threshold" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [100.0, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [100.05, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + # diff = 0.05, threshold = 0.0 + 0.001 * 100.05 = 0.10005 + result = Quiver.Binary.compare_files(path1, path2; absolute_tolerance = 0.0, relative_tolerance = 0.001) + @test result.status == Quiver.QUIVER_COMPARE_FILE_MATCH + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end + + @testset "compare_files with relative tolerance fails outside threshold" begin + path1 = make_binary_path() * "_cmp1" + path2 = make_binary_path() * "_cmp2" + try + md = make_simple_metadata() + b1 = Quiver.Binary.open_file(path1; mode = :write, metadata = md) + Quiver.Binary.write!(b1; data = [100.0, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b1) + + b2 = Quiver.Binary.open_file(path2; mode = :write, metadata = md) + Quiver.Binary.write!(b2; data = [100.2, 20.0], row = 1, col = 1) + Quiver.Binary.close!(b2) + + # diff = 0.2, threshold = 0.0 + 0.001 * 100.2 = 0.1002 + result = Quiver.Binary.compare_files(path1, path2; absolute_tolerance = 0.0, relative_tolerance = 0.001) + @test result.status == Quiver.QUIVER_COMPARE_DATA_MISMATCH + finally + cleanup_binary(path1) + cleanup_binary(path2) + end + end +end + +end diff --git a/include/quiver/binary/binary.h b/include/quiver/binary/binary.h index 5856cdf2..ee0e23d7 100644 --- a/include/quiver/binary/binary.h +++ b/include/quiver/binary/binary.h @@ -14,13 +14,6 @@ namespace quiver { -enum class CompareStatus { FileMatch, MetadataMismatch, DataMismatch }; - -struct CompareResult { - CompareStatus status; - std::string report; -}; - class QUIVER_API Binary { public: explicit Binary(const std::string& file_path, const BinaryMetadata& metadata, std::unique_ptr io); @@ -37,8 +30,6 @@ class QUIVER_API Binary { // File handling static Binary open_file(const std::string& file_path, char mode, const std::optional& metadata = {}); - static CompareResult - compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report = false); // Data handling std::vector read(const std::unordered_map& dims, bool allow_nulls = false); @@ -48,6 +39,10 @@ class QUIVER_API Binary { const BinaryMetadata& get_metadata() const; const std::string& get_file_path() const; + // Dimension iteration + std::vector next_dimensions(const std::vector& current_dimensions); + std::vector dimension_sizes_at_values(const std::vector& dimension_values) const; + private: struct Impl; std::unique_ptr impl_; @@ -57,10 +52,6 @@ class QUIVER_API Binary { void go_to_position(int64_t position, char mode); void fill_file_with_nulls(); - // Reporting - static std::string - build_data_report(const std::string& file_path1, const std::string& file_path2, Binary& binary1, Binary& binary2); - // Validations void validate_file_is_open() const; void validate_dimension_values(const std::unordered_map& dims); @@ -68,10 +59,6 @@ class QUIVER_API Binary { protected: std::iostream& get_io(); - - // Dimension iteration - std::vector next_dimensions(const std::vector& current_dimensions); - std::vector dimension_sizes_at_values(const std::vector& dimension_values) const; }; } // namespace quiver diff --git a/include/quiver/binary/binary_comparator.h b/include/quiver/binary/binary_comparator.h new file mode 100644 index 00000000..4daa619c --- /dev/null +++ b/include/quiver/binary/binary_comparator.h @@ -0,0 +1,49 @@ +#ifndef QUIVER_BINARY_COMPARATOR_H +#define QUIVER_BINARY_COMPARATOR_H + +#include "../export.h" +#include "binary.h" + +#include +#include + +namespace quiver { + +enum class CompareStatus { FileMatch, MetadataMismatch, DataMismatch }; + +struct QUIVER_API CompareOptions { + double absolute_tolerance = 1e-6; + double relative_tolerance = 0.0; + bool detailed_report = false; + int max_report_lines = 100; +}; + +struct QUIVER_API CompareResult { + CompareStatus status; + std::string report; +}; + +class QUIVER_API BinaryComparator { +public: + ~BinaryComparator(); + + BinaryComparator(const BinaryComparator&) = delete; + BinaryComparator& operator=(const BinaryComparator&) = delete; + BinaryComparator(BinaryComparator&&) noexcept; + BinaryComparator& operator=(BinaryComparator&&) noexcept; + + static CompareResult + compare(const std::string& file_path1, const std::string& file_path2, const CompareOptions& options = {}); + +private: + struct Impl; + std::unique_ptr impl_; + + BinaryComparator(Binary binary1, Binary binary2, const CompareOptions& options); + + CompareResult run(); +}; + +} // namespace quiver + +#endif // QUIVER_BINARY_COMPARATOR_H diff --git a/include/quiver/c/binary/binary.h b/include/quiver/c/binary/binary.h index aa04bdf5..15518362 100644 --- a/include/quiver/c/binary/binary.h +++ b/include/quiver/c/binary/binary.h @@ -11,13 +11,6 @@ extern "C" { // Opaque handle type typedef struct quiver_binary quiver_binary_t; -// Compare status codes -typedef enum { - QUIVER_COMPARE_FILE_MATCH = 0, - QUIVER_COMPARE_METADATA_MISMATCH = 1, - QUIVER_COMPARE_DATA_MISMATCH = 2, -} quiver_compare_status_t; - // Open/close QUIVER_C_API quiver_error_t quiver_binary_open_read(const char* path, quiver_binary_t** out); QUIVER_C_API quiver_error_t quiver_binary_open_write(const char* path, @@ -44,13 +37,6 @@ QUIVER_C_API quiver_error_t quiver_binary_write(quiver_binary_t* binary, QUIVER_C_API quiver_error_t quiver_binary_get_metadata(quiver_binary_t* binary, quiver_binary_metadata_t** out); QUIVER_C_API quiver_error_t quiver_binary_get_file_path(quiver_binary_t* binary, char** out); -// Compare -QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, - const char* path2, - int detailed_report, - quiver_compare_status_t* out_status, - char** out_report); - // Free QUIVER_C_API quiver_error_t quiver_binary_free_string(char* str); QUIVER_C_API quiver_error_t quiver_binary_free_float_array(double* data); diff --git a/include/quiver/c/binary/binary_comparator.h b/include/quiver/c/binary/binary_comparator.h new file mode 100644 index 00000000..4d4d944a --- /dev/null +++ b/include/quiver/c/binary/binary_comparator.h @@ -0,0 +1,41 @@ +#ifndef QUIVER_C_BINARY_COMPARATOR_H +#define QUIVER_C_BINARY_COMPARATOR_H + +#include "../common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// Compare status codes +typedef enum { + QUIVER_COMPARE_FILE_MATCH = 0, + QUIVER_COMPARE_METADATA_MISMATCH = 1, + QUIVER_COMPARE_DATA_MISMATCH = 2, +} quiver_compare_status_t; + +// Compare options +typedef struct { + double absolute_tolerance; + double relative_tolerance; + int detailed_report; + int max_report_lines; +} quiver_compare_options_t; + +QUIVER_C_API quiver_compare_options_t quiver_compare_options_default(void); + +// Compare +QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, + const char* path2, + const quiver_compare_options_t* options, + quiver_compare_status_t* out_status, + char** out_report); + +// Free +QUIVER_C_API quiver_error_t quiver_binary_comparator_free_string(char* str); + +#ifdef __cplusplus +} +#endif + +#endif // QUIVER_C_BINARY_COMPARATOR_H diff --git a/include/quiver/quiver.h b/include/quiver/quiver.h index 27988e52..bc7e3f2f 100644 --- a/include/quiver/quiver.h +++ b/include/quiver/quiver.h @@ -2,6 +2,7 @@ #define QUIVER_H #include "binary/binary.h" +#include "binary/binary_comparator.h" #include "binary/binary_metadata.h" #include "binary/csv_converter.h" #include "database.h" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8905d774..3c8f0c24 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ set(QUIVER_SOURCES schema_validator.cpp type_validator.cpp binary/binary.cpp + binary/binary_comparator.cpp binary/csv_converter.cpp binary/binary_metadata.cpp binary/dimension.cpp @@ -117,6 +118,7 @@ if(QUIVER_BUILD_C_API) c/element.cpp c/lua_runner.cpp c/binary/binary.cpp + c/binary/binary_comparator.cpp c/binary/csv_converter.cpp c/binary/binary_metadata.cpp ) diff --git a/src/binary/binary.cpp b/src/binary/binary.cpp index fa6fc5c9..0296d7a3 100644 --- a/src/binary/binary.cpp +++ b/src/binary/binary.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -350,144 +349,6 @@ void Binary::fill_file_with_nulls() { } } -std::string Binary::build_data_report(const std::string& file_path1, - const std::string& file_path2, - Binary& binary1, - Binary& binary2) { - constexpr int MAX_REPORT_LINES = 100; - - const auto& metadata = binary1.get_metadata(); - const auto& dimensions = metadata.dimensions; - - // Get initial dimension values - std::vector initial_dimensions; - for (const auto& dim : dimensions) { - if (dim.is_time_dimension()) { - initial_dimensions.push_back(dim.time->initial_value); - } else { - initial_dimensions.push_back(1); - } - } - - // Calculate maximum number of lines - int64_t max_lines = 1; - for (const auto& dim : dimensions) { - max_lines *= dim.size; - } - - std::string report = "Data mismatch between '" + file_path1 + "' and '" + file_path2 + "':\n"; - int report_lines = 0; - auto current_dims = initial_dimensions; - for (int64_t i = 0; i < max_lines; ++i) { - std::unordered_map dims; - for (size_t j = 0; j < dimensions.size(); ++j) { - dims[dimensions[j].name] = current_dims[j]; - } - - auto data1 = binary1.read(dims, true); - auto data2 = binary2.read(dims, true); - - for (size_t k = 0; k < data1.size(); ++k) { - bool nan1 = std::isnan(data1[k]); - bool nan2 = std::isnan(data2[k]); - if (nan1 != nan2 || (!nan1 && data1[k] != data2[k])) { - // Build dimension string - std::string dim_str; - for (size_t j = 0; j < dimensions.size(); ++j) { - if (!dim_str.empty()) - dim_str += ", "; - dim_str += dimensions[j].name + "=" + std::to_string(current_dims[j]); - } - - // Build data string - std::string data_str = "'" + metadata.labels[k] + "': "; - if (nan1) { - data_str += "NaN"; - } else { - data_str += std::to_string(data1[k]); - } - data_str += " vs "; - if (nan2) { - data_str += "NaN"; - } else { - data_str += std::to_string(data2[k]); - } - - // Append to report - report += " {" + dim_str + "}, " + data_str + "\n"; - report_lines++; - if (report_lines >= MAX_REPORT_LINES) { - report += " more ...\n"; - return report; - } - } - } - - current_dims = binary1.next_dimensions(current_dims); - if (current_dims == initial_dimensions) - break; - } - - return report; -} - -CompareResult -Binary::compare_files(const std::string& file_path1, const std::string& file_path2, bool detailed_report) { - auto binary1 = open_file(file_path1, 'r'); - auto binary2 = open_file(file_path2, 'r'); - std::string report; - - const auto& metadata1 = binary1.get_metadata(); - const auto& metadata2 = binary2.get_metadata(); - - if (metadata1 != metadata2) { - if (detailed_report) { - report = "Metadata mismatch between files '" + file_path1 + "' and '" + file_path2 + "'.\n"; - } - return {CompareStatus::MetadataMismatch, report}; - } - - // Fast comparison via raw binary data - auto& io1 = binary1.get_io(); - auto& io2 = binary2.get_io(); - io1.seekg(0); - io2.seekg(0); - - CompareStatus status = CompareStatus::FileMatch; - constexpr size_t BUFFER_SIZE = 8192; // 8 KB chunks for raw binary comparison - char buf1[BUFFER_SIZE]; - char buf2[BUFFER_SIZE]; - - while (io1.good() && io2.good()) { - io1.read(buf1, BUFFER_SIZE); - io2.read(buf2, BUFFER_SIZE); - - auto bytes_read1 = io1.gcount(); - auto bytes_read2 = io2.gcount(); - - if (bytes_read1 != bytes_read2 || std::memcmp(buf1, buf2, bytes_read1) != 0) { - status = CompareStatus::DataMismatch; - break; - } - - if (bytes_read1 == 0) - break; - } - - if (detailed_report) { - if (status == CompareStatus::DataMismatch) { - // Clear stream state flags (eofbit/failbit) set by the memcmp loop - io1.clear(); - io2.clear(); - report = build_data_report(file_path1, file_path2, binary1, binary2); - } else if (status == CompareStatus::FileMatch) { - report = "Files '" + file_path1 + "' and '" + file_path2 + "' match."; - } - } - - return {status, report}; -} - const BinaryMetadata& Binary::get_metadata() const { return impl_->metadata; } diff --git a/src/binary/binary_comparator.cpp b/src/binary/binary_comparator.cpp new file mode 100644 index 00000000..b227c652 --- /dev/null +++ b/src/binary/binary_comparator.cpp @@ -0,0 +1,152 @@ +#include "quiver/binary/binary_comparator.h" + +#include "quiver/binary/binary.h" + +#include +#include +#include +#include + +namespace { + +bool is_approx(double a, double b, double atol, double rtol) { + if (std::isnan(a) && std::isnan(b)) + return true; + if (std::isnan(a) || std::isnan(b)) + return false; + return std::abs(a - b) <= atol + rtol * std::abs(b); +} + +} // namespace + +namespace quiver { + +struct BinaryComparator::Impl { + Binary binary1; + Binary binary2; + CompareOptions options; +}; + +BinaryComparator::BinaryComparator(Binary binary1, Binary binary2, const CompareOptions& options) + : impl_(std::make_unique(Impl{std::move(binary1), std::move(binary2), options})) {} + +BinaryComparator::~BinaryComparator() = default; + +BinaryComparator::BinaryComparator(BinaryComparator&& other) noexcept = default; +BinaryComparator& BinaryComparator::operator=(BinaryComparator&& other) noexcept = default; + +CompareResult +BinaryComparator::compare(const std::string& file_path1, const std::string& file_path2, const CompareOptions& options) { + auto binary1 = Binary::open_file(file_path1, 'r'); + auto binary2 = Binary::open_file(file_path2, 'r'); + BinaryComparator comparator(std::move(binary1), std::move(binary2), options); + return comparator.run(); +} + +CompareResult BinaryComparator::run() { + auto& binary1 = impl_->binary1; + auto& binary2 = impl_->binary2; + const auto& metadata1 = binary1.get_metadata(); + const auto& metadata2 = binary2.get_metadata(); + + std::string report; + + if (metadata1 != metadata2) { + if (impl_->options.detailed_report) { + report = "Metadata mismatch between files '" + binary1.get_file_path() + "' and '" + + binary2.get_file_path() + "'.\n"; + } + return {CompareStatus::MetadataMismatch, report}; + } + + const auto& metadata = metadata1; + const auto& dimensions = metadata.dimensions; + + // Get initial dimension values + std::vector initial_dimensions; + for (const auto& dim : dimensions) { + if (dim.is_time_dimension()) { + initial_dimensions.push_back(dim.time->initial_value); + } else { + initial_dimensions.push_back(1); + } + } + + // Calculate maximum number of lines + int64_t max_lines = 1; + for (const auto& dim : dimensions) { + max_lines *= dim.size; + } + + CompareStatus status = CompareStatus::FileMatch; + int report_lines = 0; + + auto current_dims = initial_dimensions; + for (int64_t i = 0; i < max_lines; ++i) { + std::unordered_map dims; + for (size_t j = 0; j < dimensions.size(); ++j) { + dims[dimensions[j].name] = current_dims[j]; + } + + auto data1 = binary1.read(dims, true); + auto data2 = binary2.read(dims, true); + + for (size_t k = 0; k < data1.size(); ++k) { + if (!is_approx(data1[k], data2[k], impl_->options.absolute_tolerance, impl_->options.relative_tolerance)) { + status = CompareStatus::DataMismatch; + + if (!impl_->options.detailed_report) { + // Early exit — no report needed + return {status, report}; + } + + // Initialize report header on first mismatch + if (report.empty()) { + report = "Data mismatch between '" + binary1.get_file_path() + "' and '" + binary2.get_file_path() + + "':\n"; + } + + // Build dimension string + std::string dim_str; + for (size_t j = 0; j < dimensions.size(); ++j) { + if (!dim_str.empty()) + dim_str += ", "; + dim_str += dimensions[j].name + "=" + std::to_string(current_dims[j]); + } + + // Build data string + std::string data_str = "'" + metadata.labels[k] + "': "; + if (std::isnan(data1[k])) { + data_str += "NaN"; + } else { + data_str += std::to_string(data1[k]); + } + data_str += " vs "; + if (std::isnan(data2[k])) { + data_str += "NaN"; + } else { + data_str += std::to_string(data2[k]); + } + + report += " {" + dim_str + "}, " + data_str + "\n"; + report_lines++; + if (report_lines >= impl_->options.max_report_lines) { + report += " more ...\n"; + return {status, report}; + } + } + } + + current_dims = binary1.next_dimensions(current_dims); + if (current_dims == initial_dimensions) + break; + } + + if (impl_->options.detailed_report && status == CompareStatus::FileMatch) { + report = "Files '" + binary1.get_file_path() + "' and '" + binary2.get_file_path() + "' match."; + } + + return {status, report}; +} + +} // namespace quiver diff --git a/src/c/binary/binary.cpp b/src/c/binary/binary.cpp index 8c761800..dc597387 100644 --- a/src/c/binary/binary.cpp +++ b/src/c/binary/binary.cpp @@ -133,43 +133,6 @@ QUIVER_C_API quiver_error_t quiver_binary_get_file_path(quiver_binary_t* binary, return QUIVER_OK; } -// Compare - -QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, - const char* path2, - int detailed_report, - quiver_compare_status_t* out_status, - char** out_report) { - QUIVER_REQUIRE(path1, path2, out_status, out_report); - - try { - auto result = quiver::Binary::compare_files(path1, path2, detailed_report != 0); - - switch (result.status) { - case quiver::CompareStatus::FileMatch: - *out_status = QUIVER_COMPARE_FILE_MATCH; - break; - case quiver::CompareStatus::MetadataMismatch: - *out_status = QUIVER_COMPARE_METADATA_MISMATCH; - break; - case quiver::CompareStatus::DataMismatch: - *out_status = QUIVER_COMPARE_DATA_MISMATCH; - break; - } - - if (result.report.empty()) { - *out_report = nullptr; - } else { - *out_report = quiver::string::new_c_str(result.report); - } - - return QUIVER_OK; - } catch (const std::exception& e) { - quiver_set_last_error(e.what()); - return QUIVER_ERROR; - } -} - // Free QUIVER_C_API quiver_error_t quiver_binary_free_string(char* str) { diff --git a/src/c/binary/binary_comparator.cpp b/src/c/binary/binary_comparator.cpp new file mode 100644 index 00000000..08061f5d --- /dev/null +++ b/src/c/binary/binary_comparator.cpp @@ -0,0 +1,62 @@ +#include "quiver/c/binary/binary_comparator.h" + +#include "../database_helpers.h" +#include "../internal.h" +#include "quiver/binary/binary_comparator.h" + +#include + +extern "C" { + +QUIVER_C_API quiver_compare_options_t quiver_compare_options_default(void) { + return {1e-6, 0.0, 0, 100}; +} + +QUIVER_C_API quiver_error_t quiver_binary_compare_files(const char* path1, + const char* path2, + const quiver_compare_options_t* options, + quiver_compare_status_t* out_status, + char** out_report) { + QUIVER_REQUIRE(path1, path2, options, out_status, out_report); + + try { + auto result = quiver::BinaryComparator::compare(path1, + path2, + {.absolute_tolerance = options->absolute_tolerance, + .relative_tolerance = options->relative_tolerance, + .detailed_report = options->detailed_report != 0, + .max_report_lines = options->max_report_lines}); + + switch (result.status) { + case quiver::CompareStatus::FileMatch: + *out_status = QUIVER_COMPARE_FILE_MATCH; + break; + case quiver::CompareStatus::MetadataMismatch: + *out_status = QUIVER_COMPARE_METADATA_MISMATCH; + break; + case quiver::CompareStatus::DataMismatch: + *out_status = QUIVER_COMPARE_DATA_MISMATCH; + break; + } + + if (result.report.empty()) { + *out_report = nullptr; + } else { + *out_report = quiver::string::new_c_str(result.report); + } + + return QUIVER_OK; + } catch (const std::exception& e) { + quiver_set_last_error(e.what()); + return QUIVER_ERROR; + } +} + +// Free + +QUIVER_C_API quiver_error_t quiver_binary_comparator_free_string(char* str) { + delete[] str; + return QUIVER_OK; +} + +} // extern "C" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1fb83101..d7d643b4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,7 @@ include(GoogleTest) add_executable(quiver_tests test_binary.cpp + test_binary_comparator.cpp test_csv_converter.cpp test_binary_metadata.cpp test_binary_time_properties.cpp @@ -49,6 +50,7 @@ gtest_discover_tests(quiver_tests if(QUIVER_BUILD_C_API) add_executable(quiver_c_tests test_c_api_binary.cpp + test_c_api_binary_comparator.cpp test_c_api_csv_converter.cpp test_c_api_binary_metadata.cpp test_c_api_database_create.cpp diff --git a/tests/test_binary.cpp b/tests/test_binary.cpp index be78241a..93569610 100644 --- a/tests/test_binary.cpp +++ b/tests/test_binary.cpp @@ -429,334 +429,3 @@ TEST_F(BinaryTempFileFixture, SingleTimeDimensionSkipsConsistencyCheck) { auto binary = Binary::open_file(path, 'w', md); EXPECT_NO_THROW(binary.write({1.0}, {{"month", 12}, {"scenario", 3}})); } - -// ============================================================================ -// Fixture for CompareFiles -// ============================================================================ - -class BinaryCompareFixture : public ::testing::Test { -protected: - void SetUp() override { - path1 = (fs::temp_directory_path() / "quiver_compare_test1").string(); - path2 = (fs::temp_directory_path() / "quiver_compare_test2").string(); - } - - void TearDown() override { - for (const auto& p : {path1, path2}) { - for (auto ext : {".qvr", ".toml"}) { - auto full = p + ext; - if (fs::exists(full)) - fs::remove(full); - } - } - } - - std::string path1; - std::string path2; - - static BinaryMetadata make_metadata() { - BinaryMetadata md; - md.version = "1"; - md.add_dimension("scenario", 2); - md.add_dimension("block", 3); - md.unit = "MW"; - md.labels = {"plant_1", "plant_2"}; - return md; - } -}; - -// ============================================================================ -// CompareFiles — Status -// ============================================================================ - -TEST_F(BinaryCompareFixture, IdenticalFilesReturnFileMatch) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::FileMatch); -} - -TEST_F(BinaryCompareFixture, BothFilesUnwrittenReturnFileMatch) { - auto md = make_metadata(); - { - Binary::open_file(path1, 'w', md); - } - { - Binary::open_file(path2, 'w', md); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::FileMatch); -} - -TEST_F(BinaryCompareFixture, DifferentDataReturnsDataMismatch) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); -} - -TEST_F(BinaryCompareFixture, WrittenVsUnwrittenReturnsDataMismatch) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - { - Binary::open_file(path2, 'w', md); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); -} - -TEST_F(BinaryCompareFixture, DifferentMetadataReturnsMetadataMismatch) { - auto md1 = make_metadata(); - auto md2 = make_metadata(); - md2.unit = "GWh"; - { - auto bin = Binary::open_file(path1, 'w', md1); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - { - auto bin = Binary::open_file(path2, 'w', md2); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); -} - -TEST_F(BinaryCompareFixture, DifferentDimensionSizesReturnsMetadataMismatch) { - auto md1 = make_metadata(); - BinaryMetadata md2; - md2.version = "1"; - md2.add_dimension("scenario", 4); - md2.add_dimension("block", 3); - md2.unit = "MW"; - md2.labels = {"plant_1", "plant_2"}; - { - Binary::open_file(path1, 'w', md1); - } - { - Binary::open_file(path2, 'w', md2); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); -} - -TEST_F(BinaryCompareFixture, DifferentLabelsReturnsMetadataMismatch) { - auto md1 = make_metadata(); - auto md2 = make_metadata(); - md2.labels = {"gen_1", "gen_2"}; - { - Binary::open_file(path1, 'w', md1); - } - { - Binary::open_file(path2, 'w', md2); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); -} - -// ============================================================================ -// CompareFiles — Report with detailed_report=true -// ============================================================================ - -TEST_F(BinaryCompareFixture, FileMatchReportIsNotEmpty) { - auto md = make_metadata(); - { - Binary::open_file(path1, 'w', md); - } - { - Binary::open_file(path2, 'w', md); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::FileMatch); - EXPECT_FALSE(result.report.empty()); -} - -TEST_F(BinaryCompareFixture, DataMismatchReportContainsDimensionInfo) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 2}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 2}}); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); - EXPECT_NE(result.report.find("scenario=1"), std::string::npos); - EXPECT_NE(result.report.find("block=2"), std::string::npos); - EXPECT_NE(result.report.find("plant_1"), std::string::npos); -} - -TEST_F(BinaryCompareFixture, DataMismatchReportContainsValues) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_NE(result.report.find("10.0"), std::string::npos); - EXPECT_NE(result.report.find("99.0"), std::string::npos); -} - -TEST_F(BinaryCompareFixture, DataMismatchReportShowsNaN) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - // leave position unwritten (NaN) - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({5.0, 6.0}, {{"scenario", 1}, {"block", 1}}); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); - EXPECT_NE(result.report.find("NaN"), std::string::npos); -} - -TEST_F(BinaryCompareFixture, DataMismatchReportOnlyShowsDifferingPositions) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); // same - bin.write({99.0, 40.0}, {{"scenario", 1}, {"block", 2}}); // different - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); - // block=2 should appear (mismatch), block=1 should not (match) - EXPECT_NE(result.report.find("block=2"), std::string::npos); - EXPECT_EQ(result.report.find("block=1"), std::string::npos); -} - -TEST_F(BinaryCompareFixture, DataMismatchReportTruncatesAtMaxLines) { - // Use larger dimensions to exceed MAX_REPORT_LINES (100) - // 10 scenarios x 11 blocks x 1 label = 110 mismatch lines - BinaryMetadata md; - md.version = "1"; - md.add_dimension("scenario", 10); - md.add_dimension("block", 11); - md.unit = "MW"; - md.labels = {"val"}; - - { - auto bin = Binary::open_file(path1, 'w', md); - for (int64_t s = 1; s <= 10; ++s) - for (int64_t b = 1; b <= 11; ++b) - bin.write({1.0}, {{"scenario", s}, {"block", b}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - for (int64_t s = 1; s <= 10; ++s) - for (int64_t b = 1; b <= 11; ++b) - bin.write({99.0}, {{"scenario", s}, {"block", b}}); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); - EXPECT_NE(result.report.find("more ..."), std::string::npos); -} - -TEST_F(BinaryCompareFixture, MetadataMismatchReportIsNotEmpty) { - auto md1 = make_metadata(); - auto md2 = make_metadata(); - md2.unit = "GWh"; - { - Binary::open_file(path1, 'w', md1); - } - { - Binary::open_file(path2, 'w', md2); - } - - auto result = Binary::compare_files(path1, path2, true); - EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); - EXPECT_FALSE(result.report.empty()); -} - -// ============================================================================ -// CompareFiles — Report with detailed_report=false (default) -// ============================================================================ - -TEST_F(BinaryCompareFixture, FileMatchNoReportIsEmpty) { - auto md = make_metadata(); - { - Binary::open_file(path1, 'w', md); - } - { - Binary::open_file(path2, 'w', md); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::FileMatch); - EXPECT_TRUE(result.report.empty()); -} - -TEST_F(BinaryCompareFixture, DataMismatchNoReportIsEmpty) { - auto md = make_metadata(); - { - auto bin = Binary::open_file(path1, 'w', md); - bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - { - auto bin = Binary::open_file(path2, 'w', md); - bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::DataMismatch); - EXPECT_TRUE(result.report.empty()); -} - -TEST_F(BinaryCompareFixture, MetadataMismatchNoReportIsEmpty) { - auto md1 = make_metadata(); - auto md2 = make_metadata(); - md2.unit = "GWh"; - { - Binary::open_file(path1, 'w', md1); - } - { - Binary::open_file(path2, 'w', md2); - } - - auto result = Binary::compare_files(path1, path2); - EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); - EXPECT_TRUE(result.report.empty()); -} diff --git a/tests/test_binary_comparator.cpp b/tests/test_binary_comparator.cpp new file mode 100644 index 00000000..930acbc3 --- /dev/null +++ b/tests/test_binary_comparator.cpp @@ -0,0 +1,416 @@ +#include +#include +#include +#include +#include + +using namespace quiver; +namespace fs = std::filesystem; + +// ============================================================================ +// Fixture +// ============================================================================ + +class BinaryCompareFixture : public ::testing::Test { +protected: + void SetUp() override { + path1 = (fs::temp_directory_path() / "quiver_compare_test1").string(); + path2 = (fs::temp_directory_path() / "quiver_compare_test2").string(); + } + + void TearDown() override { + for (const auto& p : {path1, path2}) { + for (auto ext : {".qvr", ".toml"}) { + auto full = p + ext; + if (fs::exists(full)) + fs::remove(full); + } + } + } + + std::string path1; + std::string path2; + + static BinaryMetadata make_metadata() { + BinaryMetadata md; + md.version = "1"; + md.add_dimension("scenario", 2); + md.add_dimension("block", 3); + md.unit = "MW"; + md.labels = {"plant_1", "plant_2"}; + return md; + } +}; + +// ============================================================================ +// CompareFiles — Status +// ============================================================================ + +TEST_F(BinaryCompareFixture, IdenticalFilesReturnFileMatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, BothFilesUnwrittenReturnFileMatch) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, DifferentDataReturnsDataMismatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, WrittenVsUnwrittenReturnsDataMismatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentMetadataReturnsMetadataMismatch) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + auto bin = Binary::open_file(path1, 'w', md1); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md2); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentDimensionSizesReturnsMetadataMismatch) { + auto md1 = make_metadata(); + BinaryMetadata md2; + md2.version = "1"; + md2.add_dimension("scenario", 4); + md2.add_dimension("block", 3); + md2.unit = "MW"; + md2.labels = {"plant_1", "plant_2"}; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +TEST_F(BinaryCompareFixture, DifferentLabelsReturnsMetadataMismatch) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.labels = {"gen_1", "gen_2"}; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); +} + +// ============================================================================ +// CompareFiles — Report with detailed_report=true +// ============================================================================ + +TEST_F(BinaryCompareFixture, FileMatchReportIsNotEmpty) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::FileMatch); + EXPECT_FALSE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportContainsDimensionInfo) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 2}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("scenario=1"), std::string::npos); + EXPECT_NE(result.report.find("block=2"), std::string::npos); + EXPECT_NE(result.report.find("plant_1"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportContainsValues) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_NE(result.report.find("10.0"), std::string::npos); + EXPECT_NE(result.report.find("99.0"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportShowsNaN) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + // leave position unwritten (NaN) + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({5.0, 6.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("NaN"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, DataMismatchReportOnlyShowsDifferingPositions) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + bin.write({30.0, 40.0}, {{"scenario", 1}, {"block", 2}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); // same + bin.write({99.0, 40.0}, {{"scenario", 1}, {"block", 2}}); // different + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + // block=2 should appear (mismatch), block=1 should not (match) + EXPECT_NE(result.report.find("block=2"), std::string::npos); + EXPECT_EQ(result.report.find("block=1"), std::string::npos); +} + +TEST_F(BinaryCompareFixture, CustomMaxReportLinesTruncatesReport) { + // 2 scenarios x 3 blocks x 2 labels = 12 mismatch lines, truncated at 5 + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + for (int64_t s = 1; s <= 2; ++s) + for (int64_t b = 1; b <= 3; ++b) + bin.write({1.0, 2.0}, {{"scenario", s}, {"block", b}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + for (int64_t s = 1; s <= 2; ++s) + for (int64_t b = 1; b <= 3; ++b) + bin.write({99.0, 98.0}, {{"scenario", s}, {"block", b}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true, .max_report_lines = 5}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_NE(result.report.find("more ..."), std::string::npos); +} + +TEST_F(BinaryCompareFixture, MetadataMismatchReportIsNotEmpty) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = BinaryComparator::compare(path1, path2, {.detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); + EXPECT_FALSE(result.report.empty()); +} + +// ============================================================================ +// CompareFiles — Report with detailed_report=false (default) +// ============================================================================ + +TEST_F(BinaryCompareFixture, FileMatchNoReportIsEmpty) { + auto md = make_metadata(); + { + Binary::open_file(path1, 'w', md); + } + { + Binary::open_file(path2, 'w', md); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::FileMatch); + EXPECT_TRUE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, DataMismatchNoReportIsEmpty) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({99.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_TRUE(result.report.empty()); +} + +TEST_F(BinaryCompareFixture, MetadataMismatchNoReportIsEmpty) { + auto md1 = make_metadata(); + auto md2 = make_metadata(); + md2.unit = "GWh"; + { + Binary::open_file(path1, 'w', md1); + } + { + Binary::open_file(path2, 'w', md2); + } + + auto result = BinaryComparator::compare(path1, path2); + EXPECT_EQ(result.status, CompareStatus::MetadataMismatch); + EXPECT_TRUE(result.report.empty()); +} + +// ============================================================================ +// CompareFiles — Tolerance +// ============================================================================ + +TEST_F(BinaryCompareFixture, AbsoluteToleranceMatchesWithinThreshold) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.05, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.absolute_tolerance = 0.1}); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, AbsoluteToleranceFailsOutsideThreshold) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({10.05, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.absolute_tolerance = 0.01}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, RelativeToleranceMatch) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({100.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({100.05, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + // diff = 0.05, threshold = 0.0 + 0.001 * 100.05 = 0.10005 + auto result = BinaryComparator::compare(path1, path2, {.absolute_tolerance = 0.0, .relative_tolerance = 0.001}); + EXPECT_EQ(result.status, CompareStatus::FileMatch); +} + +TEST_F(BinaryCompareFixture, RelativeToleranceFailsOutsideThreshold) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({100.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + bin.write({100.2, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + + // diff = 0.2, threshold = 0.0 + 0.001 * 100.2 = 0.1002 + auto result = BinaryComparator::compare(path1, path2, {.absolute_tolerance = 0.0, .relative_tolerance = 0.001}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); +} + +TEST_F(BinaryCompareFixture, ToleranceReportOnlyShowsOutOfTolerance) { + auto md = make_metadata(); + { + auto bin = Binary::open_file(path1, 'w', md); + bin.write({10.0, 20.0}, {{"scenario", 1}, {"block", 1}}); + } + { + auto bin = Binary::open_file(path2, 'w', md); + // plant_1: diff = 0.01 (within atol=0.1), plant_2: diff = 5.0 (outside) + bin.write({10.01, 25.0}, {{"scenario", 1}, {"block", 1}}); + } + + auto result = BinaryComparator::compare(path1, path2, {.absolute_tolerance = 0.1, .detailed_report = true}); + EXPECT_EQ(result.status, CompareStatus::DataMismatch); + EXPECT_EQ(result.report.find("plant_1"), std::string::npos); + EXPECT_NE(result.report.find("plant_2"), std::string::npos); +} diff --git a/tests/test_c_api_binary.cpp b/tests/test_c_api_binary.cpp index 46b905a3..15fbaff0 100644 --- a/tests/test_c_api_binary.cpp +++ b/tests/test_c_api_binary.cpp @@ -460,165 +460,3 @@ TEST_F(BinaryCApiFixture, ReadAllowNulls) { quiver_binary_close(binary); } - -// ============================================================================ -// Compare files -// ============================================================================ - -class BinaryCApiCompareFixture : public ::testing::Test { -protected: - void SetUp() override { - path1 = (fs::temp_directory_path() / "quiver_c_compare_test_1").string(); - path2 = (fs::temp_directory_path() / "quiver_c_compare_test_2").string(); - } - - void TearDown() override { - for (auto* p : {&path1, &path2}) { - for (auto ext : {".qvr", ".toml"}) { - auto full = *p + ext; - if (fs::exists(full)) - fs::remove(full); - } - } - } - - std::string path1; - std::string path2; - - quiver_binary_metadata_t* make_metadata() { - quiver_element_t* el = nullptr; - quiver_element_create(&el); - quiver_element_set_string(el, "version", "1"); - quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); - quiver_element_set_string(el, "unit", "MW"); - - const char* dims[] = {"scenario", "block"}; - quiver_element_set_array_string(el, "dimensions", dims, 2); - int64_t sizes[] = {2, 3}; - quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); - const char* labels[] = {"plant_1", "plant_2"}; - quiver_element_set_array_string(el, "labels", labels, 2); - - quiver_binary_metadata_t* md = nullptr; - quiver_binary_metadata_from_element(el, &md); - quiver_element_destroy(el); - return md; - } - - void write_file(const std::string& path, quiver_binary_metadata_t* md, double val1, double val2) { - quiver_binary_t* binary = nullptr; - quiver_binary_open_write(path.c_str(), md, &binary); - - const char* dim_names[] = {"scenario", "block"}; - int64_t dim_values[] = {1, 1}; - double data[] = {val1, val2}; - quiver_binary_write(binary, dim_names, dim_values, 2, data, 2); - quiver_binary_close(binary); - } -}; - -TEST_F(BinaryCApiCompareFixture, FileMatch) { - auto* md = make_metadata(); - write_file(path1, md, 10.0, 20.0); - write_file(path2, md, 10.0, 20.0); - quiver_binary_metadata_free(md); - - quiver_compare_status_t status; - char* report = nullptr; - ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); - EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); - EXPECT_EQ(report, nullptr); -} - -TEST_F(BinaryCApiCompareFixture, DataMismatch) { - auto* md = make_metadata(); - write_file(path1, md, 10.0, 20.0); - write_file(path2, md, 99.0, 20.0); - quiver_binary_metadata_free(md); - - quiver_compare_status_t status; - char* report = nullptr; - ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); - EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); - EXPECT_EQ(report, nullptr); -} - -TEST_F(BinaryCApiCompareFixture, MetadataMismatch) { - auto* md1 = make_metadata(); - write_file(path1, md1, 10.0, 20.0); - quiver_binary_metadata_free(md1); - - // Create second file with different unit - quiver_element_t* el = nullptr; - quiver_element_create(&el); - quiver_element_set_string(el, "version", "1"); - quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); - quiver_element_set_string(el, "unit", "GWh"); - const char* dims[] = {"scenario", "block"}; - quiver_element_set_array_string(el, "dimensions", dims, 2); - int64_t sizes[] = {2, 3}; - quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); - const char* labels[] = {"plant_1", "plant_2"}; - quiver_element_set_array_string(el, "labels", labels, 2); - - quiver_binary_metadata_t* md2 = nullptr; - quiver_binary_metadata_from_element(el, &md2); - quiver_element_destroy(el); - - write_file(path2, md2, 10.0, 20.0); - quiver_binary_metadata_free(md2); - - quiver_compare_status_t status; - char* report = nullptr; - ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 0, &status, &report), QUIVER_OK); - EXPECT_EQ(status, QUIVER_COMPARE_METADATA_MISMATCH); - EXPECT_EQ(report, nullptr); -} - -TEST_F(BinaryCApiCompareFixture, DetailedReportOnMatch) { - auto* md = make_metadata(); - write_file(path1, md, 10.0, 20.0); - write_file(path2, md, 10.0, 20.0); - quiver_binary_metadata_free(md); - - quiver_compare_status_t status; - char* report = nullptr; - ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 1, &status, &report), QUIVER_OK); - EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); - ASSERT_NE(report, nullptr); - quiver_binary_free_string(report); -} - -TEST_F(BinaryCApiCompareFixture, DetailedReportOnDataMismatch) { - auto* md = make_metadata(); - write_file(path1, md, 10.0, 20.0); - write_file(path2, md, 99.0, 20.0); - quiver_binary_metadata_free(md); - - quiver_compare_status_t status; - char* report = nullptr; - ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), 1, &status, &report), QUIVER_OK); - EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); - ASSERT_NE(report, nullptr); - - std::string report_str(report); - EXPECT_NE(report_str.find("plant_1"), std::string::npos); - quiver_binary_free_string(report); -} - -TEST_F(BinaryCApiCompareFixture, NullArgs) { - quiver_compare_status_t status; - char* report = nullptr; - EXPECT_EQ(quiver_binary_compare_files(nullptr, nullptr, 0, &status, &report), QUIVER_ERROR); - EXPECT_STREQ(quiver_get_last_error(), "Null argument: path1"); -} - -TEST_F(BinaryCApiCompareFixture, NonExistentFile) { - auto* md = make_metadata(); - write_file(path1, md, 10.0, 20.0); - quiver_binary_metadata_free(md); - - quiver_compare_status_t status; - char* report = nullptr; - EXPECT_EQ(quiver_binary_compare_files(path1.c_str(), "nonexistent", 0, &status, &report), QUIVER_ERROR); -} diff --git a/tests/test_c_api_binary_comparator.cpp b/tests/test_c_api_binary_comparator.cpp new file mode 100644 index 00000000..c925257a --- /dev/null +++ b/tests/test_c_api_binary_comparator.cpp @@ -0,0 +1,185 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +// ============================================================================ +// Fixture +// ============================================================================ + +class BinaryCApiCompareFixture : public ::testing::Test { +protected: + void SetUp() override { + path1 = (fs::temp_directory_path() / "quiver_c_compare_test_1").string(); + path2 = (fs::temp_directory_path() / "quiver_c_compare_test_2").string(); + } + + void TearDown() override { + for (auto* p : {&path1, &path2}) { + for (auto ext : {".qvr", ".toml"}) { + auto full = *p + ext; + if (fs::exists(full)) + fs::remove(full); + } + } + } + + std::string path1; + std::string path2; + + quiver_binary_metadata_t* make_metadata() { + quiver_element_t* el = nullptr; + quiver_element_create(&el); + quiver_element_set_string(el, "version", "1"); + quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); + quiver_element_set_string(el, "unit", "MW"); + + const char* dims[] = {"scenario", "block"}; + quiver_element_set_array_string(el, "dimensions", dims, 2); + int64_t sizes[] = {2, 3}; + quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); + const char* labels[] = {"plant_1", "plant_2"}; + quiver_element_set_array_string(el, "labels", labels, 2); + + quiver_binary_metadata_t* md = nullptr; + quiver_binary_metadata_from_element(el, &md); + quiver_element_destroy(el); + return md; + } + + void write_file(const std::string& path, quiver_binary_metadata_t* md, double val1, double val2) { + quiver_binary_t* binary = nullptr; + quiver_binary_open_write(path.c_str(), md, &binary); + + const char* dim_names[] = {"scenario", "block"}; + int64_t dim_values[] = {1, 1}; + double data[] = {val1, val2}; + quiver_binary_write(binary, dim_names, dim_values, 2, data, 2); + quiver_binary_close(binary); + } +}; + +// ============================================================================ +// Tests +// ============================================================================ + +TEST_F(BinaryCApiCompareFixture, FileMatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + auto options = quiver_compare_options_default(); + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), &options, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, DataMismatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 99.0, 20.0); + quiver_binary_metadata_free(md); + + auto options = quiver_compare_options_default(); + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), &options, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, MetadataMismatch) { + auto* md1 = make_metadata(); + write_file(path1, md1, 10.0, 20.0); + quiver_binary_metadata_free(md1); + + // Create second file with different unit + quiver_element_t* el = nullptr; + quiver_element_create(&el); + quiver_element_set_string(el, "version", "1"); + quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); + quiver_element_set_string(el, "unit", "GWh"); + const char* dims[] = {"scenario", "block"}; + quiver_element_set_array_string(el, "dimensions", dims, 2); + int64_t sizes[] = {2, 3}; + quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); + const char* labels[] = {"plant_1", "plant_2"}; + quiver_element_set_array_string(el, "labels", labels, 2); + + quiver_binary_metadata_t* md2 = nullptr; + quiver_binary_metadata_from_element(el, &md2); + quiver_element_destroy(el); + + write_file(path2, md2, 10.0, 20.0); + quiver_binary_metadata_free(md2); + + auto options = quiver_compare_options_default(); + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), &options, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_METADATA_MISMATCH); + EXPECT_EQ(report, nullptr); +} + +TEST_F(BinaryCApiCompareFixture, DetailedReportOnMatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + auto options = quiver_compare_options_default(); + options.detailed_report = 1; + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), &options, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_FILE_MATCH); + ASSERT_NE(report, nullptr); + quiver_binary_comparator_free_string(report); +} + +TEST_F(BinaryCApiCompareFixture, DetailedReportOnDataMismatch) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + write_file(path2, md, 99.0, 20.0); + quiver_binary_metadata_free(md); + + auto options = quiver_compare_options_default(); + options.detailed_report = 1; + quiver_compare_status_t status; + char* report = nullptr; + ASSERT_EQ(quiver_binary_compare_files(path1.c_str(), path2.c_str(), &options, &status, &report), QUIVER_OK); + EXPECT_EQ(status, QUIVER_COMPARE_DATA_MISMATCH); + ASSERT_NE(report, nullptr); + + std::string report_str(report); + EXPECT_NE(report_str.find("plant_1"), std::string::npos); + quiver_binary_comparator_free_string(report); +} + +TEST_F(BinaryCApiCompareFixture, NullArgs) { + auto options = quiver_compare_options_default(); + quiver_compare_status_t status; + char* report = nullptr; + EXPECT_EQ(quiver_binary_compare_files(nullptr, nullptr, &options, &status, &report), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: path1"); +} + +TEST_F(BinaryCApiCompareFixture, NonExistentFile) { + auto* md = make_metadata(); + write_file(path1, md, 10.0, 20.0); + quiver_binary_metadata_free(md); + + auto options = quiver_compare_options_default(); + quiver_compare_status_t status; + char* report = nullptr; + EXPECT_EQ(quiver_binary_compare_files(path1.c_str(), "nonexistent", &options, &status, &report), QUIVER_ERROR); +} From 809b21f64ecd253cedde404ba533bec7307365a5 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Thu, 19 Mar 2026 16:07:14 -0300 Subject: [PATCH 06/10] format merge changes --- tests/test_binary_comparator.cpp | 2 +- tests/test_c_api_binary_comparator.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_binary_comparator.cpp b/tests/test_binary_comparator.cpp index 795806a6..97987fa5 100644 --- a/tests/test_binary_comparator.cpp +++ b/tests/test_binary_comparator.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include using namespace quiver; diff --git a/tests/test_c_api_binary_comparator.cpp b/tests/test_c_api_binary_comparator.cpp index 8122c858..b782bea0 100644 --- a/tests/test_c_api_binary_comparator.cpp +++ b/tests/test_c_api_binary_comparator.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include #include #include From 0c72382a6cd2963c287b4fd3d8234fafe9105206 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 25 Mar 2026 10:11:08 -0300 Subject: [PATCH 07/10] Add binding for BinaryFile.next_dimensions --- bindings/julia/src/binary/file.jl | 21 +++++++++++++++++++++ bindings/julia/src/c_api.jl | 8 ++++++++ include/quiver/binary/binary_file.h | 4 +++- include/quiver/c/binary/binary_file.h | 8 ++++++++ src/c/binary/binary_file.cpp | 27 +++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/bindings/julia/src/binary/file.jl b/bindings/julia/src/binary/file.jl index 6fc0aa58..4b93f0d7 100644 --- a/bindings/julia/src/binary/file.jl +++ b/bindings/julia/src/binary/file.jl @@ -71,6 +71,27 @@ function write!(file::File; data::Vector{Float64}, dims...) return nothing end +function next_dimensions(file::File, current_dimensions::Vector{Int64}) + out_dims = Ref{Ptr{Int64}}(C_NULL) + out_count = Ref{Csize_t}(0) + + check( + C.quiver_binary_file_next_dimensions( + file.ptr, current_dimensions, length(current_dimensions), + out_dims, out_count, + ), + ) + + count = out_count[] + if count == 0 || out_dims[] == C_NULL + return Int64[] + end + + result = [unsafe_load(out_dims[], i) for i in 1:count] + C.quiver_binary_file_free_int64_array(out_dims[]) + return result +end + function get_metadata(file::File) out_md = Ref{Ptr{C.quiver_binary_metadata}}(C_NULL) check(C.quiver_binary_file_get_metadata(file.ptr, out_md)) diff --git a/bindings/julia/src/c_api.jl b/bindings/julia/src/c_api.jl index 14d27a76..a91a93cc 100644 --- a/bindings/julia/src/c_api.jl +++ b/bindings/julia/src/c_api.jl @@ -615,6 +615,10 @@ function quiver_binary_file_write(binary_file, dim_names, dim_values, dim_count, @ccall libquiver_c.quiver_binary_file_write(binary_file::Ptr{quiver_binary_file_t}, dim_names::Ptr{Ptr{Cchar}}, dim_values::Ptr{Int64}, dim_count::Csize_t, data::Ptr{Cdouble}, data_count::Csize_t)::quiver_error_t end +function quiver_binary_file_next_dimensions(binary_file, current_dimensions, dim_count, out_dimensions, out_count) + @ccall libquiver_c.quiver_binary_file_next_dimensions(binary_file::Ptr{quiver_binary_file_t}, current_dimensions::Ptr{Int64}, dim_count::Csize_t, out_dimensions::Ptr{Ptr{Int64}}, out_count::Ptr{Csize_t})::quiver_error_t +end + function quiver_binary_file_get_metadata(binary_file, out) @ccall libquiver_c.quiver_binary_file_get_metadata(binary_file::Ptr{quiver_binary_file_t}, out::Ptr{Ptr{quiver_binary_metadata_t}})::quiver_error_t end @@ -656,6 +660,10 @@ function quiver_binary_file_free_float_array(data) @ccall libquiver_c.quiver_binary_file_free_float_array(data::Ptr{Cdouble})::quiver_error_t end +function quiver_binary_file_free_int64_array(data) + @ccall libquiver_c.quiver_binary_file_free_int64_array(data::Ptr{Int64})::quiver_error_t +end + # ============================================================================ # Binary CSV functions # ============================================================================ diff --git a/include/quiver/binary/binary_file.h b/include/quiver/binary/binary_file.h index ca971789..0c7195ac 100644 --- a/include/quiver/binary/binary_file.h +++ b/include/quiver/binary/binary_file.h @@ -43,12 +43,14 @@ class QUIVER_API BinaryFile { // Dimension iteration std::vector next_dimensions(const std::vector& current_dimensions); - std::vector dimension_sizes_at_values(const std::vector& dimension_values) const; private: struct Impl; std::unique_ptr impl_; + // Dimension helpers + std::vector dimension_sizes_at_values(const std::vector& dimension_values) const; + // File traversal int64_t calculate_file_position(const std::unordered_map& dims) const; void go_to_position(int64_t position, char mode); diff --git a/include/quiver/c/binary/binary_file.h b/include/quiver/c/binary/binary_file.h index 8d151987..e13b5ba3 100644 --- a/include/quiver/c/binary/binary_file.h +++ b/include/quiver/c/binary/binary_file.h @@ -33,6 +33,13 @@ QUIVER_C_API quiver_error_t quiver_binary_file_write(quiver_binary_file_t* binar const double* data, size_t data_count); +// Dimension iteration +QUIVER_C_API quiver_error_t quiver_binary_file_next_dimensions(quiver_binary_file_t* binary_file, + const int64_t* current_dimensions, + size_t dim_count, + int64_t** out_dimensions, + size_t* out_count); + // Getters QUIVER_C_API quiver_error_t quiver_binary_file_get_metadata(quiver_binary_file_t* binary_file, quiver_binary_metadata_t** out); @@ -41,6 +48,7 @@ QUIVER_C_API quiver_error_t quiver_binary_file_get_file_path(quiver_binary_file_ // Free QUIVER_C_API quiver_error_t quiver_binary_file_free_string(char* str); QUIVER_C_API quiver_error_t quiver_binary_file_free_float_array(double* data); +QUIVER_C_API quiver_error_t quiver_binary_file_free_int64_array(int64_t* data); #ifdef __cplusplus } diff --git a/src/c/binary/binary_file.cpp b/src/c/binary/binary_file.cpp index ffcbe822..2b051156 100644 --- a/src/c/binary/binary_file.cpp +++ b/src/c/binary/binary_file.cpp @@ -112,6 +112,28 @@ QUIVER_C_API quiver_error_t quiver_binary_file_write(quiver_binary_file_t* binar } } +// Dimension iteration + +QUIVER_C_API quiver_error_t quiver_binary_file_next_dimensions(quiver_binary_file_t* binary_file, + const int64_t* current_dimensions, + size_t dim_count, + int64_t** out_dimensions, + size_t* out_count) { + QUIVER_REQUIRE(binary_file, current_dimensions, out_dimensions, out_count); + + try { + std::vector current(current_dimensions, current_dimensions + dim_count); + auto result = binary_file->binary_file.next_dimensions(current); + *out_count = result.size(); + *out_dimensions = new int64_t[result.size()]; + std::copy(result.begin(), result.end(), *out_dimensions); + return QUIVER_OK; + } catch (const std::exception& e) { + quiver_set_last_error(e.what()); + return QUIVER_ERROR; + } +} + // Getters QUIVER_C_API quiver_error_t quiver_binary_file_get_metadata(quiver_binary_file_t* binary_file, @@ -146,4 +168,9 @@ QUIVER_C_API quiver_error_t quiver_binary_file_free_float_array(double* data) { return QUIVER_OK; } +QUIVER_C_API quiver_error_t quiver_binary_file_free_int64_array(int64_t* data) { + delete[] data; + return QUIVER_OK; +} + } // extern "C" From e664246d188eb9b09a680898bfa4cdd6ad527f27 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 25 Mar 2026 18:16:20 -0300 Subject: [PATCH 08/10] Add simple next_dimension tests in C and julia --- bindings/julia/test/test_binary_file.jl | 93 ++++++++++++++++ tests/test_c_api_binary_file.cpp | 138 ++++++++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/bindings/julia/test/test_binary_file.jl b/bindings/julia/test/test_binary_file.jl index 2be39b6f..76ae0ed9 100644 --- a/bindings/julia/test/test_binary_file.jl +++ b/bindings/julia/test/test_binary_file.jl @@ -561,6 +561,99 @@ end cleanup_binary_file(path) end end + + # ========================================================================== + # next_dimensions + # ========================================================================== + + @testset "next_dimensions simple increment" begin + path = make_binary_file_path() + try + md = make_simple_metadata() + file = Quiver.Binary.open_file(path; mode = :write, metadata = md) + # [1, 1] -> [1, 2] + next = Quiver.Binary.next_dimensions(file, Int64[1, 1]) + @test next == Int64[1, 2] + Quiver.Binary.close!(file) + finally + cleanup_binary_file(path) + end + end + + @testset "next_dimensions col rollover" begin + path = make_binary_file_path() + try + md = make_simple_metadata() + file = Quiver.Binary.open_file(path; mode = :write, metadata = md) + # col at max (2), row increments: [1, 2] -> [2, 1] + next = Quiver.Binary.next_dimensions(file, Int64[1, 2]) + @test next == Int64[2, 1] + Quiver.Binary.close!(file) + finally + cleanup_binary_file(path) + end + end + + @testset "next_dimensions wrap around" begin + path = make_binary_file_path() + try + md = make_simple_metadata() + file = Quiver.Binary.open_file(path; mode = :write, metadata = md) + # All at max [3, 2] -> [1, 1] + next = Quiver.Binary.next_dimensions(file, Int64[3, 2]) + @test next == Int64[1, 1] + Quiver.Binary.close!(file) + finally + cleanup_binary_file(path) + end + end + + @testset "next_dimensions time dimension variable month length" begin + path = make_binary_file_path() + try + md = make_time_metadata() + file = Quiver.Binary.open_file(path; mode = :write, metadata = md) + + # Jan has 31 days: [1, 31] -> [2, 1] + next = Quiver.Binary.next_dimensions(file, Int64[1, 31]) + @test next == Int64[2, 1] + + # Feb 2025 has 28 days: [2, 28] -> [3, 1] + next = Quiver.Binary.next_dimensions(file, Int64[2, 28]) + @test next == Int64[3, 1] + + Quiver.Binary.close!(file) + finally + cleanup_binary_file(path) + end + end + + @testset "next_dimensions full iteration covers all positions" begin + path = make_binary_file_path() + try + md = make_simple_metadata() + file = Quiver.Binary.open_file(path; mode = :write, metadata = md) + + current = Int64[1, 1] + positions = [copy(current)] + # 3 rows * 2 cols = 6 positions total; iterate 5 more steps + for _ in 1:5 + current = Quiver.Binary.next_dimensions(file, current) + push!(positions, copy(current)) + end + + @test positions[1] == Int64[1, 1] + @test positions[2] == Int64[1, 2] + @test positions[3] == Int64[2, 1] + @test positions[4] == Int64[2, 2] + @test positions[5] == Int64[3, 1] + @test positions[6] == Int64[3, 2] + + Quiver.Binary.close!(file) + finally + cleanup_binary_file(path) + end + end end end diff --git a/tests/test_c_api_binary_file.cpp b/tests/test_c_api_binary_file.cpp index 2203df5a..5ce43c25 100644 --- a/tests/test_c_api_binary_file.cpp +++ b/tests/test_c_api_binary_file.cpp @@ -396,6 +396,144 @@ TEST_F(BinaryCApiFixture, FreeFloatArrayNull) { EXPECT_EQ(quiver_binary_file_free_float_array(nullptr), QUIVER_OK); } +// ============================================================================ +// next_dimensions +// ============================================================================ + +TEST_F(BinaryCApiFixture, NextDimensionsSimpleIncrement) { + auto* md = make_simple_metadata(); + quiver_binary_file_t* binary_file = nullptr; + ASSERT_EQ(quiver_binary_file_open_write(path.c_str(), md, &binary_file), QUIVER_OK); + + // [1, 1] -> [1, 2] + int64_t current[] = {1, 1}; + int64_t* out_dims = nullptr; + size_t out_count = 0; + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, &out_dims, &out_count), QUIVER_OK); + ASSERT_EQ(out_count, 2u); + EXPECT_EQ(out_dims[0], 1); + EXPECT_EQ(out_dims[1], 2); + quiver_binary_file_free_int64_array(out_dims); + + quiver_binary_file_close(binary_file); + quiver_binary_metadata_free(md); +} + +TEST_F(BinaryCApiFixture, NextDimensionsColRollover) { + auto* md = make_simple_metadata(); + quiver_binary_file_t* binary_file = nullptr; + ASSERT_EQ(quiver_binary_file_open_write(path.c_str(), md, &binary_file), QUIVER_OK); + + // col at max (2), row increments: [1, 2] -> [2, 1] + int64_t current[] = {1, 2}; + int64_t* out_dims = nullptr; + size_t out_count = 0; + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, &out_dims, &out_count), QUIVER_OK); + ASSERT_EQ(out_count, 2u); + EXPECT_EQ(out_dims[0], 2); + EXPECT_EQ(out_dims[1], 1); + quiver_binary_file_free_int64_array(out_dims); + + quiver_binary_file_close(binary_file); + quiver_binary_metadata_free(md); +} + +TEST_F(BinaryCApiFixture, NextDimensionsWrapAround) { + auto* md = make_simple_metadata(); + quiver_binary_file_t* binary_file = nullptr; + ASSERT_EQ(quiver_binary_file_open_write(path.c_str(), md, &binary_file), QUIVER_OK); + + // All at max: [3, 2] -> [1, 1] (wraps around) + int64_t current[] = {3, 2}; + int64_t* out_dims = nullptr; + size_t out_count = 0; + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, &out_dims, &out_count), QUIVER_OK); + ASSERT_EQ(out_count, 2u); + EXPECT_EQ(out_dims[0], 1); + EXPECT_EQ(out_dims[1], 1); + quiver_binary_file_free_int64_array(out_dims); + + quiver_binary_file_close(binary_file); + quiver_binary_metadata_free(md); +} + +TEST_F(BinaryCApiFixture, NextDimensionsTimeDimensionVariableMonthLength) { + // Monthly + daily: starting 2025-01-01, Jan has 31 days. + // [1, 31] -> [2, 1] (end of Jan, move to Feb day 1) + quiver_element_t* el = nullptr; + quiver_element_create(&el); + quiver_element_set_string(el, "version", "1"); + quiver_element_set_string(el, "initial_datetime", "2025-01-01T00:00:00"); + quiver_element_set_string(el, "unit", "MW"); + const char* dims[] = {"stage", "block"}; + quiver_element_set_array_string(el, "dimensions", dims, 2); + int64_t sizes[] = {4, 31}; + quiver_element_set_array_integer(el, "dimension_sizes", sizes, 2); + const char* labels[] = {"plant_1"}; + quiver_element_set_array_string(el, "labels", labels, 1); + const char* time_dims[] = {"stage", "block"}; + quiver_element_set_array_string(el, "time_dimensions", time_dims, 2); + const char* freqs[] = {"monthly", "daily"}; + quiver_element_set_array_string(el, "frequencies", freqs, 2); + + quiver_binary_metadata_t* md = nullptr; + ASSERT_EQ(quiver_binary_metadata_from_element(el, &md), QUIVER_OK); + quiver_element_destroy(el); + + quiver_binary_file_t* binary_file = nullptr; + ASSERT_EQ(quiver_binary_file_open_write(path.c_str(), md, &binary_file), QUIVER_OK); + + // Jan has 31 days: [1, 31] -> [2, 1] + int64_t current[] = {1, 31}; + int64_t* out_dims = nullptr; + size_t out_count = 0; + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, &out_dims, &out_count), QUIVER_OK); + ASSERT_EQ(out_count, 2u); + EXPECT_EQ(out_dims[0], 2); + EXPECT_EQ(out_dims[1], 1); + quiver_binary_file_free_int64_array(out_dims); + + // Feb has 28 days in 2025: [2, 28] -> [3, 1] + int64_t current2[] = {2, 28}; + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current2, 2, &out_dims, &out_count), QUIVER_OK); + ASSERT_EQ(out_count, 2u); + EXPECT_EQ(out_dims[0], 3); + EXPECT_EQ(out_dims[1], 1); + quiver_binary_file_free_int64_array(out_dims); + + quiver_binary_file_close(binary_file); + quiver_binary_metadata_free(md); +} + +TEST_F(BinaryCApiFixture, NextDimensionsNullArgs) { + int64_t current[] = {1, 1}; + int64_t* out_dims = nullptr; + size_t out_count = 0; + + EXPECT_EQ(quiver_binary_file_next_dimensions(nullptr, current, 2, &out_dims, &out_count), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: binary_file"); + + auto* md = make_simple_metadata(); + quiver_binary_file_t* binary_file = nullptr; + ASSERT_EQ(quiver_binary_file_open_write(path.c_str(), md, &binary_file), QUIVER_OK); + + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, nullptr, 2, &out_dims, &out_count), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: current_dimensions"); + + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, nullptr, &out_count), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: out_dimensions"); + + EXPECT_EQ(quiver_binary_file_next_dimensions(binary_file, current, 2, &out_dims, nullptr), QUIVER_ERROR); + EXPECT_STREQ(quiver_get_last_error(), "Null argument: out_count"); + + quiver_binary_file_close(binary_file); + quiver_binary_metadata_free(md); +} + +TEST_F(BinaryCApiFixture, FreeInt64ArrayNull) { + EXPECT_EQ(quiver_binary_file_free_int64_array(nullptr), QUIVER_OK); +} + // ============================================================================ // Dimension mismatch errors // ============================================================================ From 8f8cdf38a8fdaa6e15597a48aba7d8dfec1c12f3 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 25 Mar 2026 18:16:40 -0300 Subject: [PATCH 09/10] Add complex next_dimensions test in C++ --- tests/test_binary_file.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_binary_file.cpp b/tests/test_binary_file.cpp index c5ded494..e434ca42 100644 --- a/tests/test_binary_file.cpp +++ b/tests/test_binary_file.cpp @@ -415,6 +415,39 @@ TEST_F(BinaryTempFileFixture, InvalidTimeDimensionCoordinates) { EXPECT_THROW(binary_file.write({1.0, 2.0}, {{"stage", 2}, {"block", 30}}), std::invalid_argument); } +// ============================================================================ +// BinaryNextDimensions +// ============================================================================ + +TEST_F(BinaryTempFileFixture, NextDimensionsTimeDimResetToInitialValueWhenParentAtInitial) { + // Dims: [t1=monthly(2), d=plain(3), t2=daily(31)], initial_datetime=2025-01-05 + // t1.initial_value=1, t2.initial_value=5 (day 5 of the month) + // When d rolls and t1 stays at its initial value (1), t2 must reset to 5 (not 1). + // When d rolls and t1 is NOT at its initial value (2), t2 resets to 1 normally. + auto md = BinaryMetadata::from_element(Element() + .set("version", "1") + .set("initial_datetime", "2025-01-05T00:00:00") + .set("unit", "MW") + .set("dimensions", {"month", "scenario", "day"}) + .set("dimension_sizes", {2, 3, 31}) + .set("time_dimensions", {"month", "day"}) + .set("frequencies", {"monthly", "daily"}) + .set("labels", {"val"})); + auto binary_file = BinaryFile::open_file(path, 'w', md); + + // [1, 2, 31]: d increments (2->3), t1 stays at 1 (its initial value). + // t2 was reset to 1 by carry logic, but 1 < initial_value(5) and t1==initial, so t2 is corrected to 5. + EXPECT_EQ(binary_file.next_dimensions({1, 2, 31}), (std::vector{1, 3, 5})); + + // [1, 3, 31]: d is at max (3), so d resets to 1 and t1 increments to 2. + // t1 is now 2, which is != its initial value (1), so t2 resets to 1 normally. + EXPECT_EQ(binary_file.next_dimensions({1, 3, 31}), (std::vector{2, 1, 1})); + + // [2, 2, 31]: d increments (2->3), t1 stays at 2 (not its initial value). + // t2 resets to 1 normally. + EXPECT_EQ(binary_file.next_dimensions({2, 2, 31}), (std::vector{2, 3, 1})); +} + TEST_F(BinaryTempFileFixture, SingleTimeDimensionSkipsConsistencyCheck) { // With only one time dimension, there's no inner time dim to validate auto md = BinaryMetadata::from_element(Element() From 907ca3f9a414c97d94649fb3da87577990887b77 Mon Sep 17 00:00:00 2001 From: gvidigal-psr Date: Wed, 1 Apr 2026 17:06:10 -0300 Subject: [PATCH 10/10] fix --- tests/test_binary_file.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_binary_file.cpp b/tests/test_binary_file.cpp index e98259ab..73cd198e 100644 --- a/tests/test_binary_file.cpp +++ b/tests/test_binary_file.cpp @@ -468,6 +468,7 @@ TEST_F(BinaryTempFileFixture, NextDimensionsTimeDimResetToInitialValueWhenParent // [2, 2, 31]: d increments (2->3), t1 stays at 2 (not its initial value). // t2 resets to 1 normally. EXPECT_EQ(binary_file.next_dimensions({2, 2, 31}), (std::vector{2, 3, 1})); +} // ============================================================================ // WriteRegistry