Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ros2_medkit_gateway/src/discovery/runtime_discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,16 @@ std::set<std::string> RuntimeDiscoveryStrategy::get_node_namespaces() {
auto names_and_namespaces = node_graph->get_node_names_and_namespaces();

for (const auto & name_and_ns : names_and_namespaces) {
std::string node_name = name_and_ns.first;
std::string ns = name_and_ns.second;
std::string area = extract_area_from_namespace(ns);
if (area != "root") {
namespaces.insert(area);
} else {
// For root namespace nodes, add the node name to prevent
// topic-based discovery from creating duplicate components
// e.g., node /fault_manager publishes /fault_manager/events
namespaces.insert(node_name);
}
}

Expand Down
64 changes: 64 additions & 0 deletions src/ros2_medkit_gateway/test/test_discovery_heuristic_apps.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ def generate_test_description():
output='screen',
additional_env=coverage_env,
),
# Node in root namespace to test duplicate component prevention
# This node publishes /root_ns_demo/temperature which could incorrectly
# create a duplicate topic-based component
launch_ros.actions.Node(
package='ros2_medkit_gateway',
executable='demo_engine_temp_sensor',
name='root_ns_demo',
namespace='/',
output='screen',
additional_env=coverage_env,
),
]

return LaunchDescription(
Expand Down Expand Up @@ -240,6 +251,59 @@ def test_areas_created_from_namespaces(self):
self.assertIn('powertrain', area_ids, f"Missing 'powertrain' area, found: {area_ids}")
self.assertIn('chassis', area_ids, f"Missing 'chassis' area, found: {area_ids}")

def test_no_duplicate_component_ids(self):
"""
Test that component IDs are unique.

Root namespace nodes publishing topics with matching prefix should
not create duplicate topic-based components. The 'root_ns_demo' node
in root namespace publishes /root_ns_demo/temperature - this should
NOT create a separate topic-based component.
"""
data = self._get_json('/components')
self.assertIn('items', data)

component_ids = [c['id'] for c in data['items']]

# Count occurrences of each component ID
id_counts = {}
for comp_id in component_ids:
id_counts[comp_id] = id_counts.get(comp_id, 0) + 1

# Assert all counts are exactly 1 (no duplicates)
duplicates = {cid: count for cid, count in id_counts.items() if count > 1}
self.assertEqual(
len(duplicates), 0,
f'Found duplicate component IDs: {duplicates}'
)

def test_root_namespace_node_exists_as_app_not_component(self):
"""
Test that root namespace node is an app, not a duplicate component.

The 'root_ns_demo' node is in root namespace (/) and publishes topics
with prefix /root_ns_demo/. Without the fix, this would create both:
- A node-based app for the root area (correct)
- A topic-based component named 'root_ns_demo' (WRONG - duplicate)

Expected: root_ns_demo exists as an app, NOT as a standalone component.
"""
# Verify root_ns_demo is NOT a standalone component
components = self._get_json('/components').get('items', [])
component_ids = [c.get('id') for c in components]
self.assertNotIn(
'root_ns_demo', component_ids,
"'root_ns_demo' should not exist as a component - it should be an app"
)

# Verify root_ns_demo IS an app
apps = self._get_json('/apps').get('items', [])
app_ids = [a.get('id') for a in apps]
self.assertIn(
'root_ns_demo', app_ids,
"'root_ns_demo' should exist as an app"
)


class TestTopicOnlyPolicy(unittest.TestCase):
"""
Expand Down
19 changes: 19 additions & 0 deletions src/ros2_medkit_gateway/test/test_discovery_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,22 @@ TEST_F(DiscoveryManagerWithPublishersTest, DiscoverTopicComponents_DoesNotDuplic
EXPECT_FALSE(comp.id.empty());
}
}

TEST_F(DiscoveryManagerWithPublishersTest, DiscoverTopicComponents_DoesNotDuplicateRootNamespaceNodeTopics) {
// Root namespace nodes publishing topics with matching prefix
// should not create duplicate topic-based components.
//
// Create a publisher with topic prefix matching the test node's name.
// The test node is named "test_discovery_node" and in root namespace.
auto pub = node_->create_publisher<std_msgs::msg::String>("/test_discovery_node/status", 10);

rclcpp::spin_some(node_);

auto topic_components = discovery_manager_->discover_topic_components();

// Should NOT create a topic-based component for "test_discovery_node"
// because there's already a node with that name in root namespace
for (const auto & comp : topic_components) {
EXPECT_NE(comp.id, "test_discovery_node") << "Should not create duplicate component for root namespace node";
}
}