|
14 | 14 |
|
15 | 15 | #include <cstddef> |
16 | 16 |
|
| 17 | +#include <atomic> |
17 | 18 | #include <functional> |
18 | 19 | #include <memory> |
| 20 | +#include <shared_mutex> |
19 | 21 | #include <string> |
| 22 | +#include <thread> |
20 | 23 | #include <utility> |
21 | 24 | #include <vector> |
22 | 25 |
|
@@ -1389,3 +1392,89 @@ TEST_F(JointStateBroadcasterTest, NoThrowWithBooleanAndDoubleInterfaceTest) |
1389 | 1392 | ASSERT_THAT(state_broadcaster_->joint_state_msg_.velocity, SizeIs(1)); |
1390 | 1393 | ASSERT_THAT(state_broadcaster_->joint_state_msg_.effort, SizeIs(1)); |
1391 | 1394 | } |
| 1395 | + |
| 1396 | +// Regression test: when a double interface temporarily fails to be read |
| 1397 | +// (get_optional returns nullopt as it is unable to lock), map_index must still advance so that |
| 1398 | +// subsequent interfaces are written to the correct mapped_values_ indexes. |
| 1399 | +// |
| 1400 | +// Without the fix, if state_interfaces_[i] returns nullopt, the next interface's value is |
| 1401 | +// written into index i instead of index i+1, corrupting all subsequent joint state values. |
| 1402 | +TEST_F(JointStateBroadcasterTest, CorrectMappingWhenInterfaceReadFailsTest) |
| 1403 | +{ |
| 1404 | + const std::string JOINT_NAMES[] = {"joint1", "joint2", "joint3"}; |
| 1405 | + const double INIT_POS[] = {1.1, 2.2, 3.3}; |
| 1406 | + |
| 1407 | + auto j1 = std::make_shared<hardware_interface::StateInterface>( |
| 1408 | + JOINT_NAMES[0], HW_IF_POSITION, "double", std::to_string(INIT_POS[0])); |
| 1409 | + auto j2 = std::make_shared<hardware_interface::StateInterface>( |
| 1410 | + JOINT_NAMES[1], HW_IF_POSITION, "double", std::to_string(INIT_POS[1])); |
| 1411 | + auto j3 = std::make_shared<hardware_interface::StateInterface>( |
| 1412 | + JOINT_NAMES[2], HW_IF_POSITION, "double", std::to_string(INIT_POS[2])); |
| 1413 | + |
| 1414 | + init_broadcaster_and_set_parameters( |
| 1415 | + "", {JOINT_NAMES[0], JOINT_NAMES[1], JOINT_NAMES[2]}, {HW_IF_POSITION}); |
| 1416 | + |
| 1417 | + std::vector<LoanedStateInterface> state_ifs; |
| 1418 | + state_ifs.emplace_back(j1); |
| 1419 | + state_ifs.emplace_back(j2); |
| 1420 | + state_ifs.emplace_back(j3); |
| 1421 | + state_broadcaster_->assign_interfaces({}, std::move(state_ifs)); |
| 1422 | + |
| 1423 | + ASSERT_EQ(state_broadcaster_->on_configure(rclcpp_lifecycle::State()), NODE_SUCCESS); |
| 1424 | + ASSERT_EQ(state_broadcaster_->on_activate(rclcpp_lifecycle::State()), NODE_SUCCESS); |
| 1425 | + |
| 1426 | + ASSERT_THAT( |
| 1427 | + state_broadcaster_->joint_state_msg_.name, |
| 1428 | + ElementsAreArray({JOINT_NAMES[0], JOINT_NAMES[1], JOINT_NAMES[2]})); |
| 1429 | + |
| 1430 | + /// Simulate a temporarily-unavailable first interface |
| 1431 | + // Hold an exclusive lock on state_interfaces_[0]'s mutex from a helper thread. |
| 1432 | + // While the lock is held, get_optional(0) on state_interfaces_[0] cannot acquire the |
| 1433 | + // shared lock and returns nullopt. |
| 1434 | + std::atomic<bool> lock_held{false}; |
| 1435 | + std::atomic<bool> release_lock{false}; |
| 1436 | + |
| 1437 | + std::thread locker( |
| 1438 | + [&]() |
| 1439 | + { |
| 1440 | + // Acquire exclusive lock on the first interface's handle mutex |
| 1441 | + std::unique_lock<std::shared_mutex> lk(j1->get_mutex()); |
| 1442 | + lock_held.store(true, std::memory_order_release); |
| 1443 | + // Hold it until the main thread finishes its update() call |
| 1444 | + while (!release_lock.load(std::memory_order_acquire)) |
| 1445 | + { |
| 1446 | + std::this_thread::yield(); |
| 1447 | + } |
| 1448 | + }); |
| 1449 | + |
| 1450 | + // Wait until the locker thread actually owns the mutex |
| 1451 | + while (!lock_held.load(std::memory_order_acquire)) |
| 1452 | + { |
| 1453 | + std::this_thread::yield(); |
| 1454 | + } |
| 1455 | + |
| 1456 | + // Call update(): joint1/position read will return nullopt (lock held by locker thread). |
| 1457 | + ASSERT_NO_THROW( |
| 1458 | + state_broadcaster_->update(rclcpp::Time(0), rclcpp::Duration::from_seconds(0.01))); |
| 1459 | + |
| 1460 | + release_lock.store(true, std::memory_order_release); |
| 1461 | + locker.join(); |
| 1462 | + |
| 1463 | + const auto & names = state_broadcaster_->joint_state_msg_.name; |
| 1464 | + const auto & pos = state_broadcaster_->joint_state_msg_.position; |
| 1465 | + ASSERT_EQ(pos.size(), 3u); |
| 1466 | + |
| 1467 | + // joint1 was not readable, its index must NOT contain joint2's value (2.2). |
| 1468 | + // (It will hold the initial NaN or any prior value, not 2.2.) |
| 1469 | + EXPECT_EQ(names[0], JOINT_NAMES[0]); |
| 1470 | + EXPECT_NE(pos[0], INIT_POS[1]) |
| 1471 | + << "joint1's position slot was overwritten with joint2's value — map_index bug is present"; |
| 1472 | + |
| 1473 | + EXPECT_EQ(names[1], JOINT_NAMES[1]); |
| 1474 | + EXPECT_DOUBLE_EQ(pos[1], INIT_POS[1]) |
| 1475 | + << "joint2's position slot has wrong value — map_index was shifted by the nullopt"; |
| 1476 | + |
| 1477 | + EXPECT_EQ(names[2], JOINT_NAMES[2]); |
| 1478 | + EXPECT_DOUBLE_EQ(pos[2], INIT_POS[2]) |
| 1479 | + << "joint3's position slot has wrong value — map_index was shifted by the nullopt"; |
| 1480 | +} |
0 commit comments