Skip to content

Commit

Permalink
Add tests for command line remapping (#268)
Browse files Browse the repository at this point in the history
* Add tests for command line remapping
  • Loading branch information
sloretz authored Jun 5, 2018
1 parent 72cc768 commit e95f4f5
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 0 deletions.
38 changes: 38 additions & 0 deletions test_cli_remapping/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
cmake_minimum_required(VERSION 3.5)

project(test_cli_remapping)

find_package(ament_cmake_auto REQUIRED)

if(BUILD_TESTING)
# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()

find_package(ament_cmake REQUIRED)
find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_pytest REQUIRED)
find_package(rclcpp REQUIRED)
find_package(test_msgs REQUIRED)

ament_lint_auto_find_test_dependencies()

add_executable(name_maker_rclcpp
test/name_maker.cpp)
ament_target_dependencies(name_maker_rclcpp
"rclcpp"
"test_msgs")

ament_add_pytest_test(test_cli_remapping
test/test_cli_remapping.py
ENV
NAME_MAKER_RCLCPP=$<TARGET_FILE:name_maker_rclcpp>
NAME_MAKER_RCLPY=${CMAKE_CURRENT_SOURCE_DIR}/test/name_maker.py
TIMEOUT 120)
set_tests_properties(test_cli_remapping
PROPERTIES DEPENDS
name_maker_rclcpp)
endif()

ament_auto_package()
27 changes: 27 additions & 0 deletions test_cli_remapping/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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_cli_remapping</name>
<version>0.4.0</version>
<description>
Test command line remapping of topic names, service names, node namespace, and node name.
</description>
<maintainer email="[email protected]">Shane Loretz</maintainer>
<license>Apache License 2.0</license>

<buildtool_depend>ament_cmake_auto</buildtool_depend>

<build_depend>ament_cmake</build_depend>

<test_depend>ament_cmake_pytest</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>launch</test_depend>
<test_depend>rclcpp</test_depend>
<test_depend>rclpy</test_depend>
<test_depend>test_msgs</test_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Empty file.
51 changes: 51 additions & 0 deletions test_cli_remapping/test/name_maker.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2018 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/empty.hpp"
#include "test_msgs/srv/empty.hpp"

int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);

std::string node_name = "original_node_name";
std::string namespace_ = "/original/namespace";
auto node = rclcpp::Node::make_shared(node_name, namespace_);

auto pub1 = node->create_publisher<test_msgs::msg::Empty>("~/private/name");
auto pub2 = node->create_publisher<test_msgs::msg::Empty>("relative/name");
auto pub3 = node->create_publisher<test_msgs::msg::Empty>("/fully/qualified/name");

auto do_nothing = [](
const test_msgs::srv::Empty::Request::SharedPtr request,
test_msgs::srv::Empty::Response::SharedPtr response) -> void
{
static_cast<void>(request);
static_cast<void>(response);
};

auto srv1 = node->create_service<test_msgs::srv::Empty>("~/private/name", do_nothing);
auto srv2 = node->create_service<test_msgs::srv::Empty>("relative/name", do_nothing);
auto srv3 = node->create_service<test_msgs::srv::Empty>("/fully/qualified/name", do_nothing);

rclcpp::spin(node);

rclcpp::shutdown();
return 0;
}
48 changes: 48 additions & 0 deletions test_cli_remapping/test/name_maker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2018 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 rclpy
import rclpy.node
from test_msgs.msg import Empty as EmptyMsg
from test_msgs.srv import Empty as EmptySrv


class NameMaker(rclpy.node.Node):

def __init__(self):
super().__init__('original_node_name', namespace='/original/namespace')

self._pubs = []
self._pubs.append(self.create_publisher(EmptyMsg, '~/private/name'))
self._pubs.append(self.create_publisher(EmptyMsg, 'relative/name'))
self._pubs.append(self.create_publisher(EmptyMsg, '/fully/qualified/name'))

self._srvs = []
self._srvs.append(self.create_service(EmptySrv, '~/private/name', lambda x: None))
self._srvs.append(self.create_service(EmptySrv, 'relative/name', lambda x: None))
self._srvs.append(self.create_service(EmptySrv, '/fully/qualified/name', lambda x: None))


if __name__ == '__main__':
rclpy.init()

node = NameMaker()

try:
rclpy.spin(node)
except KeyboardInterrupt:
print('Shutting down name_maker.py')
finally:
node.destroy_node()
rclpy.shutdown()
173 changes: 173 additions & 0 deletions test_cli_remapping/test/test_cli_remapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Copyright 2018 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 asyncio
import functools
import os
import random
import sys
import time

from launch import LaunchDescriptor
from launch.exit_handler import primary_exit_handler
from launch.launcher import DefaultLauncher
import pytest
import rclpy


def get_environment_variable(name):
"""Get environment variable or raise if it does not exist."""
path = os.getenv(name)
if not path:
raise EnvironmentError('Missing environment variable "%s"' % name)
return path


