-
Notifications
You must be signed in to change notification settings - Fork 51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sloretz/test discovery #512
base: rolling
Are you sure you want to change the base?
Changes from 6 commits
a6eb611
01ac3c5
7b659b0
8b3c560
2153480
3ad7bf5
dec1548
c1ec828
4df83da
371ec23
1d0cd6a
710592a
1c0f82d
24f5193
6cbc122
8a063c7
06a2ce8
2e970ec
2143918
f10b1fb
30eb7e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
cmake_minimum_required(VERSION 3.5) | ||
project(test_discovery) | ||
|
||
find_package(ament_cmake_ros REQUIRED) | ||
find_package(rclcpp REQUIRED) | ||
find_package(test_msgs REQUIRED) | ||
|
||
add_executable(publish_once src/publish_once.cpp) | ||
target_compile_features(publish_once PUBLIC cxx_std_17) | ||
target_link_libraries(publish_once PRIVATE | ||
rclcpp::rclcpp | ||
${test_msgs_TARGETS} | ||
) | ||
|
||
add_executable(subscribe_once src/subscribe_once.cpp) | ||
target_compile_features(subscribe_once PUBLIC cxx_std_17) | ||
target_link_libraries(subscribe_once PRIVATE | ||
rclcpp::rclcpp | ||
${test_msgs_TARGETS} | ||
) | ||
|
||
install(TARGETS publish_once subscribe_once | ||
DESTINATION lib/${PROJECT_NAME}) | ||
|
||
install(FILES | ||
roottests/test_discovery.py | ||
roottests/conftest.py | ||
DESTINATION share/${PROJECT_NAME}/roottests/) | ||
|
||
install(PROGRAMS | ||
scripts/run_root_tests.py | ||
DESTINATION lib/${PROJECT_NAME}/) | ||
|
||
if(BUILD_TESTING) | ||
find_package(ament_lint_auto REQUIRED) | ||
ament_lint_auto_find_test_dependencies() | ||
|
||
ament_add_pytest_test(test_discovery tests/test_discovery.py TIMEOUT 800) | ||
endif() | ||
|
||
ament_package() |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,30 @@ | ||||||
<?xml version="1.0"?> | ||||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> | ||||||
<package format="2"> | ||||||
<name>test_discovery</name> | ||||||
<version>0.14.0</version> | ||||||
<description> | ||||||
Test discovery behaviors in ROS 2 using semiautomated tests. | ||||||
</description> | ||||||
|
||||||
<maintainer email="[email protected]">Brandon Ong</maintainer> | ||||||
|
||||||
<license>Apache License 2.0</license> | ||||||
|
||||||
<author email="[email protected]">Shane Loretz</author> | ||||||
|
||||||
<buildtool_depend>ament_cmake_ros</buildtool_depend> | ||||||
|
||||||
<!-- <depend>mininet</depend> TODO this rosdep key --> | ||||||
<depend>rclcpp</depend> | ||||||
<depend>test_msgs</depend> | ||||||
<depend>pytest</depend> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improved package.xml in 4df83da
|
||||||
|
||||||
<test_depend>ament_lint_auto</test_depend> | ||||||
<test_depend>ament_lint_common</test_depend> | ||||||
<test_depend>test_msgs</test_depend> | ||||||
|
||||||
<export> | ||||||
<build_type>ament_cmake</build_type> | ||||||
</export> | ||||||
</package> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Copyright 2023 Open Source Robotics Foundation, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
def pytest_addoption(parser): | ||
parser.addoption('--ros-workspaces', action='store') | ||
parser.addoption('--rmws', action='store') | ||
|
||
|
||
def pytest_generate_tests(metafunc): | ||
if 'rmw' in metafunc.fixturenames: | ||
metafunc.parametrize('rmw', metafunc.config.option.rmws.split(':')) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Copyright 2023 Open Source Robotics Foundation, Inc. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
# Run discovery tests | ||
# 1. Install dependencies | ||
# sudo apt install iputils-ping iproute2 mininet | ||
# TODO(sloretz) not needed? openvswitch-switch openvswitch-testcontroller | ||
# 2. TODO(sloretz) not needed? Start openvswitch service if not already running | ||
# sudo service openvswitch-switch start | ||
|
||
from mininet.net import Mininet | ||
from mininet.topo import MinimalTopo | ||
import pytest | ||
|
||
|
||
RANGES = [ | ||
'OFF', | ||
'SUBNET', | ||
'LOCALHOST', | ||
] | ||
|
||
|
||
class MininetFixture: | ||
__slots__ = ( | ||
'net', | ||
'h1', | ||
'h2', | ||
) | ||
|
||
|
||
def h1_ipv4(net: MininetFixture) -> str: | ||
return net.h1.IP() | ||
|
||
|
||
def h2_ipv4(net: MininetFixture) -> str: | ||
return net.h2.IP() | ||
|
||
|
||
def no_peer(net: MininetFixture) -> str: | ||
return '' | ||
|
||
|
||
@pytest.fixture() | ||
def mn(): | ||
f = MininetFixture() | ||
f.net = Mininet(topo=MinimalTopo()) | ||
f.h1 = f.net.getNodeByName('h1') | ||
f.h2 = f.net.getNodeByName('h2') | ||
|
||
f.net.start() | ||
yield f | ||
f.net.stop() | ||
|
||
|
||
# TODO(sloretz) figure out ROS workspace path from environment variables | ||
@pytest.fixture(scope='session') | ||
def ros_ws(pytestconfig): | ||
return pytestconfig.getoption('ros_workspaces').split(':') | ||
|
||
|
||
def make_env_str(ros_ws, rmw, discovery_range, peer): | ||
cmd = [] | ||
for ws in ros_ws: | ||
cmd.append('.') | ||
cmd.append(f'"{ws}/setup.bash"') | ||
cmd.append('&&') | ||
cmd.append(f'RMW_IMPLEMENTATION={rmw}') | ||
cmd.append(f'ROS_AUTOMATIC_DISCOVERY_RANGE={discovery_range}') | ||
cmd.append(f'ROS_STATIC_PEERS="{peer}"') | ||
return ' '.join(cmd) | ||
|
||
|
||
@pytest.mark.parametrize('sub_peer', (no_peer, h1_ipv4)) | ||
@pytest.mark.parametrize('sub_range', RANGES) | ||
@pytest.mark.parametrize('pub_peer', (no_peer, h1_ipv4)) | ||
@pytest.mark.parametrize('pub_range', RANGES) | ||
def test_samehost(mn, ros_ws, rmw, pub_range, pub_peer, sub_range, sub_peer): | ||
pub_peer = pub_peer(mn) | ||
sub_peer = sub_peer(mn) | ||
|
||
pub_env = make_env_str(ros_ws, rmw, pub_range, pub_peer) | ||
sub_env = make_env_str(ros_ws, rmw, sub_range, sub_peer) | ||
pub_cmd = pub_env + ' ros2 run test_discovery publish_once > /dev/null &' | ||
sub_cmd = sub_env + ' ros2 run test_discovery subscribe_once' | ||
print('$', pub_cmd) | ||
print('$', sub_cmd) | ||
|
||
mn.h1.cmd(pub_cmd) | ||
result = mn.h1.cmd(sub_cmd) | ||
message_received = 'test_discovery: message was received' in result.strip() | ||
|
||
if pub_peer or sub_peer: | ||
# if either has a static peer set, discovery should succeed | ||
assert message_received, result.strip() | ||
elif 'OFF' in (pub_range, sub_range): | ||
# With no static peer, if either has discovery off then it won't succeed | ||
assert not message_received, result.strip() | ||
else: | ||
# All other cases discovery | ||
assert message_received, result.strip() | ||
|
||
|
||
@pytest.mark.parametrize('sub_peer', (no_peer, h1_ipv4)) | ||
@pytest.mark.parametrize('sub_range', RANGES) | ||
@pytest.mark.parametrize('pub_peer', (no_peer, h2_ipv4)) | ||
@pytest.mark.parametrize('pub_range', RANGES) | ||
def test_differenthost(mn, ros_ws, rmw, pub_range, pub_peer, sub_range, sub_peer): | ||
pub_peer = pub_peer(mn) | ||
sub_peer = sub_peer(mn) | ||
|
||
pub_env = make_env_str(ros_ws, rmw, pub_range, pub_peer) | ||
sub_env = make_env_str(ros_ws, rmw, sub_range, sub_peer) | ||
pub_cmd = pub_env + ' ros2 run test_discovery publish_once > /dev/null &' | ||
sub_cmd = sub_env + ' ros2 run test_discovery subscribe_once' | ||
print('$', pub_cmd) | ||
print('$', sub_cmd) | ||
|
||
mn.h1.cmd(pub_cmd) | ||
result = mn.h2.cmd(sub_cmd) | ||
message_received = 'test_discovery: message was received' in result.strip() | ||
|
||
if pub_peer or sub_peer: | ||
# if either has a static peer set, discovery should succeed | ||
assert message_received, result.strip() | ||
elif 'SUBNET' == pub_range and 'SUBNET' == sub_range: | ||
# With no static peer, succeed only if both are set to SUBNET | ||
assert message_received, result.strip() | ||
else: | ||
# All other cases discovery | ||
assert not message_received, result.strip() |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,92 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#!/usr/bin/env python3 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Copyright 2023 Open Source Robotics Foundation, Inc. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# You may obtain a copy of the License at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# limitations under the License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import argparse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from ament_index_python import get_package_share_path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
from ament_index_python import get_resources | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_rmw_implementations(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
resources = list(get_resources('rmw_typesupport').keys()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if 'rmw_implementation' in resources: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
resources.remove('rmw_implementation') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return tuple(resources) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_tests_dir(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pkg_path = get_package_share_path('test_discovery') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return pkg_path / 'roottests' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def get_workspaces(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Get an ordered list of workspaces that are sourced | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefixes = os.environ['AMENT_PREFIX_PATH'] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not prefixes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError('No ROS/Colcon workspace sourced') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
workspaces = set() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for prefix in prefixes.split(':'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if not prefix: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# env var might have began or ended with a ':' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# If there exists a parent folder containing a setup.bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# then assume this is an isolated colcon workspace | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if os.path.exists(os.path.join(prefix, '../setup.bash')): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
workspaces.add(os.path.dirname(prefix)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
# Assume a merged ament/colcon workspace | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
workspaces.add(prefix) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return tuple(workspaces) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
def main(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parser = argparse.ArgumentParser() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parser.add_argument('--rmw', default=None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parser.add_argument('--select', default=None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
args = parser.parse_args() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rmw_implementations = get_rmw_implementations() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if args.rmw: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if args.rmw not in rmw_implementations: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
raise ValueError(f'{args.rmw} is not an installed rmw: {rmw_implementations}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rmw_implementations = [args.rmw] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append('sudo') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(sys.executable) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append('-m') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append('pytest') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append('-c') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(str(get_tests_dir() / 'conftest.py')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if args.select: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append('-k') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(args.select) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(f'--rmws={":".join(rmw_implementations)}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(f'--ros-workspaces={":".join(get_workspaces())}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cmd.append(str(get_tests_dir() / 'test_discovery.py')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using more lists and extend in 371ec23 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
print('Executing the following command:') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
print('================================') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
print('$', *cmd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
print('================================') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
os.execvp(cmd[0], cmd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if __name__ == '__main__': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// Copyright 2023 Open Source Robotics Foundation, Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
#include <chrono> | ||
#include <string> | ||
|
||
#include <rclcpp/rclcpp.hpp> | ||
#include <test_msgs/msg/builtins.hpp> | ||
|
||
|
||
constexpr double kMaxDiscoveryTime = 10; | ||
|
||
|
||
int main(int argc, char * argv[]) | ||
{ | ||
rclcpp::init(argc, argv); | ||
auto node = rclcpp::Node::make_shared("publish_once"); | ||
auto publisher = node->create_publisher<test_msgs::msg::Builtins>("test_topic", 10); | ||
|
||
auto clock = node->get_clock(); | ||
|
||
auto end_time = clock->now() + rclcpp::Duration::from_seconds(kMaxDiscoveryTime); | ||
while (rclcpp::ok() && publisher->get_subscription_count() == 0) { | ||
if (clock->now() >= end_time) { | ||
return 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a warning on timeout? |
||
} | ||
rclcpp::sleep_for(std::chrono::milliseconds(100)); | ||
} | ||
|
||
publisher->publish(test_msgs::msg::Builtins()); | ||
|
||
// Do nothing until killed. | ||
rclcpp::spin(node); | ||
return 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finding
ament_cmake_pytest
should be part of ament_lint_auto_find_test_dependencies now that it's in thepackage.xml
in 4df83da