diff --git a/.github/workflows/linux.yaml b/.github/workflows/linux.yaml index 1b11ed0ec..60a2f3ec9 100644 --- a/.github/workflows/linux.yaml +++ b/.github/workflows/linux.yaml @@ -126,6 +126,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 40ab577d1..220709906 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -68,6 +68,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 14ca7b2fc..40fe81d71 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -67,6 +67,7 @@ jobs: ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_read >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=person_write >> $GITHUB_STEP_SUMMARY ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=points_read >> $GITHUB_STEP_SUMMARY + ./build/benchmarks/all/reflect-cpp-all-format-benchmarks --benchmark_filter=bulk_numeric_vectors >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo '## Benchmarks for JSON' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/benchmarks/all/bulk_numeric_vectors.cpp b/benchmarks/all/bulk_numeric_vectors.cpp new file mode 100644 index 000000000..3819ff0f3 --- /dev/null +++ b/benchmarks/all/bulk_numeric_vectors.cpp @@ -0,0 +1,244 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bulk_numeric_vectors { + +struct Point2 { + double x; + double y; + + bool operator==(const Point2&) const = default; +}; + +struct Camera { + std::vector keypoints; + std::vector landmark_ids; + + bool operator==(const Camera&) const = default; +}; + +struct World { + std::vector cameras; + + bool operator==(const World&) const = default; +}; + +static Point2 make_point(std::size_t index) { + return { + static_cast(static_cast(index % 65536) - 32768) * + 0.125, + static_cast(static_cast((index * 17) % 65536) - + 32768) * + 0.25}; +} + +static std::uint64_t make_id(std::size_t index) { + return static_cast(index) * UINT64_C(0x9e3779b97f4a7c15) + + UINT64_C(0x123456789abcdef0); +} + +static std::vector make_ids(std::size_t count) { + std::vector values(count); + for (std::size_t i = 0; i < count; ++i) values[i] = make_id(i); + return values; +} + +static std::vector make_points(std::size_t count) { + std::vector values(count); + for (std::size_t i = 0; i < count; ++i) values[i] = make_point(i); + return values; +} + +static World make_world(std::size_t cameras, std::size_t points_per_camera) { + World world; + world.cameras.resize(cameras); + for (std::size_t i = 0; i < cameras; ++i) { + auto& camera = world.cameras[i]; + camera.keypoints.resize(points_per_camera); + camera.landmark_ids.resize(points_per_camera); + for (std::size_t j = 0; j < points_per_camera; ++j) { + const auto index = i * points_per_camera + j; + camera.keypoints[j] = make_point(index); + camera.landmark_ids[j] = make_id(index); + } + } + return world; +} + +enum class Format { cereal, yas, boost_serialization }; + +template +void write(const T& input, std::ostream& stream) { + if constexpr (F == Format::cereal) + rfl::cereal::write(input, stream); + else if constexpr (F == Format::yas) + rfl::yas::write(input, stream); + else + rfl::boost_serialization::write(input, stream); +} + +template +auto read(const std::string& bytes) { + if constexpr (F == Format::yas) { + return rfl::yas::read(bytes); + } else { + std::istringstream stream(bytes); + if constexpr (F == Format::cereal) + return rfl::cereal::read(stream); + else + return rfl::boost_serialization::read(stream); + } +} + +template +static void run(benchmark::State& state, const T& input, std::int64_t items) { + std::ostringstream output; + write(input, output); + const auto bytes = output.str(); + { + const auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + return; + } + if (*result != input) { + state.SkipWithError("Binary round trip changed the input"); + return; + } + } + + // Stream construction, serialization, and destruction are timed. Fixture + // generation, output extraction, and equality checks are outside the loop. + for (auto _ : state) { + if constexpr (Read) { + auto result = read(bytes); + if (!result) { + state.SkipWithError(result.error().what()); + break; + } + benchmark::DoNotOptimize(result); + } else { + std::ostringstream stream; + write(input, stream); + benchmark::DoNotOptimize(stream); + } + } + state.SetItemsProcessed(state.iterations() * items); + state.SetBytesProcessed(state.iterations() * + static_cast(bytes.size())); + state.counters["serialized_bytes"] = static_cast(bytes.size()); +} + +template +static void BM_bulk_numeric_vectors_ids(benchmark::State& state) { + const auto input = make_ids(static_cast(state.range(0))); + run(state, input, state.range(0)); +} + +template +static void BM_bulk_numeric_vectors_points(benchmark::State& state) { + const auto input = make_points(static_cast(state.range(0))); + run(state, input, state.range(0)); +} + +template +static void BM_bulk_numeric_vectors_complete(benchmark::State& state) { + const auto input = make_world(static_cast(state.range(0)), + static_cast(state.range(1))); + run(state, input, state.range(0) * state.range(1)); +} + +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, Format::cereal) + ->Name("bulk_numeric_vectors/cereal/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, Format::yas) + ->Name("bulk_numeric_vectors/yas/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, Format::yas) + ->Name("bulk_numeric_vectors/yas/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/ids/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_ids, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/ids/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/points/write") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_points, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/points/read") + ->Arg(1500000) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, false, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/complete/write") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); +BENCHMARK_TEMPLATE(BM_bulk_numeric_vectors_complete, true, + Format::boost_serialization) + ->Name("bulk_numeric_vectors/boost_serialization/complete/read") + ->Args({500, 3000}) + ->ArgNames({"cameras", "keypoints"}) + ->Unit(benchmark::kMillisecond); + +} // namespace bulk_numeric_vectors diff --git a/include/rfl/boost_serialization/Parser.hpp b/include/rfl/boost_serialization/Parser.hpp index 7cf7e6af7..ddc9ec079 100644 --- a/include/rfl/boost_serialization/Parser.hpp +++ b/include/rfl/boost_serialization/Parser.hpp @@ -1,6 +1,15 @@ #ifndef RFL_BOOST_SERIALIZATION_PARSER_HPP_ #define RFL_BOOST_SERIALIZATION_PARSER_HPP_ +#include +#include +#include +#include +#include +#include +#include +#include + #include "../Generic.hpp" #include "../NamedTuple.hpp" #include "../Tuple.hpp" @@ -13,6 +22,49 @@ namespace rfl { namespace parsing { +template + requires(std::is_arithmetic_v && !std::is_same_v && + !std::is_same_v) +struct Parser, + boost_serialization::Writer<::boost::archive::binary_oarchive>, + std::vector, ProcessorsType> + : VectorParser< + boost_serialization::Reader<::boost::archive::binary_iarchive>, + boost_serialization::Writer<::boost::archive::binary_oarchive>, + std::vector, ProcessorsType> { + using R = boost_serialization::Reader<::boost::archive::binary_iarchive>; + using W = boost_serialization::Writer<::boost::archive::binary_oarchive>; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::uint64_t size; + *_var.ar >> size; + Type values; + if (size > values.max_size()) { + return error("Array length exceeds vector max_size"); + } + values.resize(static_cast(size)); + if (!values.empty()) { + _var.ar->load_binary(values.data(), values.size() * sizeof(T)); + } + return values; + } catch (const std::exception& e) { + return error(std::string("Boost binary array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + _w.write_array_data(_values.data(), _values.size()); + _w.end_array(&arr); + } +}; + /// Boost.Serialization requires us to explicitly set all fields. Because /// of that, we require all of the fields and then set them to nullptr, if /// necessary. diff --git a/include/rfl/boost_serialization/Writer.hpp b/include/rfl/boost_serialization/Writer.hpp index ccf1e74f9..8c18842d5 100644 --- a/include/rfl/boost_serialization/Writer.hpp +++ b/include/rfl/boost_serialization/Writer.hpp @@ -1,6 +1,7 @@ #ifndef RFL_BOOST_SERIALIZATION_WRITER_HPP_ #define RFL_BOOST_SERIALIZATION_WRITER_HPP_ +#include #include #include #include @@ -254,6 +255,13 @@ class Writer { /// @param _obj Pointer to the object (unused) void end_object(OutputObjectType* /*_obj*/) const {} + template + void write_array_data(const T* _data, const std::size_t _size) const { + if (_size != 0) { + ar_->save_binary(_data, _size * sizeof(T)); + } + } + private: void write_size(const size_t _size) const { *ar_ << static_cast(_size); diff --git a/include/rfl/cereal/Parser.hpp b/include/rfl/cereal/Parser.hpp index 626b0138e..616f2f6f6 100644 --- a/include/rfl/cereal/Parser.hpp +++ b/include/rfl/cereal/Parser.hpp @@ -1,6 +1,12 @@ #ifndef RFL_CEREAL_PARSER_HPP_ #define RFL_CEREAL_PARSER_HPP_ +#include +#include +#include +#include +#include + #include "../internal/no_field_names_v.hpp" #include "../parsing/Parser.hpp" #include "Reader.hpp" @@ -9,6 +15,42 @@ namespace rfl { namespace parsing { +template + requires(std::is_arithmetic_v && !std::is_same_v && + !std::is_same_v) +struct Parser, ProcessorsType> + : VectorParser, + ProcessorsType> { + using R = cereal::Reader; + using W = cereal::Writer; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::size_t size; + (*_var.archive_)(::cereal::make_size_tag(size)); + Type values(size); + if (!values.empty()) { + (*_var.archive_)( + ::cereal::binary_data(values.data(), values.size() * sizeof(T))); + } + return values; + } catch (const std::exception& e) { + return error(std::string("Cereal array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + _w.write_array_data(_values.data(), _values.size()); + _w.end_array(&arr); + } +}; + /// Cereal requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/include/rfl/cereal/Writer.hpp b/include/rfl/cereal/Writer.hpp index ec20478d5..15a2993fe 100644 --- a/include/rfl/cereal/Writer.hpp +++ b/include/rfl/cereal/Writer.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -177,6 +178,15 @@ class Writer { void end_object(OutputObjectType* _obj) const noexcept; + template + void write_array_data(const T* _data, const std::size_t _size) const { + if (_size != 0) { + // A pointer value preserves the element width in Cereal BinaryData. + (*archive_)(::cereal::binary_data(static_cast(_data), + _size * sizeof(T))); + } + } + private: void add_string_view(const std::string_view& _str) const; diff --git a/include/rfl/yas/Parser.hpp b/include/rfl/yas/Parser.hpp index ce5e4c833..d7ede37c5 100644 --- a/include/rfl/yas/Parser.hpp +++ b/include/rfl/yas/Parser.hpp @@ -1,6 +1,12 @@ #ifndef RFL_YAS_PARSER_HPP_ #define RFL_YAS_PARSER_HPP_ +#include +#include +#include +#include +#include + #include "../Generic.hpp" #include "../NamedTuple.hpp" #include "../Tuple.hpp" @@ -13,6 +19,43 @@ namespace rfl { namespace parsing { +template + requires((std::is_integral_v || std::is_same_v || + std::is_same_v) && + !std::is_same_v && !std::is_same_v) +struct Parser, ProcessorsType> + : VectorParser, ProcessorsType> { + using R = yas::Reader; + using W = yas::Writer; + using Type = std::vector; + + static Result read(const R&, + const typename R::InputVarType& _var) noexcept { + try { + std::size_t size; + (*_var.ar) & size; + Type values(size); + if (!values.empty()) { + _var.ar->read(values.data(), values.size() * sizeof(T)); + } + return values; + } catch (const std::exception& e) { + return error(std::string("yas array read error: ") + e.what()); + } + } + + template + static void write(const W& _w, const Type& _values, + const ParentType& _parent) { + // Keep the adapter's array prefix and parent metadata unchanged. + auto arr = Parent::add_array(_w, _values.size(), _parent); + if (!_values.empty()) { + arr.ar->write(_values.data(), _values.size() * sizeof(T)); + } + _w.end_array(&arr); + } +}; + /// Yas requires us to explicitly set the number of fields in advance. /// Because of that, we require all of the fields and then set them to nullptr, /// if necessary. diff --git a/tests/boost_serialization/test_numeric_vector_io.cpp b/tests/boost_serialization/test_numeric_vector_io.cpp new file mode 100644 index 000000000..53023b9a1 --- /dev/null +++ b/tests/boost_serialization/test_numeric_vector_io.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../check_numeric_vector_io.hpp" + +namespace test_numeric_vector_io { +namespace { +using R = rfl::boost_serialization::Reader; +using W = rfl::boost_serialization::Writer; +using Ps = rfl::Processors<>; + +template +auto legacy_write(const std::vector& input) { + std::ostringstream stream; + boost::archive::binary_oarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); +} + +template +auto legacy_read(const std::vector& bytes) { + std::istringstream stream(std::string(bytes.begin(), bytes.end())); + boost::archive::binary_iarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + return rfl::parsing::VectorParser, Ps>::read( + R{}, typename R::InputVarType{&archive}); +} +} // namespace + +TEST(boost_serialization, numeric_vector_wire_compatibility) { + check_types( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }, + [](const auto& input) { return legacy_write(input); }, + [](const auto& bytes) { return legacy_read(bytes); }); +} + +TEST(boost_serialization, numeric_vector_parent_metadata) { + check_nested( + [](const auto& input) { return rfl::boost_serialization::write(input); }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); + check_nested( + [](const auto& input) { + return rfl::boost_serialization::write(input); + }, + [](const auto& bytes) { + return rfl::boost_serialization::read(bytes); + }); +} + +TEST(boost_serialization, numeric_vector_excessive_length) { + const auto make_bytes = [] { + std::ostringstream stream; + boost::archive::binary_oarchive archive( + stream, boost::archive::no_header | boost::archive::no_tracking); + const auto size = std::numeric_limits::max(); + archive << size; + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + EXPECT_FALSE( + rfl::boost_serialization::read>(make_bytes())); +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { + +TEST(boost_serialization, numeric_vector_text_archive_fallback) { + using I = boost::archive::text_iarchive; + using O = boost::archive::text_oarchive; + using TextReader = rfl::boost_serialization::Reader; + using TextWriter = rfl::boost_serialization::Writer; + const std::vector input{1.25, -2.5, 0.0}; + std::ostringstream output; + { + O archive(output); + rfl::boost_serialization::write(archive, input); + } + std::ostringstream legacy; + { + O archive(legacy); + const TextWriter writer(&archive); + rfl::parsing:: + VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + } + EXPECT_EQ(output.str(), legacy.str()); + std::istringstream stream(output.str()); + I archive(stream); + const auto decoded = + rfl::boost_serialization::read_from_archive, I, O>( + archive); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); +} + +} // namespace test_numeric_vector_io diff --git a/tests/cereal/test_numeric_vector_io.cpp b/tests/cereal/test_numeric_vector_io.cpp new file mode 100644 index 000000000..25a67f36f --- /dev/null +++ b/tests/cereal/test_numeric_vector_io.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +#include + +#include "../check_numeric_vector_io.hpp" + +namespace test_numeric_vector_io { +namespace { +using R = rfl::cereal::Reader; +using W = rfl::cereal::Writer; +using Ps = rfl::Processors<>; + +template +auto legacy_write(const std::vector& input) { + std::ostringstream stream; + ::cereal::PortableBinaryOutputArchive archive(stream); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); +} + +template +auto legacy_read(const std::vector& bytes) { + std::istringstream stream(std::string(bytes.begin(), bytes.end())); + ::cereal::PortableBinaryInputArchive archive(stream); + return rfl::parsing::VectorParser, Ps>::read( + R{}, typename R::InputVarType{&archive}); +} +} // namespace + +TEST(cereal, numeric_vector_wire_compatibility) { + check_types( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + [](const auto& input) { return legacy_write(input); }, + [](const auto& bytes) { return legacy_read(bytes); }); +} + +TEST(cereal, numeric_vector_parent_metadata) { + check_nested( + [](const auto& input) { return rfl::cereal::write(input); }, + [](const auto& bytes) { return rfl::cereal::read(bytes); }); + check_nested( + [](const auto& input) { + return rfl::cereal::write(input); + }, + [](const auto& bytes) { + return rfl::cereal::read(bytes); + }); +} + +TEST(cereal, numeric_vector_excessive_length) { + const auto make_bytes = [] { + std::ostringstream stream; + ::cereal::PortableBinaryOutputArchive archive(stream); + const auto size = std::numeric_limits::max(); + archive(::cereal::make_size_tag(size)); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + EXPECT_FALSE(rfl::cereal::read>(make_bytes())); +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { + +TEST(cereal, numeric_vector_endianness) { + using Archive = ::cereal::PortableBinaryOutputArchive; + for (const auto options : + {Archive::Options::LittleEndian(), Archive::Options::BigEndian()}) { + const auto write = [options](const auto& input) { + std::ostringstream stream; + Archive archive(stream, options); + rfl::cereal::write(input, archive); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + const auto old_write = [options](const std::vector& input) { + std::ostringstream stream; + Archive archive(stream, options); + W writer(&archive); + rfl::parsing::VectorParser, Ps>::write( + writer, input, typename rfl::parsing::Parent::Root{}); + const auto str = stream.str(); + return std::vector(str.begin(), str.end()); + }; + check_types( + write, + [](const auto& bytes) { return rfl::cereal::read(bytes); }, + old_write, + [](const auto& bytes) { return legacy_read(bytes); }); + } +} + +} // namespace test_numeric_vector_io + +namespace test_numeric_vector_io { +TEST(cereal, numeric_vector_nested_arrays) { + const std::vector> input{{}, {1.25, -2.5}, {3.0}}; + const std::vector> legacy{{}, {1.25, -2.5}, {3.0}}; + const auto bytes = rfl::cereal::write(input); + EXPECT_EQ(bytes, rfl::cereal::write(legacy)); + const auto decoded = rfl::cereal::read>>( + rfl::cereal::write(legacy)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + const auto old_decoded = + rfl::cereal::read>>(bytes); + ASSERT_TRUE(old_decoded) << old_decoded.error().what(); + EXPECT_EQ(*old_decoded, legacy); +} +} // namespace test_numeric_vector_io diff --git a/tests/check_numeric_vector_io.hpp b/tests/check_numeric_vector_io.hpp new file mode 100644 index 000000000..abe3e55be --- /dev/null +++ b/tests/check_numeric_vector_io.hpp @@ -0,0 +1,134 @@ +#ifndef CHECK_NUMERIC_VECTOR_IO_HPP_ +#define CHECK_NUMERIC_VECTOR_IO_HPP_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace test_numeric_vector_io { + +template +void check_type(Write write, Read read, LegacyWrite legacy_write, + LegacyRead legacy_read) { + const std::vector edge_values = {std::numeric_limits::lowest(), T(0), + T(1), std::numeric_limits::max()}; + for (const auto count : {0, 1, 65}) { + SCOPED_TRACE(count); + std::vector input; + for (int i = 0; i < count; ++i) { + input.push_back(edge_values[i % edge_values.size()]); + } + const auto old_bytes = legacy_write(input); + const auto bytes = write(input); + EXPECT_EQ(bytes, old_bytes); + const auto decoded = read.template operator()>(old_bytes); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + const auto old_decoded = legacy_read.template operator()(bytes); + ASSERT_TRUE(old_decoded) << old_decoded.error().what(); + EXPECT_EQ(*old_decoded, input); + if (count != 0) { + auto truncated = bytes; + truncated.pop_back(); + EXPECT_FALSE(read.template operator()>(truncated)); + } + } + if constexpr (std::is_floating_point_v) { + const std::vector input = {-T(0), T(1.25), -T(2.5), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + EXPECT_EQ(write(input), legacy_write(input)); + const auto decoded = + read.template operator()>(legacy_write(input)); + ASSERT_TRUE(decoded) << decoded.error().what(); + EXPECT_EQ(*decoded, input); + EXPECT_EQ(write(*decoded), legacy_write(input)); + const std::vector nan_input{std::numeric_limits::quiet_NaN()}; + const auto nan_bytes = legacy_write(nan_input); + EXPECT_EQ(write(nan_input), nan_bytes); + const auto nan_decoded = + read.template operator()>(nan_bytes); + ASSERT_TRUE(nan_decoded) << nan_decoded.error().what(); + ASSERT_EQ(nan_decoded->size(), 1u); + EXPECT_TRUE(std::isnan(nan_decoded->front())); + EXPECT_EQ(write(*nan_decoded), nan_bytes); + } +} + +template +void check_types(Write write, Read read, LegacyWrite legacy_write, + LegacyRead legacy_read) { + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); + check_type(write, read, legacy_write, legacy_read); +} + +template +using Vector = std::vector; +template +using Deque = std::deque; + +template