CARLA
Loading...
Searching...
No Matches
test_ros2_middleware.cpp
Go to the documentation of this file.
1// Copyright (c) 2025 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB).
2// This work is licensed under the terms of the MIT license.
3// For a copy, see <https://opensource.org/licenses/MIT>.
4
5// Must be defined before any includes to suppress real DDS auto-includes
6// (the vendor pub/sub middleware and FastDDSSharedParticipant), while keeping
7// the CARLA_ROS2_MIDDLEWARE_FASTDDS compile-time availability branches active.
8#define CARLA_ROS2_MIDDLEWARE_TESTING
9#define CARLA_ROS2_MIDDLEWARE_FASTDDS
10
11#include "test.h"
12
25
26#include <algorithm>
27#include <cstdint>
28#include <cstdlib>
29#include <memory>
30#include <string>
31#include <vector>
32
33// ==========================================================================
34// Group 1: cdr_topic_info (2 tests)
35// ==========================================================================
36
54
67
68// ==========================================================================
69// Group 2: cdr_serialization (22 tests)
70// ==========================================================================
71
72TEST(cdr_serialization, time_round_trip) {
73 carla::ros2::msg::Time original{};
74 original.sec = 42;
75 original.nanosec = 123456789u;
76
77 auto buf = carla::ros2::serialize_to_cdr(original);
78 ASSERT_FALSE(buf.empty());
79
80 carla::ros2::msg::Time recovered{};
81 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
82 EXPECT_EQ(recovered.sec, 42);
83 EXPECT_EQ(recovered.nanosec, 123456789u);
84}
85
86// Byte-exact wire-format guard. The symmetric round-trip tests above cannot
87// detect an encoding drift (e.g. Fast-CDR defaulting to XCDRv2, which inserts
88// DHEADERs) because deserialize_from_cdr would consume whatever encoding
89// serialize_to_cdr emitted. This test pins the literal bytes so a regression
90// to XCDRv2 or big-endian is caught deterministically. These exact bytes are
91// what every backend puts on the wire: FastDDS via write_serialized_payload,
92// CycloneDDS via dds_writecdr (raw passthrough of serialize_to_cdr), and Zenoh.
93// Layout (classic CDR, encoding version 1, little-endian):
94// 00 01 00 00 encapsulation header (PLAIN_CDR little-endian + 2 option bytes)
95// 2A 00 00 00 sec = int32 42 (0x0000002A, LE)
96// 15 CD 5B 07 nanosec= uint32 123456789 (0x075BCD15, LE)
97TEST(cdr_serialization, time_golden_xcdrv1_bytes) {
98 carla::ros2::msg::Time original{};
99 original.sec = 42;
100 original.nanosec = 123456789u;
101
102 const auto buf = carla::ros2::serialize_to_cdr(original);
103
104 const std::vector<uint8_t> expected{
105 0x00u, 0x01u, 0x00u, 0x00u,
106 0x2Au, 0x00u, 0x00u, 0x00u,
107 0x15u, 0xCDu, 0x5Bu, 0x07u};
108 // Assert the size first: a failed or short serialization then yields a
109 // targeted size mismatch instead of a noisy full-vector diff.
110 ASSERT_EQ(buf.size(), expected.size());
111 EXPECT_EQ(buf, expected);
112 // Discriminator byte: classic CDR_LE is 0x01; any XCDRv2 representation
113 // (PLAIN_CDR2 / DELIMIT_CDR2 / PL_CDR2) would change byte 1.
114 EXPECT_EQ(buf[0], 0x00u);
115 EXPECT_EQ(buf[1], 0x01u);
116}
117
118TEST(cdr_serialization, header_round_trip) {
119 carla::ros2::msg::Header original{};
120 original.stamp.sec = 10;
121 original.stamp.nanosec = 500000000u;
122 original.frame_id = "base_link";
123
124 auto buf = carla::ros2::serialize_to_cdr(original);
125 ASSERT_FALSE(buf.empty());
126
127 carla::ros2::msg::Header recovered{};
128 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
129 EXPECT_EQ(recovered.stamp.sec, 10);
130 EXPECT_EQ(recovered.stamp.nanosec, 500000000u);
131 EXPECT_EQ(recovered.frame_id, "base_link");
132}
133
134TEST(cdr_serialization, vector3_round_trip) {
135 carla::ros2::msg::Vector3 original{};
136 original.x = 1.5;
137 original.y = -2.75;
138 original.z = 3.0;
139
140 auto buf = carla::ros2::serialize_to_cdr(original);
141 ASSERT_FALSE(buf.empty());
142
143 carla::ros2::msg::Vector3 recovered{};
144 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
145 EXPECT_DOUBLE_EQ(recovered.x, 1.5);
146 EXPECT_DOUBLE_EQ(recovered.y, -2.75);
147 EXPECT_DOUBLE_EQ(recovered.z, 3.0);
148}
149
150TEST(cdr_serialization, imu_round_trip) {
151 carla::ros2::msg::Imu original{};
152 original.header.stamp.sec = 5;
153 original.header.frame_id = "imu_link";
154 original.orientation.x = 0.1;
155 original.orientation.y = 0.2;
156 original.orientation.z = 0.3;
157 original.orientation.w = 0.9;
158 original.angular_velocity.x = 0.01;
159 original.linear_acceleration.z = 9.81;
160 original.orientation_covariance[0] = 1.0;
161 original.orientation_covariance[4] = 1.0;
162 original.orientation_covariance[8] = 1.0;
163
164 auto buf = carla::ros2::serialize_to_cdr(original);
165 ASSERT_FALSE(buf.empty());
166
167 carla::ros2::msg::Imu recovered{};
168 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
169 EXPECT_EQ(recovered.header.stamp.sec, 5);
170 EXPECT_EQ(recovered.header.frame_id, "imu_link");
171 EXPECT_DOUBLE_EQ(recovered.orientation.x, 0.1);
172 EXPECT_DOUBLE_EQ(recovered.orientation.w, 0.9);
173 EXPECT_DOUBLE_EQ(recovered.linear_acceleration.z, 9.81);
174 EXPECT_DOUBLE_EQ(recovered.orientation_covariance[0], 1.0);
175 EXPECT_DOUBLE_EQ(recovered.orientation_covariance[4], 1.0);
176}
177
178TEST(cdr_serialization, image_round_trip) {
179 carla::ros2::msg::Image original{};
180 original.header.frame_id = "camera";
181 original.height = 2u;
182 original.width = 3u;
183 original.encoding = "rgb8";
184 original.is_bigendian = 0u;
185 original.step = 9u;
186 original.data = {1u, 2u, 3u, 4u, 5u, 6u};
187
188 auto buf = carla::ros2::serialize_to_cdr(original);
189 ASSERT_FALSE(buf.empty());
190
191 carla::ros2::msg::Image recovered{};
192 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
193 EXPECT_EQ(recovered.header.frame_id, "camera");
194 EXPECT_EQ(recovered.height, 2u);
195 EXPECT_EQ(recovered.width, 3u);
196 EXPECT_EQ(recovered.encoding, "rgb8");
197 ASSERT_EQ(recovered.data.size(), 6u);
198 EXPECT_EQ(recovered.data[0], 1u);
199 EXPECT_EQ(recovered.data[5], 6u);
200}
201
202TEST(cdr_serialization, pointcloud2_round_trip) {
204 original.header.frame_id = "velodyne";
205 original.height = 1u;
206 original.width = 2u;
207
209 pf.name = "x";
210 pf.offset = 0u;
211 pf.datatype = static_cast<uint8_t>(carla::ros2::msg::PointField::FLOAT32);
212 pf.count = 1u;
213 original.fields.push_back(pf);
214
215 original.is_bigendian = false;
216 original.point_step = 4u;
217 original.row_step = 8u;
218 original.data = {0u, 0u, 128u, 63u, // 1.0f LE
219 0u, 0u, 0u, 64u}; // 2.0f LE
220 original.is_dense = true;
221
222 auto buf = carla::ros2::serialize_to_cdr(original);
223 ASSERT_FALSE(buf.empty());
224
226 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
227 EXPECT_EQ(recovered.header.frame_id, "velodyne");
228 EXPECT_EQ(recovered.height, 1u);
229 EXPECT_EQ(recovered.width, 2u);
230 ASSERT_EQ(recovered.fields.size(), 1u);
231 EXPECT_EQ(recovered.fields[0].name, "x");
232 EXPECT_EQ(recovered.fields[0].datatype, static_cast<uint8_t>(carla::ros2::msg::PointField::FLOAT32));
233 EXPECT_EQ(recovered.is_dense, true);
234 ASSERT_EQ(recovered.data.size(), 8u);
235}
236
237TEST(cdr_serialization, tfmessage_round_trip) {
239
241 ts.header.stamp.sec = 1;
242 ts.header.frame_id = "world";
243 ts.child_frame_id = "robot";
244 ts.transform.translation.x = 1.0;
245 ts.transform.translation.y = 2.0;
246 ts.transform.translation.z = 0.5;
247 ts.transform.rotation.w = 1.0;
248 original.transforms.push_back(ts);
249
250 auto buf = carla::ros2::serialize_to_cdr(original);
251 ASSERT_FALSE(buf.empty());
252
253 carla::ros2::msg::TFMessage recovered{};
254 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
255 ASSERT_EQ(recovered.transforms.size(), 1u);
256 EXPECT_EQ(recovered.transforms[0].header.stamp.sec, 1);
257 EXPECT_EQ(recovered.transforms[0].header.frame_id, "world");
258 EXPECT_EQ(recovered.transforms[0].child_frame_id, "robot");
259 EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.x, 1.0);
260 EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.y, 2.0);
261 EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.rotation.w, 1.0);
262}
263
264TEST(cdr_serialization, navsat_fix_round_trip) {
266 original.header.frame_id = "gps";
267 original.status.status = static_cast<uint8_t>(carla::ros2::msg::NavSatStatus::STATUS_FIX);
268 original.status.service = static_cast<uint16_t>(carla::ros2::msg::NavSatStatus::SERVICE_GPS);
269 original.latitude = 48.8566;
270 original.longitude = 2.3522;
271 original.altitude = 35.0;
272 original.position_covariance[0] = 0.01;
273 original.position_covariance_type =
275
276 auto buf = carla::ros2::serialize_to_cdr(original);
277 ASSERT_FALSE(buf.empty());
278
279 carla::ros2::msg::NavSatFix recovered{};
280 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
281 EXPECT_EQ(recovered.header.frame_id, "gps");
282 EXPECT_EQ(
283 recovered.status.status,
284 static_cast<uint8_t>(carla::ros2::msg::NavSatStatus::STATUS_FIX));
285 EXPECT_DOUBLE_EQ(recovered.latitude, 48.8566);
286 EXPECT_DOUBLE_EQ(recovered.longitude, 2.3522);
287 EXPECT_DOUBLE_EQ(recovered.altitude, 35.0);
288 EXPECT_DOUBLE_EQ(recovered.position_covariance[0], 0.01);
289 EXPECT_EQ(
290 recovered.position_covariance_type,
292}
293
294TEST(cdr_serialization, carla_ego_vehicle_control_round_trip) {
296 original.throttle = 0.75f;
297 original.steer = -0.5f;
298 original.brake = 0.0f;
299 original.hand_brake = false;
300 original.reverse = true;
301 original.gear = 2;
302 original.manual_gear_shift = false;
303
304 auto buf = carla::ros2::serialize_to_cdr(original);
305 ASSERT_FALSE(buf.empty());
306
308 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
309 EXPECT_FLOAT_EQ(recovered.throttle, 0.75f);
310 EXPECT_FLOAT_EQ(recovered.steer, -0.5f);
311 EXPECT_FLOAT_EQ(recovered.brake, 0.0f);
312 EXPECT_EQ(recovered.hand_brake, false);
313 EXPECT_EQ(recovered.reverse, true);
314 EXPECT_EQ(recovered.gear, 2);
315 EXPECT_EQ(recovered.manual_gear_shift, false);
316}
317
318TEST(cdr_serialization, carla_line_invasion_round_trip) {
320 original.header.frame_id = "vehicle";
321 original.crossed_lane_markings = {1, 4, 7};
322
323 auto buf = carla::ros2::serialize_to_cdr(original);
324 ASSERT_FALSE(buf.empty());
325
327 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
328 EXPECT_EQ(recovered.header.frame_id, "vehicle");
329 ASSERT_EQ(recovered.crossed_lane_markings.size(), 3u);
330 EXPECT_EQ(recovered.crossed_lane_markings[0], 1);
331 EXPECT_EQ(recovered.crossed_lane_markings[1], 4);
332 EXPECT_EQ(recovered.crossed_lane_markings[2], 7);
333}
334
335TEST(cdr_serialization, clock_round_trip) {
336 carla::ros2::msg::Clock original{};
337 original.clock.sec = 999;
338 original.clock.nanosec = 1u;
339
340 auto buf = carla::ros2::serialize_to_cdr(original);
341 ASSERT_FALSE(buf.empty());
342
343 carla::ros2::msg::Clock recovered{};
344 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
345 EXPECT_EQ(recovered.clock.sec, 999);
346 EXPECT_EQ(recovered.clock.nanosec, 1u);
347}
348
349TEST(cdr_serialization, empty_tfmessage_round_trip) {
351
352 auto buf = carla::ros2::serialize_to_cdr(original);
353 ASSERT_FALSE(buf.empty());
354
355 carla::ros2::msg::TFMessage recovered{};
356 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
357 EXPECT_TRUE(recovered.transforms.empty());
358}
359
360TEST(cdr_serialization, pointcloud2_multi_field_round_trip) {
361 // Exercises the manual sequence loop in serialize_cdr/deserialize_cdr for
362 // PointCloud2::fields with more than one element. The single-field
363 // round-trip above does not catch a bug in the loop step.
365 original.header.frame_id = "lidar";
366 original.height = 1u;
367 original.width = 4u;
368
370 fx.name = "x";
371 fx.offset = 0u;
373 fx.count = 1u;
374
376 fy.name = "y";
377 fy.offset = 4u;
379 fy.count = 1u;
380
382 fz.name = "z";
383 fz.offset = 8u;
385 fz.count = 1u;
386
387 original.fields.push_back(fx);
388 original.fields.push_back(fy);
389 original.fields.push_back(fz);
390 original.is_bigendian = false;
391 original.point_step = 12u;
392 original.row_step = 48u;
393 original.data.resize(48u, 0u);
394 original.is_dense = true;
395
396 auto buf = carla::ros2::serialize_to_cdr(original);
397 ASSERT_FALSE(buf.empty());
398
400 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
401 ASSERT_EQ(recovered.fields.size(), 3u);
402 EXPECT_EQ(recovered.fields[0].name, "x");
403 EXPECT_EQ(recovered.fields[0].offset, 0u);
404 EXPECT_EQ(recovered.fields[1].name, "y");
405 EXPECT_EQ(recovered.fields[1].offset, 4u);
406 EXPECT_EQ(recovered.fields[2].name, "z");
407 EXPECT_EQ(recovered.fields[2].offset, 8u);
408 EXPECT_EQ(recovered.point_step, 12u);
409 EXPECT_EQ(recovered.row_step, 48u);
410 ASSERT_EQ(recovered.data.size(), 48u);
411}
412
413TEST(cdr_serialization, tfmessage_multi_transform_round_trip) {
414 // Exercises the manual sequence loop for TFMessage::transforms with more
415 // than one element.
417
419 a.header.stamp.sec = 1;
420 a.header.frame_id = "world";
421 a.child_frame_id = "robot_a";
422 a.transform.translation.x = 1.0;
423 a.transform.rotation.w = 1.0;
424
426 b.header.stamp.sec = 2;
427 b.header.frame_id = "world";
428 b.child_frame_id = "robot_b";
429 b.transform.translation.y = 2.0;
430 b.transform.rotation.w = 1.0;
431
432 original.transforms.push_back(a);
433 original.transforms.push_back(b);
434
435 auto buf = carla::ros2::serialize_to_cdr(original);
436 ASSERT_FALSE(buf.empty());
437
438 carla::ros2::msg::TFMessage recovered{};
439 EXPECT_TRUE(carla::ros2::deserialize_from_cdr(buf.data(), buf.size(), recovered));
440 ASSERT_EQ(recovered.transforms.size(), 2u);
441 EXPECT_EQ(recovered.transforms[0].child_frame_id, "robot_a");
442 EXPECT_DOUBLE_EQ(recovered.transforms[0].transform.translation.x, 1.0);
443 EXPECT_EQ(recovered.transforms[1].child_frame_id, "robot_b");
444 EXPECT_DOUBLE_EQ(recovered.transforms[1].transform.translation.y, 2.0);
445}
446
447TEST(cdr_serialization, deserialize_truncated_returns_false) {
448 // A truncated buffer must produce a clean false return, not an uncaught
449 // Fast-CDR exception leaking out of the bool API.
450 carla::ros2::msg::Header original{};
451 original.stamp.sec = 7;
452 original.frame_id = "needs_more_bytes";
453
454 auto buf = carla::ros2::serialize_to_cdr(original);
455 ASSERT_GT(buf.size(), 8u);
456
457 carla::ros2::msg::Header recovered{};
459 buf.data(), buf.size() / 2u, recovered));
460}
461
462TEST(cdr_serialization, deserialize_corrupt_encapsulation_returns_false) {
463 // A buffer too small to even hold the 4-byte encapsulation header must
464 // produce a clean false return.
465 const uint8_t bogus[2] = {0xFFu, 0xFFu};
466 carla::ros2::msg::Time recovered{};
467 EXPECT_FALSE(carla::ros2::deserialize_from_cdr(bogus, sizeof(bogus), recovered));
468}
469
470TEST(cdr_serialization, deserialize_pointcloud2_hostile_length_returns_false) {
471 // Hand-craft a PointCloud2 buffer that claims its sequence has a hostile
472 // length (max uint32). Without the kMaxCdrSequenceElements cap, the
473 // call would attempt a multi-GB resize and abort the process.
474 std::vector<uint8_t> buf;
475 // Encapsulation header: CDR_LE + options.
476 buf.push_back(0x00u);
477 buf.push_back(0x01u);
478 buf.push_back(0x00u);
479 buf.push_back(0x00u);
480 // Header.stamp.sec (int32) + nanosec (uint32) = 8 bytes of zeros.
481 for (int i = 0; i < 8; ++i) buf.push_back(0x00u);
482 // Header.frame_id (string): length 1 (NUL only) + "\0" + 3 padding bytes
483 // to keep alignment for the next uint32.
484 buf.push_back(0x01u);
485 buf.push_back(0x00u);
486 buf.push_back(0x00u);
487 buf.push_back(0x00u);
488 buf.push_back(0x00u);
489 buf.push_back(0x00u);
490 buf.push_back(0x00u);
491 buf.push_back(0x00u);
492 // PointCloud2.height (uint32) + width (uint32).
493 for (int i = 0; i < 8; ++i) buf.push_back(0x00u);
494 // fields_size = 0xFFFFFFFF (hostile).
495 buf.push_back(0xFFu);
496 buf.push_back(0xFFu);
497 buf.push_back(0xFFu);
498 buf.push_back(0xFFu);
499
502 buf.data(), buf.size(), recovered));
503}
504
505TEST(cdr_serialization, deserialize_tfmessage_hostile_length_returns_false) {
506 // Same idea for TFMessage::transforms — claim a 4-billion-element
507 // sequence and verify the cap rejects it instead of OOM-aborting.
508 std::vector<uint8_t> buf;
509 buf.push_back(0x00u);
510 buf.push_back(0x01u);
511 buf.push_back(0x00u);
512 buf.push_back(0x00u);
513 buf.push_back(0xFFu);
514 buf.push_back(0xFFu);
515 buf.push_back(0xFFu);
516 buf.push_back(0xFFu);
517
518 carla::ros2::msg::TFMessage recovered{};
520 buf.data(), buf.size(), recovered));
521}
522
523TEST(cdr_serialization, cdr_serialized_size_matches_serialize_to_cdr) {
524 // cdr_serialized_size(msg) must return the same byte count as
525 // serialize_to_cdr(msg).size() for every message type. This is the contract
526 // that GenericCdrPubSubType::getSerializedSizeProvider relies on.
527 {
529 msg.stamp.sec = 42;
530 msg.frame_id = "map";
533 }
534 {
536 msg.height = 2u;
537 msg.width = 3u;
538 msg.encoding = "rgb8";
539 msg.data.assign(6u, 0xAAu);
542 }
543 {
545 msg.height = 1u;
546 msg.width = 4u;
547 msg.data.assign(48u, 0xBBu);
550 }
551 {
553 msg.transforms.resize(2u);
554 msg.transforms[0].header.frame_id = "world";
555 msg.transforms[1].header.frame_id = "base_link";
558 }
559}
560
561TEST(cdr_serialization, cdr_serialized_size_image_exceeds_static_max) {
562 // A real 800x600 RGB camera frame is ~1.4 MB. The static
563 // CdrTopicInfo<Image>::max_serialized_size() is only 648 bytes.
564 // cdr_serialized_size() must return a value greater than the static max,
565 // and the round-trip must recover the original data.size().
567 msg.height = 600u;
568 msg.width = 800u;
569 msg.encoding = "rgb8";
570 msg.step = 800u * 3u;
571 const size_t data_bytes = 800u * 600u * 3u; // 1,440,000 bytes
572 msg.data.assign(data_bytes, 0x7Fu);
573
574 const uint32_t computed = carla::ros2::cdr_serialized_size(msg);
575 EXPECT_GT(computed,
576 static_cast<uint32_t>(
578
579 const auto bytes = carla::ros2::serialize_to_cdr(msg);
580 ASSERT_FALSE(bytes.empty());
581 EXPECT_EQ(computed, static_cast<uint32_t>(bytes.size()));
582
583 carla::ros2::msg::Image recovered{};
585 bytes.data(), bytes.size(), recovered));
586 EXPECT_EQ(recovered.data.size(), data_bytes);
587}
588
589TEST(cdr_serialization, cdr_serialized_size_pointcloud2_exceeds_static_max) {
590 // A typical LiDAR scan is 1-20 MB. The static max_serialized_size() for
591 // PointCloud2 is 27597 bytes. This test uses ~1 MB of data to verify the
592 // same contract as the Image test above.
594 msg.height = 1u;
595 msg.width = 22000u;
596 msg.row_step = 22000u * 16u;
597 const size_t data_bytes = 22000u * 16u; // ~352,000 bytes (~0.35 MB)
598 msg.data.assign(data_bytes, 0x3Cu);
600 pf.name = "x";
601 pf.offset = 0u;
602 pf.datatype = 7u; // FLOAT32
603 pf.count = 1u;
604 msg.fields.push_back(pf);
605
606 const uint32_t computed = carla::ros2::cdr_serialized_size(msg);
607 EXPECT_GT(computed,
608 static_cast<uint32_t>(
610
611 const auto bytes = carla::ros2::serialize_to_cdr(msg);
612 ASSERT_FALSE(bytes.empty());
613 EXPECT_EQ(computed, static_cast<uint32_t>(bytes.size()));
614
617 bytes.data(), bytes.size(), recovered));
618 EXPECT_EQ(recovered.data.size(), data_bytes);
619}
620
621// ==========================================================================
622// Middleware abstraction test infrastructure
623// ==========================================================================
624
625using namespace carla::ros2;
626
627namespace {
628
629struct TestMsg {
630 int value{0};
631};
632
633struct TestPubTraits {
634 using msg_type = TestMsg;
635};
636
637struct TestSubTraits {
638 using msg_type = TestMsg;
639};
640
641} // namespace
642
643// -- Factory fixture (resets static state) ------------------------------------
644
645class MiddlewareFactoryFixture : public ::testing::Test {
646 protected:
647 void SetUp() override {
648 MiddlewareFactory::SetMiddleware(Middleware::FastDDS);
649 }
650};
651
652// ==========================================================================
653// Group 3: middleware_to_string (3 tests)
654// ==========================================================================
655
656TEST(middleware_to_string, fastdds_returns_correct_string) {
657 EXPECT_STREQ(MiddlewareToString(Middleware::FastDDS), "FastDDS");
658}
659
660TEST(middleware_to_string, cyclonedds_returns_correct_string) {
661 EXPECT_STREQ(MiddlewareToString(Middleware::CycloneDDS), "CycloneDDS");
662}
663
664TEST(middleware_to_string, zenoh_returns_correct_string) {
665 EXPECT_STREQ(MiddlewareToString(Middleware::Zenoh), "Zenoh");
666}
667
668// ==========================================================================
669// Group 4: middleware_from_string (7 tests)
670// ==========================================================================
671
672TEST(middleware_from_string, fastdds_lowercase_valid) {
673 auto result = MiddlewareFromString("fastdds");
674 EXPECT_TRUE(result.valid);
675 EXPECT_EQ(result.middleware, Middleware::FastDDS);
676}
677
678TEST(middleware_from_string, unknown_string_invalid) {
679 auto result = MiddlewareFromString("unknowndds");
680 EXPECT_FALSE(result.valid);
681}
682
683TEST(middleware_from_string, cyclonedds_lowercase_valid) {
684 auto result = MiddlewareFromString("cyclonedds");
685 EXPECT_TRUE(result.valid);
686 EXPECT_EQ(result.middleware, Middleware::CycloneDDS);
687}
688
689TEST(middleware_from_string, zenoh_lowercase_valid) {
690 auto result = MiddlewareFromString("zenoh");
691 EXPECT_TRUE(result.valid);
692 EXPECT_EQ(result.middleware, Middleware::Zenoh);
693}
694
695TEST(middleware_from_string, empty_string_invalid) {
696 auto result = MiddlewareFromString("");
697 EXPECT_FALSE(result.valid);
698}
699
700TEST(middleware_from_string, uppercase_rejected) {
701 auto result = MiddlewareFromString("FastDDS");
702 EXPECT_FALSE(result.valid);
703}
704
705TEST(middleware_from_string, partial_match_rejected) {
706 auto result = MiddlewareFromString("fast");
707 EXPECT_FALSE(result.valid);
708}
709
710// ==========================================================================
711// Group 5: middleware_available (4 tests)
712// ==========================================================================
713
714TEST(middleware_available, fastdds_available) {
715 EXPECT_TRUE(
716 MiddlewareFactory::IsMiddlewareAvailable(Middleware::FastDDS));
717}
718
719TEST(middleware_available, available_string_contains_fastdds) {
720 std::string available = GetAvailableMiddlewareString();
721 EXPECT_NE(available.find("FastDDS"), std::string::npos);
722}
723
724TEST(middleware_available, cyclonedds_not_available_without_macro) {
725 EXPECT_FALSE(
726 MiddlewareFactory::IsMiddlewareAvailable(Middleware::CycloneDDS));
727}
728
729TEST(middleware_available, zenoh_not_available_without_macro) {
730 EXPECT_FALSE(
732}
733
734// ==========================================================================
735// Group 6: middleware_type_name (4 tests)
736// ==========================================================================
737
738TEST(middleware_type_name, bare_name) {
739 EXPECT_EQ(ToROS2TypeName("Image"), "dds_::Image_");
740}
741
742TEST(middleware_type_name, fully_qualified) {
743 EXPECT_EQ(
744 ToROS2TypeName("sensor_msgs::msg::Image"),
745 "sensor_msgs::msg::dds_::Image_");
746}
747
748TEST(middleware_type_name, single_namespace) {
749 EXPECT_EQ(ToROS2TypeName("msg::Image"), "msg::dds_::Image_");
750}
751
752TEST(middleware_type_name, empty_string) {
753 EXPECT_EQ(ToROS2TypeName(""), "dds_::_");
754}
755
756// ==========================================================================
757// Group 7: MiddlewareFactoryFixture (12 tests)
758// ==========================================================================
759
760TEST_F(MiddlewareFactoryFixture, set_and_get_middleware) {
761 MiddlewareFactory::SetMiddleware(Middleware::FastDDS);
762 EXPECT_EQ(MiddlewareFactory::GetMiddleware(), Middleware::FastDDS);
763}
764
765TEST_F(MiddlewareFactoryFixture, default_is_fastdds) {
766 EXPECT_EQ(MiddlewareFactory::GetMiddleware(), Middleware::FastDDS);
767}
768
769TEST_F(MiddlewareFactoryFixture, resolve_available_middleware) {
770 auto resolution =
771 MiddlewareFactory::ResolveMiddleware(Middleware::FastDDS);
772 EXPECT_TRUE(resolution.success);
773 EXPECT_EQ(resolution.middleware, Middleware::FastDDS);
774}
775
776TEST_F(MiddlewareFactoryFixture, factory_available_string) {
777 std::string available = MiddlewareFactory::GetAvailableMiddlewareString();
778 EXPECT_NE(available.find("FastDDS"), std::string::npos);
779}
780
781TEST_F(MiddlewareFactoryFixture, set_and_get_cyclonedds) {
782 MiddlewareFactory::SetMiddleware(Middleware::CycloneDDS);
783 EXPECT_EQ(MiddlewareFactory::GetMiddleware(), Middleware::CycloneDDS);
784}
785
786TEST_F(MiddlewareFactoryFixture, resolve_unavailable_cyclonedds) {
787 auto resolution =
788 MiddlewareFactory::ResolveMiddleware(Middleware::CycloneDDS);
789 EXPECT_FALSE(resolution.success);
790 EXPECT_EQ(resolution.middleware, Middleware::CycloneDDS);
791}
792
793TEST_F(MiddlewareFactoryFixture, create_publisher_cyclonedds_unavailable) {
794 MiddlewareFactory::SetMiddleware(Middleware::CycloneDDS);
795 ::testing::internal::CaptureStderr();
796 auto pub = MiddlewareFactory::CreatePublisher<TestPubTraits>();
797 ::testing::internal::GetCapturedStderr();
798 EXPECT_EQ(pub, nullptr);
799}
800
801TEST_F(MiddlewareFactoryFixture, create_subscriber_cyclonedds_unavailable) {
802 MiddlewareFactory::SetMiddleware(Middleware::CycloneDDS);
803 ::testing::internal::CaptureStderr();
804 auto sub = MiddlewareFactory::CreateSubscriber<TestSubTraits>();
805 ::testing::internal::GetCapturedStderr();
806 EXPECT_EQ(sub, nullptr);
807}
808
809TEST_F(MiddlewareFactoryFixture, set_and_get_zenoh) {
810 MiddlewareFactory::SetMiddleware(Middleware::Zenoh);
811 EXPECT_EQ(MiddlewareFactory::GetMiddleware(), Middleware::Zenoh);
812}
813
814TEST_F(MiddlewareFactoryFixture, resolve_unavailable_zenoh) {
815 auto resolution =
816 MiddlewareFactory::ResolveMiddleware(Middleware::Zenoh);
817 EXPECT_FALSE(resolution.success);
818 EXPECT_EQ(resolution.middleware, Middleware::Zenoh);
819}
820
821TEST_F(MiddlewareFactoryFixture, create_publisher_zenoh_unavailable) {
822 MiddlewareFactory::SetMiddleware(Middleware::Zenoh);
823 ::testing::internal::CaptureStderr();
824 auto pub = MiddlewareFactory::CreatePublisher<TestPubTraits>();
825 ::testing::internal::GetCapturedStderr();
826 EXPECT_EQ(pub, nullptr);
827}
828
829TEST_F(MiddlewareFactoryFixture, create_subscriber_zenoh_unavailable) {
830 MiddlewareFactory::SetMiddleware(Middleware::Zenoh);
831 ::testing::internal::CaptureStderr();
832 auto sub = MiddlewareFactory::CreateSubscriber<TestSubTraits>();
833 ::testing::internal::GetCapturedStderr();
834 EXPECT_EQ(sub, nullptr);
835}
836
837// ==========================================================================
838// Group 8: generic_cdr_pubsubtype (6 tests)
839// Tests for GenericCdrPubSubType<T> — the single FastDDS TopicDataType
840// implementation that replaces 30 fastddsgen-generated PubSubType classes.
841// ==========================================================================
842
843TEST(generic_cdr_pubsubtype, type_name_matches_cdr_topic_info) {
844 // The name set in the GenericCdrPubSubType constructor must equal
845 // CdrTopicInfo::type_name() — FastDDS uses it for publisher/subscriber matching.
846 EXPECT_STREQ(
849 EXPECT_STREQ(
852 EXPECT_STREQ(
855 EXPECT_STREQ(
858 EXPECT_STREQ(
861}
862
863TEST(generic_cdr_pubsubtype, m_typesize_is_positive) {
864 // FastDDS uses m_typeSize to pre-allocate payload buffers.
865 // It must be > 0 for every type (min: max_serialized_size + 4 encapsulation bytes).
866 EXPECT_GT(GenericCdrPubSubType<msg::Time>().m_typeSize, 0u);
867 EXPECT_GT(GenericCdrPubSubType<msg::Clock>().m_typeSize, 0u);
868 EXPECT_GT(GenericCdrPubSubType<msg::Header>().m_typeSize, 0u);
869 EXPECT_GT(GenericCdrPubSubType<msg::Imu>().m_typeSize, 0u);
870 EXPECT_GT(GenericCdrPubSubType<msg::Image>().m_typeSize, 0u);
871 EXPECT_GT(GenericCdrPubSubType<msg::PointCloud2>().m_typeSize, 0u);
872}
873
874TEST(generic_cdr_pubsubtype, serialize_deserialize_fixed_size_via_payload) {
875 // Round-trip a fixed-size type (Clock) through a SerializedPayload_t buffer.
876 // This exercises the exact code path FastDDS DataWriter/DataReader use.
877 using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t;
878
880 msg::Clock original{};
881 original.clock.sec = 100;
882 original.clock.nanosec = 500u;
883
884 SerializedPayload_t payload(1024u);
885 ASSERT_TRUE(pubsub_type.serialize(static_cast<void*>(&original), &payload));
886 EXPECT_GT(payload.length, 0u);
887
888 msg::Clock recovered{};
889 ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast<void*>(&recovered)));
890 EXPECT_EQ(recovered.clock.sec, 100);
891 EXPECT_EQ(recovered.clock.nanosec, 500u);
892}
893
894TEST(generic_cdr_pubsubtype, serialize_deserialize_string_type_via_payload) {
895 // Round-trip a type containing a std::string (Header) through SerializedPayload_t.
896 using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t;
897
899 msg::Header original{};
900 original.stamp.sec = 42;
901 original.frame_id = "test_frame";
902
903 SerializedPayload_t payload(4096u);
904 ASSERT_TRUE(pubsub_type.serialize(static_cast<void*>(&original), &payload));
905 EXPECT_GT(payload.length, 0u);
906
907 msg::Header recovered{};
908 ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast<void*>(&recovered)));
909 EXPECT_EQ(recovered.stamp.sec, 42);
910 EXPECT_EQ(recovered.frame_id, "test_frame");
911}
912
913TEST(generic_cdr_pubsubtype, create_and_delete_data) {
915
916 void* data = pubsub_type.createData();
917 ASSERT_NE(data, nullptr);
918
919 // Cast to verify it is a properly-constructed Clock
920 msg::Clock* clock = static_cast<msg::Clock*>(data);
921 EXPECT_EQ(clock->clock.sec, 0);
922 EXPECT_EQ(clock->clock.nanosec, 0u);
923
924 // Must not crash
925 pubsub_type.deleteData(data);
926}
927
928TEST(generic_cdr_pubsubtype, getkey_returns_false) {
929 // CARLA has no keyed topics — getKey must always return false.
931 EXPECT_FALSE(pubsub_type.m_isGetKeyDefined);
932 EXPECT_FALSE(pubsub_type.getKey(nullptr, nullptr, false));
933}
934
935// ==========================================================================
936// Group 9: generic_cdr_pubsubtype_large_payload (3 tests)
937// Tests that getSerializedSizeProvider() returns the actual instance size and
938// that serialize/deserialize succeed for payloads exceeding max_serialized_size().
939// These tests catch the runtime bug where Image and PointCloud2 publish failed
940// with RETCODE_ERROR because the static max size (~648 bytes for Image) was far
941// smaller than a real camera frame (~1-8 MB).
942// ==========================================================================
943
944TEST(generic_cdr_pubsubtype_large_payload, image_large_payload_serialize_succeeds) {
945 using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t;
946
947 // 640x480 BGRA image: 1,228,800 bytes of pixel data.
948 // This is vastly larger than CdrTopicInfo<msg::Image>::max_serialized_size() = 648.
949 constexpr uint32_t width = 640u;
950 constexpr uint32_t height = 480u;
951 constexpr uint32_t channels = 4u;
952 const uint32_t pixel_bytes = width * height * channels;
953
954 msg::Image original{};
955 original.header.stamp.sec = 42;
956 original.header.frame_id = "camera";
957 original.height = height;
958 original.width = width;
959 original.encoding = "bgra8";
960 original.is_bigendian = 0u;
961 original.step = width * channels;
962 original.data.assign(pixel_bytes, 0xABu);
963
965
966 // getSerializedSizeProvider() must report the actual instance size, not the
967 // static max. The returned size must accommodate all pixel data.
968 auto size_fn = pubsub_type.getSerializedSizeProvider(
969 static_cast<void*>(&original));
970 uint32_t reported_size = size_fn();
971 EXPECT_GT(reported_size, pixel_bytes)
972 << "getSerializedSizeProvider must exceed the raw pixel count";
973
974 SerializedPayload_t payload(reported_size);
975 ASSERT_TRUE(pubsub_type.serialize(static_cast<void*>(&original), &payload));
976 EXPECT_GT(payload.length, pixel_bytes);
977
978 msg::Image recovered{};
979 ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast<void*>(&recovered)));
980 EXPECT_EQ(recovered.header.frame_id, "camera");
981 EXPECT_EQ(recovered.height, height);
982 EXPECT_EQ(recovered.width, width);
983 EXPECT_EQ(recovered.encoding, "bgra8");
984 ASSERT_EQ(recovered.data.size(), pixel_bytes);
985 EXPECT_EQ(recovered.data[0], 0xABu);
986 EXPECT_EQ(recovered.data[pixel_bytes - 1u], 0xABu);
987}
988
989TEST(generic_cdr_pubsubtype_large_payload, pointcloud2_large_payload_serialize_succeeds) {
990 using SerializedPayload_t = eprosima::fastrtps::rtps::SerializedPayload_t;
991
992 // 10,000 points x 16 bytes/point = 160,000 bytes of point data.
993 // This exceeds CdrTopicInfo<msg::PointCloud2>::max_serialized_size() = 27,597.
994 constexpr uint32_t num_points = 10000u;
995 constexpr uint32_t point_step = 16u;
996 const uint32_t data_bytes = num_points * point_step;
997
998 msg::PointCloud2 original{};
999 original.header.frame_id = "velodyne";
1000 original.height = 1u;
1001 original.width = num_points;
1002 original.point_step = point_step;
1003 original.row_step = num_points * point_step;
1004 original.is_dense = true;
1005 original.data.assign(data_bytes, 0x55u);
1006
1007 msg::PointField field{};
1008 field.name = "x";
1009 field.offset = 0u;
1010 field.datatype = static_cast<uint8_t>(msg::PointField::FLOAT32);
1011 field.count = 1u;
1012 original.fields.push_back(field);
1013
1015
1016 auto size_fn = pubsub_type.getSerializedSizeProvider(
1017 static_cast<void*>(&original));
1018 uint32_t reported_size = size_fn();
1019 EXPECT_GT(reported_size, data_bytes)
1020 << "getSerializedSizeProvider must exceed the raw point data size";
1021
1022 SerializedPayload_t payload(reported_size);
1023 ASSERT_TRUE(pubsub_type.serialize(static_cast<void*>(&original), &payload));
1024 EXPECT_GT(payload.length, data_bytes);
1025
1026 msg::PointCloud2 recovered{};
1027 ASSERT_TRUE(pubsub_type.deserialize(&payload, static_cast<void*>(&recovered)));
1028 EXPECT_EQ(recovered.header.frame_id, "velodyne");
1029 EXPECT_EQ(recovered.height, 1u);
1030 EXPECT_EQ(recovered.width, num_points);
1031 EXPECT_EQ(recovered.is_dense, true);
1032 ASSERT_EQ(recovered.data.size(), data_bytes);
1033 EXPECT_EQ(recovered.data[0], 0x55u);
1034}
1035
1036TEST(generic_cdr_pubsubtype_large_payload, size_provider_returns_actual_size_not_max) {
1037 // getSerializedSizeProvider() must return different sizes for messages with
1038 // different amounts of variable-length data, proving it is instance-dependent.
1039 msg::Image small_img{};
1040 small_img.data.assign(100u, 0u);
1041
1042 msg::Image large_img{};
1043 large_img.data.assign(100000u, 0u);
1044
1046
1047 auto small_fn = pubsub_type.getSerializedSizeProvider(
1048 static_cast<void*>(&small_img));
1049 auto large_fn = pubsub_type.getSerializedSizeProvider(
1050 static_cast<void*>(&large_img));
1051
1052 uint32_t small_size = small_fn();
1053 uint32_t large_size = large_fn();
1054
1055 EXPECT_LT(small_size, large_size)
1056 << "Size must grow with the data vector length";
1057 EXPECT_GT(large_size, 100000u)
1058 << "Size must at least cover the raw data bytes";
1059}
1060
1061// ==========================================================================
1062// PublisherImpl / SubscriberImpl test infrastructure
1063// ==========================================================================
1064// Reuses the TestMsg / TestPubTraits / TestSubTraits helpers defined above for
1065// the middleware abstraction groups.
1066
1067// -- Mock publisher middleware ------------------------------------------------
1068
1070public:
1073 bool alive{true};
1074
1075 bool init_called{false};
1076 bool publish_called{false};
1077 std::string last_topic_name;
1078 void *last_published_data{nullptr};
1079
1080 bool Init(const std::string &topic_name) override {
1081 init_called = true;
1082 last_topic_name = topic_name;
1083 return init_return_value;
1084 }
1085
1086 bool Publish(void *message_data) override {
1087 publish_called = true;
1088 last_published_data = message_data;
1089 return publish_return_value;
1090 }
1091
1092 bool IsAlive() const override { return alive; }
1093 std::string GetTopicName() const override { return last_topic_name; }
1094};
1095
1096// -- Mock subscriber middleware -----------------------------------------------
1097
1099public:
1101 bool alive{true};
1102
1103 bool init_called{false};
1104 std::string last_topic_name;
1105 void *stored_message_ptr{nullptr};
1106 bool *stored_flag_ptr{nullptr};
1107
1108 bool Init(
1109 const std::string &topic_name,
1110 void *message_ptr,
1111 bool *new_message_flag) override {
1112 init_called = true;
1113 last_topic_name = topic_name;
1114 stored_message_ptr = message_ptr;
1115 stored_flag_ptr = new_message_flag;
1116 return init_return_value;
1117 }
1118
1119 bool IsAlive() const override { return alive; }
1120 std::string GetTopicName() const override { return last_topic_name; }
1121};
1122
1123// ==========================================================================
1124// Group 10: publisher_impl (7 tests)
1125// ==========================================================================
1126
1127TEST(publisher_impl, get_message_returns_pointer) {
1129 TestMsg *msg = pub.GetMessage();
1130 ASSERT_NE(msg, nullptr);
1131 msg->value = 7;
1132 EXPECT_EQ(pub.GetMessage()->value, 7);
1133}
1134
1135TEST(publisher_impl, init_delegates_to_middleware) {
1137 auto *mock = new MockPublisherMiddleware();
1138 pub.SetMiddlewareForTesting(
1139 std::unique_ptr<IPublisherMiddleware>(mock));
1140 EXPECT_TRUE(pub.Init("rt/test_topic"));
1141 EXPECT_TRUE(mock->init_called);
1142 EXPECT_EQ(mock->last_topic_name, "rt/test_topic");
1143}
1144
1145TEST(publisher_impl, publish_delegates_to_middleware) {
1147 auto *mock = new MockPublisherMiddleware();
1148 pub.SetMiddlewareForTesting(
1149 std::unique_ptr<IPublisherMiddleware>(mock));
1150 pub.Init("rt/test_topic");
1151 EXPECT_TRUE(pub.Publish());
1152 EXPECT_TRUE(mock->publish_called);
1153 EXPECT_EQ(mock->last_published_data, pub.GetMessage());
1154}
1155
1156TEST(publisher_impl, publish_before_init_fails) {
1158 ::testing::internal::CaptureStderr();
1159 EXPECT_FALSE(pub.Publish());
1160 ::testing::internal::GetCapturedStderr();
1161}
1162
1163TEST(publisher_impl, is_alive_delegates) {
1165 auto *mock = new MockPublisherMiddleware();
1166 mock->alive = true;
1167 pub.SetMiddlewareForTesting(
1168 std::unique_ptr<IPublisherMiddleware>(mock));
1169 pub.Init("rt/test_topic");
1170 EXPECT_TRUE(pub.IsAlive());
1171 mock->alive = false;
1172 EXPECT_FALSE(pub.IsAlive());
1173}
1174
1175TEST(publisher_impl, topic_name_delegates) {
1177 auto *mock = new MockPublisherMiddleware();
1178 pub.SetMiddlewareForTesting(
1179 std::unique_ptr<IPublisherMiddleware>(mock));
1180 pub.Init("rt/camera/image");
1181 EXPECT_EQ(pub.GetTopicName(), "rt/camera/image");
1182}
1183
1184TEST(publisher_impl, data_flows_through_publish) {
1186 auto *mock = new MockPublisherMiddleware();
1187 pub.SetMiddlewareForTesting(
1188 std::unique_ptr<IPublisherMiddleware>(mock));
1189 pub.Init("rt/test_topic");
1190
1191 pub.GetMessage()->value = 42;
1192 pub.Publish();
1193
1194 ASSERT_NE(mock->last_published_data, nullptr);
1195 auto *published = static_cast<TestMsg *>(mock->last_published_data);
1196 EXPECT_EQ(published->value, 42);
1197}
1198
1199// ==========================================================================
1200// Group 11: subscriber_impl (7 tests)
1201// ==========================================================================
1202
1203TEST(subscriber_impl, has_new_message_initially_false) {
1205 EXPECT_FALSE(sub.HasNewMessage());
1206}
1207
1208TEST(subscriber_impl, init_delegates_to_middleware) {
1210 auto *mock = new MockSubscriberMiddleware();
1211 sub.SetMiddlewareForTesting(
1212 std::unique_ptr<ISubscriberMiddleware>(mock));
1213 EXPECT_TRUE(sub.Init("rt/test_topic"));
1214 EXPECT_TRUE(mock->init_called);
1215 EXPECT_EQ(mock->last_topic_name, "rt/test_topic");
1216 EXPECT_NE(mock->stored_message_ptr, nullptr);
1217 EXPECT_NE(mock->stored_flag_ptr, nullptr);
1218}
1219
1220TEST(subscriber_impl, get_message_clears_flag) {
1222 auto *mock = new MockSubscriberMiddleware();
1223 sub.SetMiddlewareForTesting(
1224 std::unique_ptr<ISubscriberMiddleware>(mock));
1225 sub.Init("rt/test_topic");
1226
1227 TestMsg msg;
1228 msg.value = 77;
1229 sub.SimulateMessageReceiptForTesting(msg);
1230 EXPECT_TRUE(sub.HasNewMessage());
1231
1232 TestMsg retrieved = sub.GetMessage();
1233 EXPECT_EQ(retrieved.value, 77);
1234 EXPECT_FALSE(sub.HasNewMessage());
1235}
1236
1237TEST(subscriber_impl, is_alive_delegates) {
1239 auto *mock = new MockSubscriberMiddleware();
1240 mock->alive = true;
1241 sub.SetMiddlewareForTesting(
1242 std::unique_ptr<ISubscriberMiddleware>(mock));
1243 sub.Init("rt/test_topic");
1244 EXPECT_TRUE(sub.IsAlive());
1245 mock->alive = false;
1246 EXPECT_FALSE(sub.IsAlive());
1247}
1248
1249TEST(subscriber_impl, topic_name_delegates) {
1251 auto *mock = new MockSubscriberMiddleware();
1252 sub.SetMiddlewareForTesting(
1253 std::unique_ptr<ISubscriberMiddleware>(mock));
1254 sub.Init("rt/lidar/points");
1255 EXPECT_EQ(sub.GetTopicName(), "rt/lidar/points");
1256}
1257
1258TEST(subscriber_impl, simulate_message_receipt) {
1260 auto *mock = new MockSubscriberMiddleware();
1261 sub.SetMiddlewareForTesting(
1262 std::unique_ptr<ISubscriberMiddleware>(mock));
1263 sub.Init("rt/test_topic");
1264
1265 EXPECT_FALSE(sub.HasNewMessage());
1266 TestMsg msg;
1267 msg.value = 123;
1268 sub.SimulateMessageReceiptForTesting(msg);
1269 EXPECT_TRUE(sub.HasNewMessage());
1270 EXPECT_EQ(sub.GetMessage().value, 123);
1271}
1272
1273TEST(subscriber_impl, init_failure_propagated) {
1275 auto *mock = new MockSubscriberMiddleware();
1276 mock->init_return_value = false;
1277 sub.SetMiddlewareForTesting(
1278 std::unique_ptr<ISubscriberMiddleware>(mock));
1279 EXPECT_FALSE(sub.Init("rt/test_topic"));
1280}
1281
1282// ==========================================================================
1283// Group 12: middleware_config (domain id) (8 tests)
1284// ==========================================================================
1285
1286// Restores the global domain id to the unset sentinel after each test so the
1287// process-wide MiddlewareConfig state does not leak between cases.
1288class MiddlewareConfigFixture : public ::testing::Test {
1289 protected:
1293};
1294
1295TEST_F(MiddlewareConfigFixture, default_domain_id_is_unset) {
1297}
1298
1299TEST_F(MiddlewareConfigFixture, set_and_get_round_trip) {
1301 EXPECT_EQ(MiddlewareConfig::GetDomainId(), 42);
1302}
1303
1304TEST_F(MiddlewareConfigFixture, set_zero_is_stored) {
1306 EXPECT_EQ(MiddlewareConfig::GetDomainId(), 0);
1307}
1308
1309TEST(middleware_config_valid, zero_is_valid) {
1310 EXPECT_TRUE(IsValidDomainId(0));
1311}
1312
1313TEST(middleware_config_valid, one_is_valid) {
1314 EXPECT_TRUE(IsValidDomainId(1));
1315}
1316
1317TEST(middleware_config_valid, max_is_valid) {
1318 EXPECT_TRUE(IsValidDomainId(kMaxDomainId));
1319}
1320
1321TEST(middleware_config_valid, above_max_is_invalid) {
1322 EXPECT_FALSE(IsValidDomainId(kMaxDomainId + 1));
1323}
1324
1325TEST(middleware_config_valid, unset_sentinel_is_invalid) {
1326 EXPECT_FALSE(IsValidDomainId(kUnsetDomainId));
1327}
1328
1329// ==========================================================================
1330// Group 13: domain_id_resolution (16 tests)
1331// Pure tests for TryParseDomainId and ResolveDomainId. The environment value is
1332// passed as a string, so the precedence rules need no real environment.
1333// ==========================================================================
1334
1335TEST(domain_id_parsing, nullptr_is_rejected) {
1336 int out = -99;
1337 EXPECT_FALSE(TryParseDomainId(nullptr, out));
1338}
1339
1340TEST(domain_id_parsing, empty_string_is_rejected) {
1341 int out = -99;
1342 EXPECT_FALSE(TryParseDomainId("", out));
1343}
1344
1345TEST(domain_id_parsing, non_numeric_is_rejected) {
1346 int out = -99;
1347 EXPECT_FALSE(TryParseDomainId("abc", out));
1348}
1349
1350TEST(domain_id_parsing, trailing_garbage_is_rejected) {
1351 int out = -99;
1352 EXPECT_FALSE(TryParseDomainId("12abc", out));
1353}
1354
1355TEST(domain_id_parsing, above_max_is_rejected) {
1356 int out = -99;
1357 EXPECT_FALSE(TryParseDomainId("233", out));
1358}
1359
1360TEST(domain_id_parsing, negative_is_rejected) {
1361 int out = -99;
1362 EXPECT_FALSE(TryParseDomainId("-1", out));
1363}
1364
1365TEST(domain_id_parsing, zero_is_accepted) {
1366 int out = -99;
1367 EXPECT_TRUE(TryParseDomainId("0", out));
1368 EXPECT_EQ(out, 0);
1369}
1370
1371TEST(domain_id_parsing, mid_value_is_accepted) {
1372 int out = -99;
1373 EXPECT_TRUE(TryParseDomainId("42", out));
1374 EXPECT_EQ(out, 42);
1375}
1376
1377TEST(domain_id_parsing, max_value_is_accepted) {
1378 int out = -99;
1379 EXPECT_TRUE(TryParseDomainId("232", out));
1380 EXPECT_EQ(out, kMaxDomainId);
1381}
1382
1383TEST(domain_id_resolution, command_line_wins_over_environment) {
1384 auto resolved = ResolveDomainId(42, "7");
1385 EXPECT_EQ(resolved.id, 42);
1386 EXPECT_EQ(resolved.source, DomainIdSource::CommandLine);
1387}
1388
1389TEST(domain_id_resolution, command_line_zero_wins_over_environment) {
1390 auto resolved = ResolveDomainId(0, "7");
1391 EXPECT_EQ(resolved.id, 0);
1392 EXPECT_EQ(resolved.source, DomainIdSource::CommandLine);
1393}
1394
1395TEST(domain_id_resolution, environment_used_when_command_line_unset) {
1396 auto resolved = ResolveDomainId(kUnsetDomainId, "7");
1397 EXPECT_EQ(resolved.id, 7);
1398 EXPECT_EQ(resolved.source, DomainIdSource::Environment);
1399}
1400
1401TEST(domain_id_resolution, default_when_both_absent) {
1402 auto resolved = ResolveDomainId(kUnsetDomainId, nullptr);
1403 EXPECT_EQ(resolved.id, kDefaultDomainId);
1404 EXPECT_EQ(resolved.source, DomainIdSource::Default);
1405}
1406
1407TEST(domain_id_resolution, default_when_environment_empty) {
1408 auto resolved = ResolveDomainId(kUnsetDomainId, "");
1409 EXPECT_EQ(resolved.id, kDefaultDomainId);
1410 EXPECT_EQ(resolved.source, DomainIdSource::Default);
1411}
1412
1413TEST(domain_id_resolution, invalid_environment_falls_back_to_default) {
1414 auto resolved = ResolveDomainId(kUnsetDomainId, "999");
1415 EXPECT_EQ(resolved.id, kDefaultDomainId);
1416 EXPECT_EQ(resolved.source, DomainIdSource::Default);
1417}
1418
1419TEST(domain_id_resolution, invalid_command_line_falls_through_to_environment) {
1420 auto resolved = ResolveDomainId(500, "7");
1421 EXPECT_EQ(resolved.id, 7);
1422 EXPECT_EQ(resolved.source, DomainIdSource::Environment);
1423}
1424
1425// ==========================================================================
1426// Group 14: zenoh_wire_format (3 tests)
1427// Pure string tests for the rmw_zenoh keyexpr builders: the domain id must be
1428// the leading segment of every keyexpr so we only match rmw_zenoh peers on
1429// the same domain.
1430// ==========================================================================
1431
1432TEST(zenoh_wire_format, topic_keyexpr_starts_with_domain_segment) {
1433 const std::string ke = zenoh_make_topic_keyexpr(
1434 "42", "vehicle/imu", "sensor_msgs::msg::dds_::Imu_", "RIHS01_abc");
1435 EXPECT_EQ(ke.compare(0, 3, "42/"), 0) << "keyexpr: " << ke;
1436 EXPECT_NE(ke.find("/vehicle/imu/"), std::string::npos);
1437}
1438
1439TEST(zenoh_wire_format, node_liveliness_keyexpr_starts_with_domain_segment) {
1440 const std::string ke = zenoh_make_node_liveliness_keyexpr("42", "deadbeef");
1441 EXPECT_EQ(ke.compare(0, 12, "@ros2_lv/42/"), 0) << "keyexpr: " << ke;
1442}
1443
1444TEST(zenoh_wire_format, topic_liveliness_keyexpr_has_domain_and_mangled_topic) {
1445 const std::string ke = zenoh_make_topic_liveliness_keyexpr(
1446 "42", "deadbeef", 7u, kZenohEntityKindPub, "vehicle/imu",
1447 "sensor_msgs::msg::dds_::Imu_", "RIHS01_abc", kZenohDefaultQos);
1448 EXPECT_EQ(ke.compare(0, 12, "@ros2_lv/42/"), 0) << "keyexpr: " << ke;
1449 EXPECT_NE(ke.find("%vehicle%imu"), std::string::npos);
1450}
1451
1452#ifndef _WIN32
1453// ==========================================================================
1454// Group 15: effective_domain_id_env (3 tests)
1455// Exercises the thin std::getenv wrapper in MiddlewareConfig. POSIX-only:
1456// setenv/unsetenv are not available on Windows, and Windows does not build the
1457// ROS 2 middleware anyway.
1458// ==========================================================================
1459
1460// Saves and restores ROS_DOMAIN_ID and the configured domain id so the
1461// process-wide state does not leak between tests.
1462class RosDomainIdEnvFixture : public ::testing::Test {
1463 protected:
1464 void SetUp() override {
1465 const char* existing = std::getenv(kRosDomainIdEnvVar);
1466 had_env_ = existing != nullptr;
1467 if (had_env_) {
1468 saved_ = existing;
1469 }
1471 }
1472
1473 void TearDown() override {
1475 if (had_env_) {
1476 ::setenv(kRosDomainIdEnvVar, saved_.c_str(), 1);
1477 } else {
1478 ::unsetenv(kRosDomainIdEnvVar);
1479 }
1480 }
1481
1482 bool had_env_{false};
1483 std::string saved_;
1484};
1485
1486TEST_F(RosDomainIdEnvFixture, environment_applies_when_command_line_unset) {
1487 ::setenv(kRosDomainIdEnvVar, "13", 1);
1489}
1490
1491TEST_F(RosDomainIdEnvFixture, command_line_overrides_environment) {
1492 ::setenv(kRosDomainIdEnvVar, "13", 1);
1495}
1496
1497TEST_F(RosDomainIdEnvFixture, default_when_environment_unset) {
1498 ::unsetenv(kRosDomainIdEnvVar);
1500}
1501
1502// ==========================================================================
1503// Group 16: zenoh_domain_id (5 tests)
1504// zenoh_ros_domain_id() feeds the leading keyexpr segment and must honor the
1505// MiddlewareConfig resolution order (--ros-domain-id, then ROS_DOMAIN_ID,
1506// then the default domain 0), like the DDS middlewares do for their
1507// DomainParticipant.
1508// ==========================================================================
1509
1510// Same save/restore needs as the effective_domain_id_env group.
1512
1513TEST_F(ZenohDomainIdFixture, command_line_value_is_used) {
1514 ::unsetenv(kRosDomainIdEnvVar);
1516 EXPECT_EQ(zenoh_ros_domain_id(), "42");
1517}
1518
1519TEST_F(ZenohDomainIdFixture, environment_used_when_command_line_unset) {
1520 ::setenv(kRosDomainIdEnvVar, "13", 1);
1521 EXPECT_EQ(zenoh_ros_domain_id(), "13");
1522}
1523
1524TEST_F(ZenohDomainIdFixture, command_line_overrides_environment) {
1525 ::setenv(kRosDomainIdEnvVar, "13", 1);
1527 EXPECT_EQ(zenoh_ros_domain_id(), "99");
1528}
1529
1530TEST_F(ZenohDomainIdFixture, default_is_zero) {
1531 ::unsetenv(kRosDomainIdEnvVar);
1532 EXPECT_EQ(zenoh_ros_domain_id(), "0");
1533}
1534
1535TEST_F(ZenohDomainIdFixture, invalid_environment_falls_back_to_default) {
1536 ::setenv(kRosDomainIdEnvVar, "abc", 1);
1537 EXPECT_EQ(zenoh_ros_domain_id(), "0");
1538}
1539#endif // _WIN32
1540
1541// ==========================================================================
1542// Group 17: cyclonedds_fragment_gather (6 tests)
1543// carla_cdr_gather_fragments() reassembles the fragchain CycloneDDS hands to
1544// from_ser into the contiguous CDR buffer stored in carla_cdr_serdata. Peers
1545// such as an Autoware stack on rmw_cyclonedds deliver samples larger than one
1546// fragment through this path, so the walk must handle multi-fragment chains,
1547// overlapping retransmits, and truncated chains — all pure logic, no
1548// participant needed.
1549// ==========================================================================
1550
1551namespace {
1552
1553struct FakeFrag {
1554 const FakeFrag* nextfrag{nullptr};
1555 uint32_t min{0u};
1556 uint32_t maxp1{0u};
1557 const uint8_t* payload{nullptr}; ///< bytes for [min, maxp1)
1558};
1559
1560const uint8_t* FakeFragPayload(const FakeFrag* f) { return f->payload; }
1561
1562std::vector<uint8_t> MakePatternBytes(size_t size) {
1563 std::vector<uint8_t> bytes(size);
1564 for (size_t i = 0u; i < size; ++i) {
1565 bytes[i] = static_cast<uint8_t>((i * 31u + 7u) & 0xFFu);
1566 }
1567 return bytes;
1568}
1569
1570/// Split [0, bytes.size()) into a nextfrag-linked chain of fragments of at
1571/// most frag_size bytes each, all viewing the same backing buffer.
1572std::vector<FakeFrag> MakeFragChain(
1573 const std::vector<uint8_t>& bytes,
1574 size_t frag_size)
1575{
1576 std::vector<FakeFrag> frags;
1577 for (size_t off = 0u; off < bytes.size(); off += frag_size) {
1578 const size_t end = std::min(bytes.size(), off + frag_size);
1579 FakeFrag f;
1580 f.min = static_cast<uint32_t>(off);
1581 f.maxp1 = static_cast<uint32_t>(end);
1582 f.payload = bytes.data() + off;
1583 frags.push_back(f);
1584 }
1585 for (size_t i = 1u; i < frags.size(); ++i) {
1586 frags[i - 1u].nextfrag = &frags[i];
1587 }
1588 return frags;
1589}
1590
1591} // namespace
1592
1593TEST(cyclonedds_fragment_gather, single_fragment_copies_all) {
1594 const auto bytes = MakePatternBytes(256u);
1595 const auto frags = MakeFragChain(bytes, bytes.size());
1596 ASSERT_EQ(frags.size(), 1u);
1597
1598 std::vector<uint8_t> dest(bytes.size(), 0u);
1599 const size_t gathered = carla_cdr_gather_fragments(
1600 dest.data(), dest.size(), frags.data(), FakeFragPayload);
1601
1602 EXPECT_EQ(gathered, bytes.size());
1603 EXPECT_EQ(dest, bytes);
1604}
1605
1606TEST(cyclonedds_fragment_gather, multi_fragment_reassembles_in_order) {
1607 const auto bytes = MakePatternBytes(5000u);
1608 const auto frags = MakeFragChain(bytes, 1400u);
1609 ASSERT_GT(frags.size(), 3u);
1610
1611 std::vector<uint8_t> dest(bytes.size(), 0u);
1612 const size_t gathered = carla_cdr_gather_fragments(
1613 dest.data(), dest.size(), frags.data(), FakeFragPayload);
1614
1615 EXPECT_EQ(gathered, bytes.size());
1616 EXPECT_EQ(dest, bytes);
1617}
1618
1619TEST(cyclonedds_fragment_gather, overlapping_fragments_gather_once) {
1620 const auto bytes = MakePatternBytes(1200u);
1621
1622 // Retransmit-style overlap: consecutive fragments re-cover earlier bytes.
1623 FakeFrag f2{nullptr, 800u, 1200u, bytes.data() + 800u};
1624 FakeFrag f1{&f2, 400u, 1000u, bytes.data() + 400u};
1625 FakeFrag f0{&f1, 0u, 600u, bytes.data()};
1626
1627 std::vector<uint8_t> dest(bytes.size(), 0u);
1628 const size_t gathered = carla_cdr_gather_fragments(
1629 dest.data(), dest.size(), &f0, FakeFragPayload);
1630
1631 EXPECT_EQ(gathered, bytes.size());
1632 EXPECT_EQ(dest, bytes);
1633}
1634
1635TEST(cyclonedds_fragment_gather, truncated_chain_reports_short_count) {
1636 const auto bytes = MakePatternBytes(1200u);
1637
1638 // Gap between the fragments: [0, 500) then [900, 1200).
1639 FakeFrag f1{nullptr, 900u, 1200u, bytes.data() + 900u};
1640 FakeFrag f0{&f1, 0u, 500u, bytes.data()};
1641
1642 std::vector<uint8_t> dest(bytes.size(), 0u);
1643 const size_t gathered = carla_cdr_gather_fragments(
1644 dest.data(), dest.size(), &f0, FakeFragPayload);
1645
1646 EXPECT_EQ(gathered, 500u);
1647}
1648
1649TEST(cyclonedds_fragment_gather, oversized_fragment_is_clamped_to_size) {
1650 const auto bytes = MakePatternBytes(128u);
1651
1652 FakeFrag f0{nullptr, 0u, 128u, bytes.data()};
1653
1654 std::vector<uint8_t> dest(100u, 0u);
1655 const size_t gathered = carla_cdr_gather_fragments(
1656 dest.data(), dest.size(), &f0, FakeFragPayload);
1657
1658 EXPECT_EQ(gathered, 100u);
1659 EXPECT_TRUE(std::equal(dest.begin(), dest.end(), bytes.begin()));
1660}
1661
1662TEST(cyclonedds_fragment_gather, large_pointcloud_round_trips_through_fragments) {
1663 // >64KB sample: 5,000 points x 16 bytes/point = 80,000 bytes of point data,
1664 // fragmented at a UDP-like 1,400 bytes — the shape of the Autoware-on-
1665 // CycloneDDS traffic that hits from_ser instead of from_ser_iov.
1666 constexpr uint32_t num_points = 5000u;
1667 constexpr uint32_t point_step = 16u;
1668 const uint32_t data_bytes = num_points * point_step;
1669
1670 msg::PointCloud2 original{};
1671 original.header.frame_id = "velodyne";
1672 original.height = 1u;
1673 original.width = num_points;
1674 original.point_step = point_step;
1675 original.row_step = num_points * point_step;
1676 original.is_dense = true;
1677 original.data.assign(data_bytes, 0u);
1678 for (uint32_t i = 0u; i < data_bytes; ++i) {
1679 original.data[i] = static_cast<uint8_t>((i * 13u + 5u) & 0xFFu);
1680 }
1681
1682 const std::vector<uint8_t> wire = serialize_to_cdr(original);
1683 ASSERT_GT(wire.size(), 65536u) << "sample must exceed one 64KB fragment unit";
1684
1685 const auto frags = MakeFragChain(wire, 1400u);
1686 ASSERT_GT(frags.size(), 40u);
1687
1688 std::vector<uint8_t> dest(wire.size(), 0u);
1689 const size_t gathered = carla_cdr_gather_fragments(
1690 dest.data(), dest.size(), frags.data(), FakeFragPayload);
1691 ASSERT_EQ(gathered, wire.size());
1692
1693 msg::PointCloud2 recovered{};
1694 ASSERT_TRUE(deserialize_from_cdr(dest.data(), dest.size(), recovered));
1695 EXPECT_EQ(recovered.header.frame_id, "velodyne");
1696 EXPECT_EQ(recovered.width, num_points);
1697 ASSERT_EQ(recovered.data.size(), data_bytes);
1698 EXPECT_EQ(recovered.data, original.data);
1699}
double min(double v1, double v2)
Definition Simplify.h:285
bool Publish(void *message_data) override
Serialize and write a message to the network.
std::string GetTopicName() const override
bool Init(const std::string &topic_name) override
Initialize the underlying middleware entities.
bool Init(const std::string &topic_name, void *message_ptr, bool *new_message_flag) override
Initialize the underlying middleware entities.
std::string GetTopicName() const override
Generic FastDDS TopicDataType that serializes carla::ros2::msg::* POD structs directly to CDR via Cdr...
std::function< uint32_t()> getSerializedSizeProvider(void *data) override
Return a function that gives the actual CDR-serialized size for this specific message instance.
bool deserialize(SerializedPayload_t *payload, void *data) override
Deserialize a FastDDS payload buffer into a MsgType instance.
bool serialize(void *data, SerializedPayload_t *payload) override
Serialize a MsgType instance into the FastDDS payload buffer.
void deleteData(void *data) override
Delete a MsgType previously returned by createData().
void * createData() override
Allocate a new default-initialized MsgType on the heap.
bool getKey(void *, eprosima::fastrtps::rtps::InstanceHandle_t *, bool) override
CARLA topics are not keyed — always return false.
Type-erased abstract interface for a publisher middleware.
Type-erased abstract interface for a subscriber middleware.
static void SetDomainId(int id)
Configure the ROS 2 domain id for all subsequent transport contexts.
static void SetMiddleware(Middleware middleware)
Select the middleware for all subsequent publisher/subscriber creation.
static std::string GetAvailableMiddlewareString()
Return a readable list of middleware implementations compiled into this binary.
static MiddlewareResolution ResolveMiddleware(Middleware requested)
Resolve the requested middleware strictly — no fallback to other middleware.
static bool IsMiddlewareAvailable(Middleware middleware)
Check whether a specific middleware was compiled into this binary.
bool Init(std::string topic_name)
bool Init(std::string topic_name)
bool IsValidDomainId(int id)
constexpr int kDefaultDomainId
Domain id used when neither the command line nor the environment provides one.
const char * MiddlewareToString(Middleware middleware)
Convert a Middleware enum value to a readable string.
Definition Middleware.h:22
std::vector< uint8_t > serialize_to_cdr(const T &msg)
Serialize a msg::X to a CDR byte buffer including the DDS encapsulation header (Classic CDR,...
constexpr int kMaxDomainId
Highest valid ROS 2 domain id (RTPS maximum).
ResolvedDomainId ResolveDomainId(int cli_value, const char *env_value)
Resolve the effective domain id from a command line value and an environment value,...
constexpr int kUnsetDomainId
Sentinel domain id meaning "no domain id was configured on the command line"; each middleware falls b...
std::string GetAvailableMiddlewareString()
Return a readable list of middleware implementations compiled into this binary.
Definition Middleware.h:56
uint32_t cdr_serialized_size(const T &msg)
Return the exact CDR-serialized size in bytes for a message instance, including the 4-byte DDS encaps...
bool TryParseDomainId(const char *text, int &out)
Parse a ROS_DOMAIN_ID-style string into a valid domain id.
bool deserialize_from_cdr(const uint8_t *data, size_t size, T &msg)
Deserialize a msg::X from a CDR byte buffer that was produced by serialize_to_cdr() or by any ROS2-co...
std::string ToROS2TypeName(const std::string &type_name)
Mangle a type name into the ROS2-compatible format.
Definition Middleware.h:82
constexpr char kRosDomainIdEnvVar[]
Name of the standard ROS 2 environment variable that selects the domain id.
MiddlewareParseResult MiddlewareFromString(const std::string &name)
Parse a middleware name string (lowercase).
Definition Middleware.h:42
Per-type metadata needed by the middleware layer.
std::string frame_id
Definition Header.h:15
std::vector< uint8_t > data
static const uint8_t COVARIANCE_TYPE_DIAGONAL_KNOWN
Definition NavSatFix.h:18
static const uint8_t STATUS_FIX
static const uint16_t SERVICE_GPS
static const uint8_t FLOAT32
Definition PointField.h:20
std::vector< TransformStamped > transforms
Definition TFMessage.h:14
TEST(cdr_topic_info, type_names_are_non_empty)
TEST_F(MiddlewareFactoryFixture, set_and_get_middleware)