CLIENT_LIBRARY_EXECUTABLES = (
get_environment_variable('NAME_MAKER_RCLCPP'),
get_environment_variable('NAME_MAKER_RCLPY')
)


@pytest.fixture(scope='module', params=CLIENT_LIBRARY_EXECUTABLES)
def node_fixture(request):
"""Create a fixture with a node, name_maker executable, and random string."""
rclpy.init()
node = rclpy.create_node('test_cli_remapping')
try:
yield {
'node': node,
'executable': request.param,
'random_string': '%d_%s' % (
random.randint(0, 9999), time.strftime('%H_%M_%S', time.gmtime()))
}
finally:
node.destroy_node()
rclpy.shutdown()


def remapping_test(*, cli_args):
"""Return a decorator that returns a test function."""
def real_decorator(coroutine_test):
"""Return a test function that runs a coroutine test in a loop with a launched process."""
nonlocal cli_args

@functools.wraps(coroutine_test)
def test_func(node_fixture):
"""Run an executable with cli_args and coroutine test in the same asyncio loop."""
nonlocal cli_args

# Create a command launching a name_maker executable specified by the pytest fixture
command = [node_fixture['executable']]
# format command line arguments with random string from test fixture
for arg in cli_args:
command.append(arg.format(random_string=node_fixture['random_string']))

# Execute python files using same python used to start this test
env = dict(os.environ)
if command[0][-3:] == '.py':
command.insert(0, sys.executable)
env['PYTHONUNBUFFERED'] = '1'

ld = LaunchDescriptor()
ld.add_process(
cmd=command,
name='name_maker_' + coroutine_test.__name__,
env=env
)
ld.add_coroutine(
coroutine_test(node_fixture),
name=coroutine_test.__name__,
exit_handler=primary_exit_handler
)
launcher = DefaultLauncher()
launcher.add_launch_descriptor(ld)
return_code = launcher.launch()
assert return_code == 0, 'Launch failed with exit code %r' % (return_code,)
return test_func
return real_decorator


def get_topics(node_fixture):
topic_names_and_types = node_fixture['node'].get_topic_names_and_types()
return [name for name, _ in topic_names_and_types]


def get_services(node_fixture):
service_names_and_types = node_fixture['node'].get_service_names_and_types()
return [name for name, _ in service_names_and_types]


ATTEMPTS = 10
TIME_BETWEEN_ATTEMPTS = 1


@remapping_test(cli_args=('__node:=node_{random_string}',))
async def test_node_name_replacement_new(node_fixture):
node_name = 'node_{random_string}'.format(**node_fixture)

for attempt in range(ATTEMPTS):
if node_name in node_fixture['node'].get_node_names():
break
await asyncio.sleep(TIME_BETWEEN_ATTEMPTS)
rclpy.spin_once(node_fixture['node'], timeout_sec=0)
assert node_name in node_fixture['node'].get_node_names()


@remapping_test(cli_args=('__ns:=/ns/s{random_string}',))
async def test_namespace_replacement(node_fixture):
name = '/ns/s{random_string}/relative/name'.format(**node_fixture)

for attempt in range(ATTEMPTS):
if name in get_topics(node_fixture) and name in get_services(node_fixture):
break
await asyncio.sleep(TIME_BETWEEN_ATTEMPTS)
rclpy.spin_once(node_fixture['node'], timeout_sec=0)
assert name in get_topics(node_fixture) and name in get_services(node_fixture)


@remapping_test(cli_args=('/fully/qualified/name:=/remapped/s{random_string}',))
async def test_topic_and_service_replacement(node_fixture):
name = '/remapped/s{random_string}'.format(**node_fixture)

for attempt in range(ATTEMPTS):
if name in get_topics(node_fixture) and name in get_services(node_fixture):
break
await asyncio.sleep(TIME_BETWEEN_ATTEMPTS)
rclpy.spin_once(node_fixture['node'], timeout_sec=0)
assert name in get_topics(node_fixture) and name in get_services(node_fixture)


@remapping_test(cli_args=('rostopic://~/private/name:=/remapped/s{random_string}',))
async def test_topic_replacement(node_fixture):
name = '/remapped/s{random_string}'.format(**node_fixture)

for attempt in range(ATTEMPTS):
if name in get_topics(node_fixture):
break
await asyncio.sleep(TIME_BETWEEN_ATTEMPTS)
rclpy.spin_once(node_fixture['node'], timeout_sec=0)
assert name in get_topics(node_fixture) and name not in get_services(node_fixture)


@remapping_test(cli_args=('rosservice://~/private/name:=/remapped/s{random_string}',))
async def test_service_replacement(node_fixture):
name = '/remapped/s{random_string}'.format(**node_fixture)

for attempt in range(ATTEMPTS):
if name in get_services(node_fixture):
break
await asyncio.sleep(TIME_BETWEEN_ATTEMPTS)
rclpy.spin_once(node_fixture['node'], timeout_sec=0)
assert name not in get_topics(node_fixture) and name in get_services(node_fixture)

0 comments on commit e95f4f5

Please sign in to comment.