diff --git a/LibCarla/cmake/test/CMakeLists.txt b/LibCarla/cmake/test/CMakeLists.txt index 432f509e726..a636b38f46a 100644 --- a/LibCarla/cmake/test/CMakeLists.txt +++ b/LibCarla/cmake/test/CMakeLists.txt @@ -54,6 +54,17 @@ foreach(target ${build_targets}) target_include_directories(${target} PRIVATE "${libcarla_source_path}/test") + # Server tests exercise CDR serialization (CdrSerialization.h) and + # GenericCdrPubSubType (inherits TopicDataType from fastrtps). + # Enable exceptions because Fast-CDR templates use try/catch internally. + if (CMAKE_BUILD_TYPE STREQUAL "Server") + target_include_directories(${target} SYSTEM PRIVATE "${FASTDDS_INCLUDE_PATH}") + target_compile_options(${target} PRIVATE -fexceptions) + target_link_libraries(${target} "${FASTDDS_LIB_PATH}/libfastrtps.a") + target_link_libraries(${target} "${FASTDDS_LIB_PATH}/libfastcdr.a") + target_link_libraries(${target} "${FASTDDS_LIB_PATH}/libfoonathan_memory-0.7.3.a") + endif() + if (WIN32) target_link_libraries(${target} "gtest_main.lib") target_link_libraries(${target} "gtest.lib") diff --git a/LibCarla/source/carla/ros2/dds/fastdds/FastDDSPublisherMiddleware.h b/LibCarla/source/carla/ros2/dds/fastdds/FastDDSPublisherMiddleware.h index 07ffec6bbaf..87e37f081e1 100644 --- a/LibCarla/source/carla/ros2/dds/fastdds/FastDDSPublisherMiddleware.h +++ b/LibCarla/source/carla/ros2/dds/fastdds/FastDDSPublisherMiddleware.h @@ -5,7 +5,7 @@ #pragma once #include "carla/ros2/dds/IDDSPublisherMiddleware.h" -#include "carla/ros2/dds/fastdds/FastDDSTypeMap.h" +#include "carla/ros2/dds/fastdds/GenericCdrPubSubType.h" #include "carla/Logging.h" #include @@ -30,17 +30,14 @@ using erc = eprosima::fastrtps::types::ReturnCode_t; /// FastDDS implementation of IDDSPublisherMiddleware. /// Parameterized on a traits type T that provides: -/// T::msg_type — the message type -/// Native FastDDS types are resolved via FastDDSTypeMap. +/// T::msg_type — the message type (a carla::ros2::msg::* POD struct) +/// Serialization is handled by GenericCdrPubSubType via CdrSerialization.h. template class FastDDSPublisherMiddleware : public IDDSPublisherMiddleware, public eprosima::fastdds::dds::DataWriterListener { public: using msg_type = typename T::msg_type; - using type_map = FastDDSTypeMap; - using fastdds_type = typename type_map::fastdds_type; - using fastdds_pubsub_type = typename type_map::fastdds_pubsub_type; void on_publication_matched( efd::DataWriter* writer, @@ -108,10 +105,8 @@ class FastDDSPublisherMiddleware } bool Publish(void* message_data) override { - auto* msg = static_cast(message_data); - to_fastdds(*msg, _fastdds_msg); eprosima::fastrtps::rtps::InstanceHandle_t instance_handle; - erc rcode = _datawriter->write(&_fastdds_msg, instance_handle); + erc rcode = _datawriter->write(message_data, instance_handle); if (rcode == erc::ReturnCodeValue::RETCODE_OK) { return true; } @@ -133,11 +128,10 @@ class FastDDSPublisherMiddleware efd::Publisher* _publisher { nullptr }; efd::Topic* _topic { nullptr }; efd::DataWriter* _datawriter { nullptr }; - efd::TypeSupport _type { new fastdds_pubsub_type() }; + efd::TypeSupport _type { new GenericCdrPubSubType() }; - fastdds_type _fastdds_msg; - std::string _topic_name; - bool _alive { false }; + std::string _topic_name; + bool _alive { false }; }; } // namespace ros2 diff --git a/LibCarla/source/carla/ros2/dds/fastdds/FastDDSSubscriberMiddleware.h b/LibCarla/source/carla/ros2/dds/fastdds/FastDDSSubscriberMiddleware.h index 03f50f757ba..c679f802580 100644 --- a/LibCarla/source/carla/ros2/dds/fastdds/FastDDSSubscriberMiddleware.h +++ b/LibCarla/source/carla/ros2/dds/fastdds/FastDDSSubscriberMiddleware.h @@ -5,7 +5,7 @@ #pragma once #include "carla/ros2/dds/IDDSSubscriberMiddleware.h" -#include "carla/ros2/dds/fastdds/FastDDSTypeMap.h" +#include "carla/ros2/dds/fastdds/GenericCdrPubSubType.h" #include "carla/Logging.h" #include @@ -31,17 +31,14 @@ using erc = eprosima::fastrtps::types::ReturnCode_t; /// FastDDS implementation of IDDSSubscriberMiddleware. /// Parameterized on traits type S that provides: -/// S::msg_type — the message type -/// Native FastDDS types are resolved via FastDDSTypeMap. +/// S::msg_type — the message type (a carla::ros2::msg::* POD struct) +/// Deserialization is handled by GenericCdrPubSubType via CdrSerialization.h. template class FastDDSSubscriberMiddleware : public IDDSSubscriberMiddleware, public eprosima::fastdds::dds::DataReaderListener { public: using msg_type = typename S::msg_type; - using type_map = FastDDSTypeMap; - using fastdds_type = typename type_map::fastdds_type; - using fastdds_pubsub_type = typename type_map::fastdds_pubsub_type; void on_subscription_matched( efd::DataReader* reader, @@ -51,9 +48,8 @@ class FastDDSSubscriberMiddleware void on_data_available(efd::DataReader* reader) override { efd::SampleInfo info; - erc rcode = reader->take_next_sample(&_fastdds_msg, &info); + erc rcode = reader->take_next_sample(_message_ptr, &info); if (rcode == erc::ReturnCodeValue::RETCODE_OK) { - from_fastdds(_fastdds_msg, *_message_ptr); *_new_message_ptr = true; } else { log_error("FastDDSSubscriberMiddleware::on_data_available (", @@ -137,11 +133,10 @@ class FastDDSSubscriberMiddleware efd::Subscriber* _subscriber { nullptr }; efd::Topic* _topic { nullptr }; efd::DataReader* _datareader { nullptr }; - efd::TypeSupport _type { new fastdds_pubsub_type() }; + efd::TypeSupport _type { new GenericCdrPubSubType() }; - fastdds_type _fastdds_msg; - msg_type* _message_ptr { nullptr }; - bool* _new_message_ptr { nullptr }; + msg_type* _message_ptr { nullptr }; + bool* _new_message_ptr { nullptr }; std::string _topic_name; bool _alive { false }; diff --git a/LibCarla/source/carla/ros2/dds/fastdds/GenericCdrPubSubType.h b/LibCarla/source/carla/ros2/dds/fastdds/GenericCdrPubSubType.h new file mode 100644 index 00000000000..13bb6973f7c --- /dev/null +++ b/LibCarla/source/carla/ros2/dds/fastdds/GenericCdrPubSubType.h @@ -0,0 +1,151 @@ +// Copyright (c) 2025 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB). +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +#pragma once + +#include "carla/ros2/types/CdrSerialization.h" +#include "carla/ros2/types/CdrTopicInfo.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace carla { +namespace ros2 { + +/// Generic FastDDS TopicDataType that serializes carla::ros2::msg::* POD structs +/// directly to CDR via CdrSerialization.h, without needing fastddsgen-generated +/// per-type PubSubType classes. +/// +/// Replaces all 30 hand-generated *PubSubType classes and FastDDSTypeMap<>. +/// Type name and max size are provided by CdrTopicInfo. +template +class GenericCdrPubSubType : public eprosima::fastdds::dds::TopicDataType { + public: + using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t; + + GenericCdrPubSubType() { + setName(CdrTopicInfo::type_name()); + // m_typeSize is max CDR payload including the 4-byte DDS encapsulation header. + // FastDDS uses this to pre-allocate payload buffers. + const uint32_t max_payload = static_cast( + CdrTopicInfo::max_serialized_size()); + // Add alignment padding + 4-byte encapsulation header, matching the pattern + // in fastddsgen-generated constructors (e.g. ClockPubSubTypes.cpp:36-37). + m_typeSize = max_payload + + static_cast( + eprosima::fastcdr::Cdr::alignment(max_payload, 4u)) + + 4u; + m_isGetKeyDefined = false; + } + + ~GenericCdrPubSubType() override = default; + + /// Serialize a MsgType instance into the FastDDS payload buffer. + /// Called by FastDDS DataWriter::write() before sending on the wire. + /// Serializes into an auto-growing heap buffer first so variable-length + /// fields (e.g. Image::data, PointCloud2::data) are not bounded by the + /// pre-allocated payload->data size. The bytes are then memcpy'd across. + /// FastDDS resizes payload->data before this call via getSerializedSizeProvider, + /// so the copy will always fit for correctly sized messages. + bool serialize( + void* data, + SerializedPayload_t* payload) override { + const MsgType* msg = static_cast(data); + + // Auto-growing FastBuffer: no fixed-size ceiling, handles any payload. + eprosima::fastcdr::FastBuffer fb; + // Force LITTLE_ENDIANNESS so the encapsulation header is CDR_LE + // ({0x00, 0x01}) per DDSI-RTPS v2.5 Table 10.3, regardless of host + // endianness. ROS2 ecosystems test against CDR_LE. + eprosima::fastcdr::Cdr ser( + fb, + eprosima::fastcdr::Cdr::LITTLE_ENDIANNESS, + eprosima::fastcdr::Cdr::DDS_CDR); + payload->encapsulation = CDR_LE; + + try { + ser.serialize_encapsulation(); + serialize_cdr(ser, *msg); + } catch (eprosima::fastcdr::exception::Exception& /*e*/) { + return false; + } + + const uint32_t len = static_cast(ser.getSerializedDataLength()); + if (len > payload->max_size) { + return false; + } + std::memcpy(payload->data, fb.getBuffer(), len); + payload->length = len; + return true; + } + + /// Deserialize a FastDDS payload buffer into a MsgType instance. + /// Called by FastDDS DataReader after receiving data from the wire. + bool deserialize( + SerializedPayload_t* payload, + void* data) override { + MsgType* msg = static_cast(data); + + eprosima::fastcdr::FastBuffer fastbuffer( + reinterpret_cast(payload->data), + static_cast(payload->length)); + // The deserializer must accept either endianness on the wire, the + // actual byte order is determined from the encapsulation header by + // read_encapsulation(). LITTLE_ENDIANNESS here is just the initial + // hint Fast-CDR uses before the header is parsed. + eprosima::fastcdr::Cdr deser( + fastbuffer, + eprosima::fastcdr::Cdr::LITTLE_ENDIANNESS, + eprosima::fastcdr::Cdr::DDS_CDR); + + try { + deser.read_encapsulation(); + payload->encapsulation = (deser.endianness() == + eprosima::fastcdr::Cdr::BIG_ENDIANNESS) ? CDR_BE : CDR_LE; + deserialize_cdr(deser, *msg); + } catch (eprosima::fastcdr::exception::Exception& /*e*/) { + return false; + } + + return true; + } + + /// Return a function that gives the actual CDR-serialized size for this + /// specific message instance. FastDDS calls this before serialize() to + /// size (or resize) the payload buffer, so the buffer is always large enough + /// for variable-length fields like Image::data or PointCloud2::data. + std::function getSerializedSizeProvider(void* data) override { + const MsgType* msg = static_cast(data); + return [msg]() -> uint32_t { + return cdr_serialized_size(*msg); + }; + } + + /// Allocate a new default-initialized MsgType on the heap. + void* createData() override { + return static_cast(new MsgType()); + } + + /// Delete a MsgType previously returned by createData(). + void deleteData(void* data) override { + delete static_cast(data); + } + + /// CARLA topics are not keyed — always return false. + bool getKey( + void* /*data*/, + eprosima::fastrtps::rtps::InstanceHandle_t* /*ihandle*/, + bool /*force_md5*/) override { + return false; + } +}; + +} // namespace ros2 +} // namespace carla diff --git a/LibCarla/source/carla/ros2/types/CdrSerialization.h b/LibCarla/source/carla/ros2/types/CdrSerialization.h new file mode 100644 index 00000000000..dcc90801651 --- /dev/null +++ b/LibCarla/source/carla/ros2/types/CdrSerialization.h @@ -0,0 +1,715 @@ +// Copyright (c) 2025 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB). +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "carla/ros2/types/msg/AckermannDrive.h" +#include "carla/ros2/types/msg/AckermannDriveStamped.h" +#include "carla/ros2/types/msg/CameraInfo.h" +#include "carla/ros2/types/msg/CarlaCollisionEvent.h" +#include "carla/ros2/types/msg/CarlaEgoVehicleControl.h" +#include "carla/ros2/types/msg/CarlaLineInvasion.h" +#include "carla/ros2/types/msg/Clock.h" +#include "carla/ros2/types/msg/Float32.h" +#include "carla/ros2/types/msg/Header.h" +#include "carla/ros2/types/msg/Image.h" +#include "carla/ros2/types/msg/Imu.h" +#include "carla/ros2/types/msg/NavSatFix.h" +#include "carla/ros2/types/msg/NavSatStatus.h" +#include "carla/ros2/types/msg/Odometry.h" +#include "carla/ros2/types/msg/Point.h" +#include "carla/ros2/types/msg/Point32.h" +#include "carla/ros2/types/msg/PointCloud2.h" +#include "carla/ros2/types/msg/PointField.h" +#include "carla/ros2/types/msg/Pose.h" +#include "carla/ros2/types/msg/PoseWithCovariance.h" +#include "carla/ros2/types/msg/Quaternion.h" +#include "carla/ros2/types/msg/RegionOfInterest.h" +#include "carla/ros2/types/msg/String.h" +#include "carla/ros2/types/msg/TF2Error.h" +#include "carla/ros2/types/msg/TFMessage.h" +#include "carla/ros2/types/msg/Time.h" +#include "carla/ros2/types/msg/Transform.h" +#include "carla/ros2/types/msg/TransformStamped.h" +#include "carla/ros2/types/msg/Twist.h" +#include "carla/ros2/types/msg/TwistWithCovariance.h" +#include "carla/ros2/types/msg/Vector3.h" + +namespace carla { +namespace ros2 { + +// ========================================================================== +// Internal CDR helpers — one overload pair per message type. +// Ordered from least-dependent to most-dependent so each helper's body +// can call the helpers for its nested types without forward declarations. +// +// Wire format: OMG DDSI-RTPS v2.5 Section 10 + DDS-XTypes 1.3 clause 7.4.1.1 +// (Classic CDR, encoding version 1, little-endian). Sequences are encoded +// as a uint32_t length followed by elements; strings as a uint32_t length +// (including the terminating NUL) followed by bytes; bool as a single octet. +// ========================================================================== + +/// Sanity cap for the length field of a CDR sequence read from the wire. +/// Protects against malformed/hostile payloads claiming a multi-GB sequence, +/// which would otherwise OOM-abort the process inside std::vector::resize(). +/// 1,048,576 elements is far above any realistic ROS2 message (PointCloud2 +/// rarely has more than a dozen fields; TFMessage rarely has more than a few +/// hundred transforms) while bounding the worst-case allocation to ~80 MiB. +static constexpr uint32_t kMaxCdrSequenceElements = 1u << 20; + +// -------------------------------------------------------------------------- +// Leaf types (no nested msg:: fields) +// -------------------------------------------------------------------------- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Time& m) { + cdr << m.sec; + cdr << m.nanosec; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Time& m) { + cdr >> m.sec; + cdr >> m.nanosec; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Vector3& m) { + cdr << m.x; + cdr << m.y; + cdr << m.z; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Vector3& m) { + cdr >> m.x; + cdr >> m.y; + cdr >> m.z; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Quaternion& m) { + cdr << m.x; + cdr << m.y; + cdr << m.z; + cdr << m.w; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Quaternion& m) { + cdr >> m.x; + cdr >> m.y; + cdr >> m.z; + cdr >> m.w; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Point& m) { + cdr << m.x; + cdr << m.y; + cdr << m.z; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Point& m) { + cdr >> m.x; + cdr >> m.y; + cdr >> m.z; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Point32& m) { + cdr << m.x; + cdr << m.y; + cdr << m.z; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Point32& m) { + cdr >> m.x; + cdr >> m.y; + cdr >> m.z; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::NavSatStatus& m) { + cdr << m.status; + cdr << m.service; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::NavSatStatus& m) { + cdr >> m.status; + cdr >> m.service; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::RegionOfInterest& m) { + cdr << m.x_offset; + cdr << m.y_offset; + cdr << m.height; + cdr << m.width; + cdr << m.do_rectify; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::RegionOfInterest& m) { + cdr >> m.x_offset; + cdr >> m.y_offset; + cdr >> m.height; + cdr >> m.width; + cdr >> m.do_rectify; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Float32& m) { + cdr << m.data; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Float32& m) { + cdr >> m.data; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::AckermannDrive& m) { + cdr << m.steering_angle; + cdr << m.steering_angle_velocity; + cdr << m.speed; + cdr << m.acceleration; + cdr << m.jerk; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::AckermannDrive& m) { + cdr >> m.steering_angle; + cdr >> m.steering_angle_velocity; + cdr >> m.speed; + cdr >> m.acceleration; + cdr >> m.jerk; +} + +// -- + +/// PointField: name is a string; offset, datatype, count are primitives. +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::PointField& m) { + cdr << m.name; + cdr << m.offset; + cdr << m.datatype; + cdr << m.count; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::PointField& m) { + cdr >> m.name; + cdr >> m.offset; + cdr >> m.datatype; + cdr >> m.count; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::TF2Error& m) { + cdr << m.error; + cdr << m.error_string; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::TF2Error& m) { + cdr >> m.error; + cdr >> m.error_string; +} + +// -------------------------------------------------------------------------- +// Types with nested msg:: fields +// -------------------------------------------------------------------------- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Header& m) { + serialize_cdr(cdr, m.stamp); + cdr << m.frame_id; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Header& m) { + deserialize_cdr(cdr, m.stamp); + cdr >> m.frame_id; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Twist& m) { + serialize_cdr(cdr, m.linear); + serialize_cdr(cdr, m.angular); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Twist& m) { + deserialize_cdr(cdr, m.linear); + deserialize_cdr(cdr, m.angular); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Transform& m) { + serialize_cdr(cdr, m.translation); + serialize_cdr(cdr, m.rotation); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Transform& m) { + deserialize_cdr(cdr, m.translation); + deserialize_cdr(cdr, m.rotation); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Pose& m) { + serialize_cdr(cdr, m.position); + serialize_cdr(cdr, m.orientation); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Pose& m) { + deserialize_cdr(cdr, m.position); + deserialize_cdr(cdr, m.orientation); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Clock& m) { + serialize_cdr(cdr, m.clock); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Clock& m) { + deserialize_cdr(cdr, m.clock); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::String& m) { + cdr << m.data; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::String& m) { + cdr >> m.data; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::TransformStamped& m) { + serialize_cdr(cdr, m.header); + cdr << m.child_frame_id; + serialize_cdr(cdr, m.transform); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::TransformStamped& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.child_frame_id; + deserialize_cdr(cdr, m.transform); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::TwistWithCovariance& m) { + serialize_cdr(cdr, m.twist); + cdr << m.covariance; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::TwistWithCovariance& m) { + deserialize_cdr(cdr, m.twist); + cdr >> m.covariance; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::PoseWithCovariance& m) { + serialize_cdr(cdr, m.pose); + cdr << m.covariance; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::PoseWithCovariance& m) { + deserialize_cdr(cdr, m.pose); + cdr >> m.covariance; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::AckermannDriveStamped& m) { + serialize_cdr(cdr, m.header); + serialize_cdr(cdr, m.drive); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::AckermannDriveStamped& m) { + deserialize_cdr(cdr, m.header); + deserialize_cdr(cdr, m.drive); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::CarlaCollisionEvent& m) { + serialize_cdr(cdr, m.header); + cdr << m.other_actor_id; + serialize_cdr(cdr, m.normal_impulse); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::CarlaCollisionEvent& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.other_actor_id; + deserialize_cdr(cdr, m.normal_impulse); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::NavSatFix& m) { + serialize_cdr(cdr, m.header); + serialize_cdr(cdr, m.status); + cdr << m.latitude; + cdr << m.longitude; + cdr << m.altitude; + cdr << m.position_covariance; + cdr << m.position_covariance_type; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::NavSatFix& m) { + deserialize_cdr(cdr, m.header); + deserialize_cdr(cdr, m.status); + cdr >> m.latitude; + cdr >> m.longitude; + cdr >> m.altitude; + cdr >> m.position_covariance; + cdr >> m.position_covariance_type; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Imu& m) { + serialize_cdr(cdr, m.header); + serialize_cdr(cdr, m.orientation); + cdr << m.orientation_covariance; + serialize_cdr(cdr, m.angular_velocity); + cdr << m.angular_velocity_covariance; + serialize_cdr(cdr, m.linear_acceleration); + cdr << m.linear_acceleration_covariance; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Imu& m) { + deserialize_cdr(cdr, m.header); + deserialize_cdr(cdr, m.orientation); + cdr >> m.orientation_covariance; + deserialize_cdr(cdr, m.angular_velocity); + cdr >> m.angular_velocity_covariance; + deserialize_cdr(cdr, m.linear_acceleration); + cdr >> m.linear_acceleration_covariance; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::CarlaEgoVehicleControl& m) { + serialize_cdr(cdr, m.header); + cdr << m.throttle; + cdr << m.steer; + cdr << m.brake; + cdr << m.hand_brake; + cdr << m.reverse; + cdr << m.gear; + cdr << m.manual_gear_shift; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::CarlaEgoVehicleControl& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.throttle; + cdr >> m.steer; + cdr >> m.brake; + cdr >> m.hand_brake; + cdr >> m.reverse; + cdr >> m.gear; + cdr >> m.manual_gear_shift; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::CarlaLineInvasion& m) { + serialize_cdr(cdr, m.header); + cdr << m.crossed_lane_markings; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::CarlaLineInvasion& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.crossed_lane_markings; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Odometry& m) { + serialize_cdr(cdr, m.header); + cdr << m.child_frame_id; + serialize_cdr(cdr, m.pose); + serialize_cdr(cdr, m.twist); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Odometry& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.child_frame_id; + deserialize_cdr(cdr, m.pose); + deserialize_cdr(cdr, m.twist); +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::Image& m) { + serialize_cdr(cdr, m.header); + cdr << m.height; + cdr << m.width; + cdr << m.encoding; + cdr << m.is_bigendian; + cdr << m.step; + cdr << m.data; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::Image& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.height; + cdr >> m.width; + cdr >> m.encoding; + cdr >> m.is_bigendian; + cdr >> m.step; + cdr >> m.data; +} + +// -- + +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::CameraInfo& m) { + serialize_cdr(cdr, m.header); + cdr << m.height; + cdr << m.width; + cdr << m.distortion_model; + cdr << m.d; + cdr << m.k; + cdr << m.r; + cdr << m.p; + cdr << m.binning_x; + cdr << m.binning_y; + serialize_cdr(cdr, m.roi); +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::CameraInfo& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.height; + cdr >> m.width; + cdr >> m.distortion_model; + cdr >> m.d; + cdr >> m.k; + cdr >> m.r; + cdr >> m.p; + cdr >> m.binning_x; + cdr >> m.binning_y; + deserialize_cdr(cdr, m.roi); +} + +// -- + +/// PointCloud2::fields is a sequence of structs; FastCDR's generic vector +/// operator calls serialize() on each element, which our POD types don't +/// provide. Write length + elements manually. +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::PointCloud2& m) { + serialize_cdr(cdr, m.header); + cdr << m.height; + cdr << m.width; + // CDR sequence length is uint32_t per DDS-XTypes 1.3 clause 7.4.1.1. + cdr << static_cast(m.fields.size()); + for (const auto& f : m.fields) { + serialize_cdr(cdr, f); + } + cdr << m.is_bigendian; + cdr << m.point_step; + cdr << m.row_step; + cdr << m.data; + cdr << m.is_dense; +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::PointCloud2& m) { + deserialize_cdr(cdr, m.header); + cdr >> m.height; + cdr >> m.width; + uint32_t fields_size{0u}; + cdr >> fields_size; + if (fields_size > kMaxCdrSequenceElements) { + throw eprosima::fastcdr::exception::BadParamException( + "PointCloud2::fields length exceeds sane CDR sequence cap"); + } + m.fields.resize(static_cast(fields_size)); + for (auto& f : m.fields) { + deserialize_cdr(cdr, f); + } + cdr >> m.is_bigendian; + cdr >> m.point_step; + cdr >> m.row_step; + cdr >> m.data; + cdr >> m.is_dense; +} + +// -- + +/// TFMessage::transforms is a sequence of TransformStamped structs. +/// Write length + elements manually. +inline void serialize_cdr( + eprosima::fastcdr::Cdr& cdr, const msg::TFMessage& m) { + // CDR sequence length is uint32_t per DDS-XTypes 1.3 clause 7.4.1.1. + cdr << static_cast(m.transforms.size()); + for (const auto& t : m.transforms) { + serialize_cdr(cdr, t); + } +} + +inline void deserialize_cdr( + eprosima::fastcdr::Cdr& cdr, msg::TFMessage& m) { + uint32_t transforms_size{0u}; + cdr >> transforms_size; + if (transforms_size > kMaxCdrSequenceElements) { + throw eprosima::fastcdr::exception::BadParamException( + "TFMessage::transforms length exceeds sane CDR sequence cap"); + } + m.transforms.resize(static_cast(transforms_size)); + for (auto& t : m.transforms) { + deserialize_cdr(cdr, t); + } +} + +// ========================================================================== +// Public API +// ========================================================================== + +/// Serialize a msg::X to a CDR byte buffer including the DDS encapsulation +/// header (Classic CDR, encoding version 1, little-endian). The returned +/// buffer is wire-compatible with all ROS2 distros and can be passed to +/// FastDDS write_serialized_payload() or CycloneDDS dds_writecdr(). +/// Returns an empty vector if Fast-CDR raises an exception (e.g. out of +/// memory while growing the internal FastBuffer). +template +std::vector serialize_to_cdr(const T& msg) { + eprosima::fastcdr::FastBuffer fb; + // Force LITTLE_ENDIANNESS so the encapsulation header is CDR_LE + // ({0x00, 0x01}) per DDSI-RTPS v2.5 Table 10.3, regardless of host + // endianness. ROS2 ecosystems test against CDR_LE. + eprosima::fastcdr::Cdr cdr{ + fb, + eprosima::fastcdr::Cdr::LITTLE_ENDIANNESS, + eprosima::fastcdr::Cdr::DDS_CDR}; + try { + cdr.serialize_encapsulation(); + serialize_cdr(cdr, msg); + } catch (const eprosima::fastcdr::exception::Exception&) { + return std::vector{}; + } + const char* buf{fb.getBuffer()}; + const size_t len{cdr.getSerializedDataLength()}; + return std::vector{ + reinterpret_cast(buf), + reinterpret_cast(buf) + len}; +} + +/// Return the exact CDR-serialized size in bytes for a message instance, +/// including the 4-byte DDS encapsulation header. Used by GenericCdrPubSubType +/// to tell FastDDS the actual payload size before write(), so the payload +/// buffer is sized correctly for variable-length fields (e.g. Image::data, +/// PointCloud2::data). The result is provably equal to serialize_to_cdr(msg).size(). +template +uint32_t cdr_serialized_size(const T& msg) { + eprosima::fastcdr::FastBuffer fb; + eprosima::fastcdr::Cdr cdr{ + fb, + eprosima::fastcdr::Cdr::LITTLE_ENDIANNESS, + eprosima::fastcdr::Cdr::DDS_CDR}; + cdr.serialize_encapsulation(); + serialize_cdr(cdr, msg); + return static_cast(cdr.getSerializedDataLength()); +} + +/// Deserialize a msg::X from a CDR byte buffer that was produced by +/// serialize_to_cdr() or by any ROS2-compatible DDS middleware. +/// Returns true on success, false on any Fast-CDR error (truncated buffer, +/// malformed encapsulation header, sequence length exceeding the sanity +/// cap, etc.). The buffer must include the 4-byte DDS encapsulation header. +template +bool deserialize_from_cdr( + const uint8_t* data, size_t size, T& msg) { + eprosima::fastcdr::FastBuffer fb{ + // FastBuffer requires a non-const pointer; the buffer is only read. + reinterpret_cast(const_cast(data)), + size}; + eprosima::fastcdr::Cdr cdr{ + fb, + eprosima::fastcdr::Cdr::LITTLE_ENDIANNESS, + eprosima::fastcdr::Cdr::DDS_CDR}; + try { + cdr.read_encapsulation(); + deserialize_cdr(cdr, msg); + } catch (const eprosima::fastcdr::exception::Exception&) { + return false; + } + return true; +} + +} // namespace ros2 +} // namespace carla diff --git a/LibCarla/source/carla/ros2/types/CdrTopicInfo.h b/LibCarla/source/carla/ros2/types/CdrTopicInfo.h new file mode 100644 index 00000000000..1b9b10c1ce7 --- /dev/null +++ b/LibCarla/source/carla/ros2/types/CdrTopicInfo.h @@ -0,0 +1,285 @@ +// Copyright (c) 2025 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB). +// This work is licensed under the terms of the MIT license. +// For a copy, see . + +#pragma once + +#include + +#include "carla/ros2/types/msg/AckermannDrive.h" +#include "carla/ros2/types/msg/AckermannDriveStamped.h" +#include "carla/ros2/types/msg/CameraInfo.h" +#include "carla/ros2/types/msg/CarlaCollisionEvent.h" +#include "carla/ros2/types/msg/CarlaEgoVehicleControl.h" +#include "carla/ros2/types/msg/CarlaLineInvasion.h" +#include "carla/ros2/types/msg/Clock.h" +#include "carla/ros2/types/msg/Float32.h" +#include "carla/ros2/types/msg/Header.h" +#include "carla/ros2/types/msg/Image.h" +#include "carla/ros2/types/msg/Imu.h" +#include "carla/ros2/types/msg/NavSatFix.h" +#include "carla/ros2/types/msg/NavSatStatus.h" +#include "carla/ros2/types/msg/Odometry.h" +#include "carla/ros2/types/msg/Point.h" +#include "carla/ros2/types/msg/Point32.h" +#include "carla/ros2/types/msg/PointCloud2.h" +#include "carla/ros2/types/msg/PointField.h" +#include "carla/ros2/types/msg/Pose.h" +#include "carla/ros2/types/msg/PoseWithCovariance.h" +#include "carla/ros2/types/msg/Quaternion.h" +#include "carla/ros2/types/msg/RegionOfInterest.h" +#include "carla/ros2/types/msg/String.h" +#include "carla/ros2/types/msg/TF2Error.h" +#include "carla/ros2/types/msg/TFMessage.h" +#include "carla/ros2/types/msg/Time.h" +#include "carla/ros2/types/msg/Transform.h" +#include "carla/ros2/types/msg/TransformStamped.h" +#include "carla/ros2/types/msg/Twist.h" +#include "carla/ros2/types/msg/TwistWithCovariance.h" +#include "carla/ros2/types/msg/Vector3.h" + +namespace carla { +namespace ros2 { + +/// Per-type metadata needed by the DDS middleware layer. +/// +/// type_name() — ROS2-compatible DDS type name string used when +/// registering the type with a DomainParticipant. +/// Follows the "pkg::msg::dds_::TypeName_" pattern. +/// +/// max_serialized_size() — Initial preallocation hint for the CDR payload size +/// in bytes, excluding the 4-byte DDS encapsulation +/// header. Used by FastDDS to pre-allocate payload +/// buffers. For types with variable-length fields +/// (strings, vectors) this is a minimum hint, not a +/// hard limit. The actual size per message instance is +/// computed dynamically by cdr_serialized_size(). +/// +/// Primary template is intentionally undefined — only specializations are +/// valid. +template struct CdrTopicInfo; + +// ========================================================================== +// Specializations — ordered alphabetically by C++ type name +// ========================================================================== + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "ackermann_msgs::msg::dds_::AckermannDrive_"; + } + static size_t max_serialized_size() { return 20u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "ackermann_msgs::msg::dds_::AckermannDriveStamped_"; + } + static size_t max_serialized_size() { return 288u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::CameraInfo_"; + } + static size_t max_serialized_size() { return 3793u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "carla_msgs::msg::dds_::CarlaCollisionEvent_"; + } + static size_t max_serialized_size() { return 296u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "carla_msgs::msg::dds_::CarlaEgoVehicleControl_"; + } + static size_t max_serialized_size() { return 289u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "carla_msgs::msg::dds_::LaneInvasionEvent_"; + } + static size_t max_serialized_size() { return 672u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "rosgraph_msgs::msg::dds_::Clock_"; + } + // Clock holds one Time (8 bytes = 2 × int32). + static size_t max_serialized_size() { return 8u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "std_msgs::msg::dds_::Float32_"; + } + static size_t max_serialized_size() { return 4u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "std_msgs::msg::dds_::Header_"; + } + static size_t max_serialized_size() { return 268u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::Image_"; + } + static size_t max_serialized_size() { return 648u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::Imu_"; + } + static size_t max_serialized_size() { return 568u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::NavSatFix_"; + } + static size_t max_serialized_size() { return 369u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::NavSatStatus_"; + } + static size_t max_serialized_size() { return 4u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "nav_msgs::msg::dds_::Odometry_"; + } + static size_t max_serialized_size() { return 1208u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Point_"; + } + static size_t max_serialized_size() { return 24u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Point32_"; + } + static size_t max_serialized_size() { return 12u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::PointCloud2_"; + } + static size_t max_serialized_size() { return 27597u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::PointField_"; + } + static size_t max_serialized_size() { return 272u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Pose_"; + } + static size_t max_serialized_size() { return 56u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::PoseWithCovariance_"; + } + static size_t max_serialized_size() { return 344u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Quaternion_"; + } + static size_t max_serialized_size() { return 32u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "sensor_msgs::msg::dds_::RegionOfInterest_"; + } + static size_t max_serialized_size() { return 17u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "std_msgs::msg::dds_::String_"; + } + static size_t max_serialized_size() { return 260u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "tf2_msgs::msg::dds_::TF2Error_"; + } + static size_t max_serialized_size() { return 264u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "tf2_msgs::msg::dds_::TFMessage_"; + } + static size_t max_serialized_size() { return 58408u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "builtin_interfaces::msg::dds_::Time_"; + } + static size_t max_serialized_size() { return 8u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Transform_"; + } + static size_t max_serialized_size() { return 56u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::TransformStamped_"; + } + static size_t max_serialized_size() { return 584u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Twist_"; + } + static size_t max_serialized_size() { return 48u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::TwistWithCovariance_"; + } + static size_t max_serialized_size() { return 336u; } +}; + +template<> struct CdrTopicInfo { + static const char* type_name() { + return "geometry_msgs::msg::dds_::Vector3_"; + } + static size_t max_serialized_size() { return 24u; } +}; + +} // namespace ros2 +} // namespace carla diff --git a/LibCarla/source/test/server/test_dds_middleware.cpp b/LibCarla/source/test/server/test_dds_middleware.cpp index 5ec7116292c..22ccce3e2fe 100644 --- a/LibCarla/source/test/server/test_dds_middleware.cpp +++ b/LibCarla/source/test/server/test_dds_middleware.cpp @@ -15,9 +15,15 @@ #include #include #include +#include +#include +#include +#include +#include #include #include +#include using namespace carla::ros2; @@ -538,3 +544,657 @@ TEST(subscriber_impl, init_failure_propagated) { std::unique_ptr(mock)); EXPECT_FALSE(sub.Init("rt/test_topic")); } + +// ========================================================================== +// Group 9: cdr_topic_info (2 tests) +// ========================================================================== + +TEST(cdr_topic_info, type_names_are_non_empty) { + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); + EXPECT_STRNE("", carla::ros2::CdrTopicInfo::type_name()); +} + +TEST(cdr_topic_info, max_sizes_are_positive) { + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); + EXPECT_GT(carla::ros2::CdrTopicInfo::max_serialized_size(), 0u); +} + +// ========================================================================== +// Group 10: cdr_serialization (21 tests) +// ========================================================================== + +TEST(cdr_serialization, time_round_trip) { + carla::ros2::msg::Time original{}; + original.sec = 42; + original.nanosec = 123456789u; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Time recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.sec, 42); + EXPECT_EQ(recovered.nanosec, 123456789u); +} + +TEST(cdr_serialization, header_round_trip) { + carla::ros2::msg::Header original{}; + original.stamp.sec = 10; + original.stamp.nanosec = 500000000u; + original.frame_id = "base_link"; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Header recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.stamp.sec, 10); + EXPECT_EQ(recovered.stamp.nanosec, 500000000u); + EXPECT_EQ(recovered.frame_id, "base_link"); +} + +TEST(cdr_serialization, vector3_round_trip) { + carla::ros2::msg::Vector3 original{}; + original.x = 1.5; + original.y = -2.75; + original.z = 3.0; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Vector3 recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_DOUBLE_EQ(recovered.x, 1.5); + EXPECT_DOUBLE_EQ(recovered.y, -2.75); + EXPECT_DOUBLE_EQ(recovered.z, 3.0); +} + +TEST(cdr_serialization, imu_round_trip) { + carla::ros2::msg::Imu original{}; + original.header.stamp.sec = 5; + original.header.frame_id = "imu_link"; + original.orientation.x = 0.1; + original.orientation.y = 0.2; + original.orientation.z = 0.3; + original.orientation.w = 0.9; + original.angular_velocity.x = 0.01; + original.linear_acceleration.z = 9.81; + original.orientation_covariance[0] = 1.0; + original.orientation_covariance[4] = 1.0; + original.orientation_covariance[8] = 1.0; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Imu recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.header.stamp.sec, 5); + EXPECT_EQ(recovered.header.frame_id, "imu_link"); + EXPECT_DOUBLE_EQ(recovered.orientation.x, 0.1); + EXPECT_DOUBLE_EQ(recovered.orientation.w, 0.9); + EXPECT_DOUBLE_EQ(recovered.linear_acceleration.z, 9.81); + EXPECT_DOUBLE_EQ(recovered.orientation_covariance[0], 1.0); + EXPECT_DOUBLE_EQ(recovered.orientation_covariance[4], 1.0); +} + +TEST(cdr_serialization, image_round_trip) { + carla::ros2::msg::Image original{}; + original.header.frame_id = "camera"; + original.height = 2u; + original.width = 3u; + original.encoding = "rgb8"; + original.is_bigendian = 0u; + original.step = 9u; + original.data = {1u, 2u, 3u, 4u, 5u, 6u}; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Image recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.header.frame_id, "camera"); + EXPECT_EQ(recovered.height, 2u); + EXPECT_EQ(recovered.width, 3u); + EXPECT_EQ(recovered.encoding, "rgb8"); + ASSERT_EQ(recovered.data.size(), 6u); + EXPECT_EQ(recovered.data[0], 1u); + EXPECT_EQ(recovered.data[5], 6u); +} + +TEST(cdr_serialization, pointcloud2_round_trip) { + carla::ros2::msg::PointCloud2 original{}; + original.header.frame_id = "velodyne"; + original.height = 1u; + original.width = 2u; + + carla::ros2::msg::PointField pf{}; + pf.name = "x"; + pf.offset = 0u; + pf.datatype = static_cast(carla::ros2::msg::PointField::FLOAT32); + pf.count = 1u; + original.fields.push_back(pf); + + original.is_bigendian = false; + original.point_step = 4u; + original.row_step = 8u; + original.data = {0u, 0u, 128u, 63u, // 1.0f LE + 0u, 0u, 0u, 64u}; // 2.0f LE + original.is_dense = true; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::PointCloud2 recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.header.frame_id, "velodyne"); + EXPECT_EQ(recovered.height, 1u); + EXPECT_EQ(recovered.width, 2u); + ASSERT_EQ(recovered.fields.size(), 1u); + EXPECT_EQ(recovered.fields[0].name, "x"); + EXPECT_EQ(recovered.fields[0].datatype, static_cast(carla::ros2::msg::PointField::FLOAT32)); + EXPECT_EQ(recovered.is_dense, true); + ASSERT_EQ(recovered.data.size(), 8u); +} + +TEST(cdr_serialization, tfmessage_round_trip) { + carla::ros2::msg::TFMessage original{}; + + carla::ros2::msg::TransformStamped ts{}; + ts.header.stamp.sec = 1; + ts.header.frame_id = "world"; + ts.child_frame_id = "robot"; + ts.transform.translation.x = 1.0; + ts.transform.translation.y = 2.0; + ts.transform.translation.z = 0.5; + ts.transform.rotation.w = 1.0; + original.transforms.push_back(ts); + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::TFMessage recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + ASSERT_EQ(recovered.transforms.size(), 1u); + EXPECT_EQ(recovered.transforms[0].header.stamp.sec, 1); + EXPECT_EQ(recovered.transforms[0].header.frame_id, "world"); + EXPECT_EQ(recovered.transforms[0].child_frame_id, "robot"); + EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.x, 1.0); + EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.y, 2.0); + EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.rotation.w, 1.0); +} + +TEST(cdr_serialization, navsat_fix_round_trip) { + carla::ros2::msg::NavSatFix original{}; + original.header.frame_id = "gps"; + original.status.status = static_cast(carla::ros2::msg::NavSatStatus::STATUS_FIX); + original.status.service = static_cast(carla::ros2::msg::NavSatStatus::SERVICE_GPS); + original.latitude = 48.8566; + original.longitude = 2.3522; + original.altitude = 35.0; + original.position_covariance[0] = 0.01; + original.position_covariance_type = + static_cast(carla::ros2::msg::NavSatFix::COVARIANCE_TYPE_DIAGONAL_KNOWN); + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::NavSatFix recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.header.frame_id, "gps"); + EXPECT_EQ( + recovered.status.status, + static_cast(carla::ros2::msg::NavSatStatus::STATUS_FIX)); + EXPECT_DOUBLE_EQ(recovered.latitude, 48.8566); + EXPECT_DOUBLE_EQ(recovered.longitude, 2.3522); + EXPECT_DOUBLE_EQ(recovered.altitude, 35.0); + EXPECT_DOUBLE_EQ(recovered.position_covariance[0], 0.01); + EXPECT_EQ( + recovered.position_covariance_type, + static_cast(carla::ros2::msg::NavSatFix::COVARIANCE_TYPE_DIAGONAL_KNOWN)); +} + +TEST(cdr_serialization, carla_ego_vehicle_control_round_trip) { + carla::ros2::msg::CarlaEgoVehicleControl original{}; + original.throttle = 0.75f; + original.steer = -0.5f; + original.brake = 0.0f; + original.hand_brake = false; + original.reverse = true; + original.gear = 2; + original.manual_gear_shift = false; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::CarlaEgoVehicleControl recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_FLOAT_EQ(recovered.throttle, 0.75f); + EXPECT_FLOAT_EQ(recovered.steer, -0.5f); + EXPECT_FLOAT_EQ(recovered.brake, 0.0f); + EXPECT_EQ(recovered.hand_brake, false); + EXPECT_EQ(recovered.reverse, true); + EXPECT_EQ(recovered.gear, 2); + EXPECT_EQ(recovered.manual_gear_shift, false); +} + +TEST(cdr_serialization, carla_line_invasion_round_trip) { + carla::ros2::msg::CarlaLineInvasion original{}; + original.header.frame_id = "vehicle"; + original.crossed_lane_markings = {1, 4, 7}; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::CarlaLineInvasion recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.header.frame_id, "vehicle"); + ASSERT_EQ(recovered.crossed_lane_markings.size(), 3u); + EXPECT_EQ(recovered.crossed_lane_markings[0], 1); + EXPECT_EQ(recovered.crossed_lane_markings[1], 4); + EXPECT_EQ(recovered.crossed_lane_markings[2], 7); +} + +TEST(cdr_serialization, clock_round_trip) { + carla::ros2::msg::Clock original{}; + original.clock.sec = 999; + original.clock.nanosec = 1u; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::Clock recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_EQ(recovered.clock.sec, 999); + EXPECT_EQ(recovered.clock.nanosec, 1u); +} + +TEST(cdr_serialization, empty_tfmessage_round_trip) { + carla::ros2::msg::TFMessage original{}; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::TFMessage recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + EXPECT_TRUE(recovered.transforms.empty()); +} + +TEST(cdr_serialization, pointcloud2_multi_field_round_trip) { + // Exercises the manual sequence loop in serialize_cdr/deserialize_cdr for + // PointCloud2::fields with more than one element. The single-field + // round-trip above does not catch a bug in the loop step. + carla::ros2::msg::PointCloud2 original{}; + original.header.frame_id = "lidar"; + original.height = 1u; + original.width = 4u; + + carla::ros2::msg::PointField fx{}; + fx.name = "x"; + fx.offset = 0u; + fx.datatype = carla::ros2::msg::PointField::FLOAT32; + fx.count = 1u; + + carla::ros2::msg::PointField fy{}; + fy.name = "y"; + fy.offset = 4u; + fy.datatype = carla::ros2::msg::PointField::FLOAT32; + fy.count = 1u; + + carla::ros2::msg::PointField fz{}; + fz.name = "z"; + fz.offset = 8u; + fz.datatype = carla::ros2::msg::PointField::FLOAT32; + fz.count = 1u; + + original.fields.push_back(fx); + original.fields.push_back(fy); + original.fields.push_back(fz); + original.is_bigendian = false; + original.point_step = 12u; + original.row_step = 48u; + original.data.resize(48u, 0u); + original.is_dense = true; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::PointCloud2 recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + ASSERT_EQ(recovered.fields.size(), 3u); + EXPECT_EQ(recovered.fields[0].name, "x"); + EXPECT_EQ(recovered.fields[0].offset, 0u); + EXPECT_EQ(recovered.fields[1].name, "y"); + EXPECT_EQ(recovered.fields[1].offset, 4u); + EXPECT_EQ(recovered.fields[2].name, "z"); + EXPECT_EQ(recovered.fields[2].offset, 8u); + EXPECT_EQ(recovered.point_step, 12u); + EXPECT_EQ(recovered.row_step, 48u); + ASSERT_EQ(recovered.data.size(), 48u); +} + +TEST(cdr_serialization, tfmessage_multi_transform_round_trip) { + // Exercises the manual sequence loop for TFMessage::transforms with more + // than one element. + carla::ros2::msg::TFMessage original{}; + + carla::ros2::msg::TransformStamped a{}; + a.header.stamp.sec = 1; + a.header.frame_id = "world"; + a.child_frame_id = "robot_a"; + a.transform.translation.x = 1.0; + a.transform.rotation.w = 1.0; + + carla::ros2::msg::TransformStamped b{}; + b.header.stamp.sec = 2; + b.header.frame_id = "world"; + b.child_frame_id = "robot_b"; + b.transform.translation.y = 2.0; + b.transform.rotation.w = 1.0; + + original.transforms.push_back(a); + original.transforms.push_back(b); + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_FALSE(buf.empty()); + + carla::ros2::msg::TFMessage recovered{}; + EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered)); + ASSERT_EQ(recovered.transforms.size(), 2u); + EXPECT_EQ(recovered.transforms[0].child_frame_id, "robot_a"); + EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.x, 1.0); + EXPECT_EQ(recovered.transforms[1].child_frame_id, "robot_b"); + EXPECT_DOUBLE_EQ(recovered.transforms[1].transform.translation.y, 2.0); +} + +TEST(cdr_serialization, deserialize_truncated_returns_false) { + // A truncated buffer must produce a clean false return, not an uncaught + // Fast-CDR exception leaking out of the bool API. + carla::ros2::msg::Header original{}; + original.stamp.sec = 7; + original.frame_id = "needs_more_bytes"; + + auto buf = carla::ros2::serialize_to_cdr(original); + ASSERT_GT(buf.size(), 8u); + + carla::ros2::msg::Header recovered{}; + EXPECT_FALSE(carla::ros2::deserialize_from_cdr( + buf.data(), buf.size() / 2u, recovered)); +} + +TEST(cdr_serialization, deserialize_corrupt_encapsulation_returns_false) { + // A buffer too small to even hold the 4-byte encapsulation header must + // produce a clean false return. + const uint8_t bogus[2] = {0xFFu, 0xFFu}; + carla::ros2::msg::Time recovered{}; + EXPECT_FALSE(carla::ros2::deserialize_from_cdr(bogus, sizeof(bogus), recovered)); +} + +TEST(cdr_serialization, deserialize_pointcloud2_hostile_length_returns_false) { + // Hand-craft a PointCloud2 buffer that claims its sequence has a hostile + // length (max uint32). Without the kMaxCdrSequenceElements cap, the + // call would attempt a multi-GB resize and abort the process. + std::vector buf; + // Encapsulation header: CDR_LE + options. + buf.push_back(0x00u); + buf.push_back(0x01u); + buf.push_back(0x00u); + buf.push_back(0x00u); + // Header.stamp.sec (int32) + nanosec (uint32) = 8 bytes of zeros. + for (int i = 0; i < 8; ++i) buf.push_back(0x00u); + // Header.frame_id (string): length 1 (NUL only) + "\0" + 3 padding bytes + // to keep alignment for the next uint32. + buf.push_back(0x01u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0x00u); + // PointCloud2.height (uint32) + width (uint32). + for (int i = 0; i < 8; ++i) buf.push_back(0x00u); + // fields_size = 0xFFFFFFFF (hostile). + buf.push_back(0xFFu); + buf.push_back(0xFFu); + buf.push_back(0xFFu); + buf.push_back(0xFFu); + + carla::ros2::msg::PointCloud2 recovered{}; + EXPECT_FALSE(carla::ros2::deserialize_from_cdr( + buf.data(), buf.size(), recovered)); +} + +TEST(cdr_serialization, deserialize_tfmessage_hostile_length_returns_false) { + // Same idea for TFMessage::transforms — claim a 4-billion-element + // sequence and verify the cap rejects it instead of OOM-aborting. + std::vector buf; + buf.push_back(0x00u); + buf.push_back(0x01u); + buf.push_back(0x00u); + buf.push_back(0x00u); + buf.push_back(0xFFu); + buf.push_back(0xFFu); + buf.push_back(0xFFu); + buf.push_back(0xFFu); + + carla::ros2::msg::TFMessage recovered{}; + EXPECT_FALSE(carla::ros2::deserialize_from_cdr( + buf.data(), buf.size(), recovered)); +} + +TEST(cdr_serialization, cdr_serialized_size_matches_serialize_to_cdr) { + // cdr_serialized_size(msg) must return the same byte count as + // serialize_to_cdr(msg).size() for every message type. This is the contract + // that GenericCdrPubSubType::getSerializedSizeProvider relies on. + { + carla::ros2::msg::Header msg{}; + msg.stamp.sec = 42; + msg.frame_id = "map"; + EXPECT_EQ(carla::ros2::cdr_serialized_size(msg), + carla::ros2::serialize_to_cdr(msg).size()); + } + { + carla::ros2::msg::Image msg{}; + msg.height = 2u; + msg.width = 3u; + msg.encoding = "rgb8"; + msg.data.assign(6u, 0xAAu); + EXPECT_EQ(carla::ros2::cdr_serialized_size(msg), + carla::ros2::serialize_to_cdr(msg).size()); + } + { + carla::ros2::msg::PointCloud2 msg{}; + msg.height = 1u; + msg.width = 4u; + msg.data.assign(48u, 0xBBu); + EXPECT_EQ(carla::ros2::cdr_serialized_size(msg), + carla::ros2::serialize_to_cdr(msg).size()); + } + { + carla::ros2::msg::TFMessage msg{}; + msg.transforms.resize(2u); + msg.transforms[0].header.frame_id = "world"; + msg.transforms[1].header.frame_id = "base_link"; + EXPECT_EQ(carla::ros2::cdr_serialized_size(msg), + carla::ros2::serialize_to_cdr(msg).size()); + } +} + +TEST(cdr_serialization, cdr_serialized_size_image_exceeds_static_max) { + // A real 800x600 RGB camera frame is ~1.4 MB. The static + // CdrTopicInfo::max_serialized_size() is only 648 bytes. + // cdr_serialized_size() must return a value greater than the static max, + // and the round-trip must recover the original data.size(). + carla::ros2::msg::Image msg{}; + msg.height = 600u; + msg.width = 800u; + msg.encoding = "rgb8"; + msg.step = 800u * 3u; + const size_t data_bytes = 800u * 600u * 3u; // 1,440,000 bytes + msg.data.assign(data_bytes, 0x7Fu); + + const uint32_t computed = carla::ros2::cdr_serialized_size(msg); + EXPECT_GT(computed, + static_cast( + carla::ros2::CdrTopicInfo::max_serialized_size())); + + const auto bytes = carla::ros2::serialize_to_cdr(msg); + ASSERT_FALSE(bytes.empty()); + EXPECT_EQ(computed, static_cast(bytes.size())); + + carla::ros2::msg::Image recovered{}; + ASSERT_TRUE(carla::ros2::deserialize_from_cdr( + bytes.data(), bytes.size(), recovered)); + EXPECT_EQ(recovered.data.size(), data_bytes); +} + +TEST(cdr_serialization, cdr_serialized_size_pointcloud2_exceeds_static_max) { + // A typical LiDAR scan is 1-20 MB. The static max_serialized_size() for + // PointCloud2 is 27597 bytes. This test uses ~1 MB of data to verify the + // same contract as the Image test above. + carla::ros2::msg::PointCloud2 msg{}; + msg.height = 1u; + msg.width = 22000u; + msg.row_step = 22000u * 16u; + const size_t data_bytes = 22000u * 16u; // ~352,000 bytes (~0.35 MB) + msg.data.assign(data_bytes, 0x3Cu); + carla::ros2::msg::PointField pf{}; + pf.name = "x"; + pf.offset = 0u; + pf.datatype = 7u; // FLOAT32 + pf.count = 1u; + msg.fields.push_back(pf); + + const uint32_t computed = carla::ros2::cdr_serialized_size(msg); + EXPECT_GT(computed, + static_cast( + carla::ros2::CdrTopicInfo::max_serialized_size())); + + const auto bytes = carla::ros2::serialize_to_cdr(msg); + ASSERT_FALSE(bytes.empty()); + EXPECT_EQ(computed, static_cast(bytes.size())); + + carla::ros2::msg::PointCloud2 recovered{}; + ASSERT_TRUE(carla::ros2::deserialize_from_cdr( + bytes.data(), bytes.size(), recovered)); + EXPECT_EQ(recovered.data.size(), data_bytes); +} + +// ========================================================================== +// Group 11: generic_cdr_pubsubtype (6 tests) +// Tests for GenericCdrPubSubType — the single FastDDS TopicDataType +// implementation that replaces 30 fastddsgen-generated PubSubType classes. +// ========================================================================== + +TEST(generic_cdr_pubsubtype, type_name_matches_cdr_topic_info) { + // The name set in the GenericCdrPubSubType constructor must equal + // CdrTopicInfo::type_name() — FastDDS uses it for publisher/subscriber matching. + EXPECT_STREQ( + CdrTopicInfo::type_name(), + GenericCdrPubSubType().getName()); + EXPECT_STREQ( + CdrTopicInfo::type_name(), + GenericCdrPubSubType().getName()); + EXPECT_STREQ( + CdrTopicInfo::type_name(), + GenericCdrPubSubType().getName()); + EXPECT_STREQ( + CdrTopicInfo::type_name(), + GenericCdrPubSubType().getName()); + EXPECT_STREQ( + CdrTopicInfo::type_name(), + GenericCdrPubSubType().getName()); +} + +TEST(generic_cdr_pubsubtype, m_typesize_is_positive) { + // FastDDS uses m_typeSize to pre-allocate payload buffers. + // It must be > 0 for every type (min: max_serialized_size + 4 encapsulation bytes). + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); + EXPECT_GT(GenericCdrPubSubType().m_typeSize, 0u); +} + +TEST(generic_cdr_pubsubtype, serialize_deserialize_fixed_size_via_payload) { + // Round-trip a fixed-size type (Clock) through a SerializedPayload_t buffer. + // This exercises the exact code path FastDDS DataWriter/DataReader use. + using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t; + + GenericCdrPubSubType pubsub_type; + msg::Clock original{}; + original.clock.sec = 100; + original.clock.nanosec = 500u; + + SerializedPayload_t payload(1024u); + ASSERT_TRUE(pubsub_type.serialize(static_cast(&original), &payload)); + EXPECT_GT(payload.length, 0u); + + msg::Clock recovered{}; + ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast(&recovered))); + EXPECT_EQ(recovered.clock.sec, 100); + EXPECT_EQ(recovered.clock.nanosec, 500u); +} + +TEST(generic_cdr_pubsubtype, serialize_deserialize_string_type_via_payload) { + // Round-trip a type containing a std::string (Header) through SerializedPayload_t. + using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t; + + GenericCdrPubSubType pubsub_type; + msg::Header original{}; + original.stamp.sec = 42; + original.frame_id = "test_frame"; + + SerializedPayload_t payload(4096u); + ASSERT_TRUE(pubsub_type.serialize(static_cast(&original), &payload)); + EXPECT_GT(payload.length, 0u); + + msg::Header recovered{}; + ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast(&recovered))); + EXPECT_EQ(recovered.stamp.sec, 42); + EXPECT_EQ(recovered.frame_id, "test_frame"); +} + +TEST(generic_cdr_pubsubtype, create_and_delete_data) { + GenericCdrPubSubType pubsub_type; + + void* data = pubsub_type.createData(); + ASSERT_NE(data, nullptr); + + // Cast to verify it is a properly-constructed Clock + msg::Clock* clock = static_cast(data); + EXPECT_EQ(clock->clock.sec, 0); + EXPECT_EQ(clock->clock.nanosec, 0u); + + // Must not crash + pubsub_type.deleteData(data); +} + +TEST(generic_cdr_pubsubtype, getkey_returns_false) { + // CARLA has no keyed topics — getKey must always return false. + GenericCdrPubSubType pubsub_type; + EXPECT_FALSE(pubsub_type.m_isGetKeyDefined); + EXPECT_FALSE(pubsub_type.getKey(nullptr, nullptr, false)); +}