Skip to content

Commit 5108030

Browse files
committed
Add reproducability test to the corruption issue
1 parent 52f647b commit 5108030

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

joint_state_broadcaster/test/test_joint_state_broadcaster.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
#include <cstddef>
1616

17+
#include <atomic>
1718
#include <functional>
1819
#include <memory>
20+
#include <shared_mutex>
1921
#include <string>
22+
#include <thread>
2023
#include <utility>
2124
#include <vector>
2225

@@ -1389,3 +1392,89 @@ TEST_F(JointStateBroadcasterTest, NoThrowWithBooleanAndDoubleInterfaceTest)
13891392
ASSERT_THAT(state_broadcaster_->joint_state_msg_.velocity, SizeIs(1));
13901393
ASSERT_THAT(state_broadcaster_->joint_state_msg_.effort, SizeIs(1));
13911394
}
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+
}

joint_state_broadcaster/test/test_joint_state_broadcaster.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class FriendJointStateBroadcaster : public joint_state_broadcaster::JointStateBr
5252
FRIEND_TEST(JointStateBroadcasterTest, ExtraJointStatePublishTest);
5353
FRIEND_TEST(JointStateBroadcasterTest, NoThrowWithBooleanInterfaceTest);
5454
FRIEND_TEST(JointStateBroadcasterTest, NoThrowWithBooleanAndDoubleInterfaceTest);
55+
FRIEND_TEST(JointStateBroadcasterTest, CorrectMappingWhenInterfaceReadFailsTest);
5556
};
5657

5758
class JointStateBroadcasterTest : public ::testing::Test

0 commit comments

Comments
 (0)