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 6 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 800)
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
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
ament_add_pytest_test(test_discovery tests/test_discovery.py TIMEOUT 800)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
find_package(ament_cmake_pytest REQUIRED)
ament_add_pytest_test(test_discovery tests/test_discovery.py TIMEOUT 800)

Copy link
Contributor Author

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 the package.xml in 4df83da

endif()

ament_package()
Empty file added test_discovery/README.md
Empty file.
30 changes: 30 additions & 0 deletions test_discovery/package.xml
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>
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>test_msgs</test_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(':'))
141 changes: 141 additions & 0 deletions test_discovery/roottests/test_discovery.py
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()
92 changes: 92 additions & 0 deletions test_discovery/scripts/run_root_tests.py
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'))
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
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'))
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'
])

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
46 changes: 46 additions & 0 deletions test_discovery/src/publish_once.cpp
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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
Loading