Skip to content
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

Draft
wants to merge 21 commits into
base: rolling
Choose a base branch
from
Draft
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
41 changes: 41 additions & 0 deletions test_discovery/CMakeLists.txt
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 1600)
endif()

ament_package()
40 changes: 40 additions & 0 deletions test_discovery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# test_discovery

This package tests the use of `ROS_AUTOMATIC_DISCOVERY_RANGE` and `ROS_STATIC_PEERS` environment variables.
There are two sets of tests: automated tests that run when testing this package, and semiautomated tests that must be run manually after building this package.

# Automated tests

The automated tests run when testing this package.
They test only the cases that apply when two processes are on the same host.

## Semi-automated tests

The semiautomated tests use `mininet` to test discovery behavior across two different (virtual) hosts.
These tests require `root` access, and a working `mininet` install.

### Installing prerequisites

A working `mininet` install has only been tested on an Ubuntu based machine.
If you're running in a container, that container will need root priviledges.

First install the necessary dependencies:

```bash
sudo apt install \
iputils-ping \
iproute2 \
mininet \
openvswitch-switch \
openvswitch-testcontroller
```

Next, make sure the openvswitch service is running

```bash
sudo service openvswitch-switch start
```

You're now ready to run the tests.

### Running the semi-automated tests
34 changes: 34 additions & 0 deletions test_discovery/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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 make this a rosdep key -->
<depend>rclcpp</depend>
<depend>test_msgs</depend>
<depend>pytest</depend>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<depend>pytest</depend>
<depend>ament_cmake_pytest</depend>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved package.xml in 4df83da

ament_cmake_pytest is a test_depend, but pytest is still a run depend for the tests that run as root


<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_pytest</test_depend>

<test_depend>test_msgs</test_depend>

<exec_depend>ros2cli</exec_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
22 changes: 22 additions & 0 deletions test_discovery/roottests/conftest.py
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(':'))
123 changes: 123 additions & 0 deletions test_discovery/roottests/test_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# 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 openvswitch-switch openvswitch-testcontroller
# 2. 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 = [
# None, # idential to LOCALHOST, but takes too long to test here
'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.extend([
'.',
f'"{ws}/setup.bash"',
'&&',
])
cmd.extend([
f'RMW_IMPLEMENTATION={rmw}',
f'ROS_STATIC_PEERS="{peer}"',
])
if discovery_range is not None:
cmd.append(f'ROS_AUTOMATIC_DISCOVERY_RANGE={discovery_range}')
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, 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)

# Invalid node configuration could make OFF tests appear to succeed
assert 'test_discovery: node successfully created' in result.strip()

message_received = 'test_discovery: message was received' in result.strip()

if 'OFF' in (pub_range, sub_range):
# If either has discovery off then it won't succeed
assert not message_received, result.strip()
elif 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 don't discover each other
assert not message_received, result.strip()
97 changes: 97 additions & 0 deletions test_discovery/scripts/run_root_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/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 = [
'sudo',
sys.executable,
'-m',
'pytest',
'-c',
str(get_tests_dir() / 'conftest.py'),
]
if args.select:
cmd.extend([
'-k',
args.select,
])
cmd.extend([
f'--rmws={":".join(rmw_implementations)}',
f'--ros-workspaces={":".join(get_workspaces())}',
str(get_tests_dir() / 'test_discovery.py'),
])

print('Executing the following command:')
print('================================')
print('$', *cmd)
print('================================')

os.execvp(cmd[0], cmd)


if __name__ == '__main__':
main()
Loading