diff --git a/apex_rostest/apex_launchtest/.gitignore b/apex_rostest/apex_launchtest/.gitignore deleted file mode 100644 index 239c9252..00000000 --- a/apex_rostest/apex_launchtest/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.egg-info -*.pyc -*.pytest_cache -htmlcov/ diff --git a/apex_rostest/apex_launchtest/apex_launchtest/__init__.py b/apex_rostest/apex_launchtest/apex_launchtest/__init__.py deleted file mode 100644 index d6b60bdf..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - - -from .decorator import post_shutdown_test -from .io_handler import ActiveIoHandler, IoHandler -from .proc_info_handler import ActiveProcInfoHandler, ProcInfoHandler -from .ready_aggregator import ReadyAggregator - -__all__ = [ - # Functions - 'post_shutdown_test', - - # Classes - 'ActiveIoHandler', - 'ActiveProcInfoHandler', - 'IoHandler', - 'ProcInfoHandler', - 'ReadyAggregator', -] diff --git a/apex_rostest/apex_launchtest/apex_launchtest/apex_launchtest_main.py b/apex_rostest/apex_launchtest/apex_launchtest/apex_launchtest_main.py deleted file mode 100644 index b29a6212..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/apex_launchtest_main.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2019 Apex.AI, 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 logging -from importlib.machinery import SourceFileLoader -import os -import sys - -from .apex_runner import ApexRunner -from .junitxml import unittestResultsToXml -from .print_arguments import print_arguments_of_launch_description - -_logger_ = logging.getLogger(__name__) - - -def _load_python_file_as_module(python_file_path): - """Load a given Python launch file (by path) as a Python module.""" - # Taken from apex_core to not introduce a weird dependency thing - loader = SourceFileLoader('python_launch_file', python_file_path) - return loader.load_module() - - -def apex_launchtest_main(): - - logging.basicConfig() - - parser = argparse.ArgumentParser( - description="Integration test framework for Apex AI" - ) - - parser.add_argument('test_file') - - parser.add_argument('-v', '--verbose', - action="store_true", - default=False, - help="Run with verbose output") - - parser.add_argument('-s', '--show-args', '--show-arguments', - action='store_true', - default=False, - help='Show arguments that may be given to the test file.') - - parser.add_argument( - 'launch_arguments', - nargs='*', - help="Arguments to the launch file; ':=' (for duplicates, last one wins)" - ) - - parser.add_argument( - "--junit-xml", - action="store", - dest="xmlpath", - default=None, - help="write junit XML style report to specified path" - ) - - args = parser.parse_args() - - if args.verbose: - _logger_.setLevel(logging.DEBUG) - _logger_.debug("Running with verbose output") - - # Load the test file as a module and make sure it has the required - # components to run it as an apex integration test - _logger_.debug("Loading tests from file '{}'".format(args.test_file)) - if not os.path.isfile(args.test_file): - # Note to future reader: parser.error also exits as a side effect - parser.error("Test file '{}' does not exist".format(args.test_file)) - - args.test_file = os.path.abspath(args.test_file) - test_module = _load_python_file_as_module(args.test_file) - - _logger_.debug("Checking for generate_test_description") - if not hasattr(test_module, 'generate_test_description'): - parser.error( - "Test file '{}' is missing generate_test_description function".format(args.test_file) - ) - - dut_test_description_func = test_module.generate_test_description - _logger_.debug("Checking generate_test_description function signature") - - runner = ApexRunner( - gen_launch_description_fn=dut_test_description_func, - test_module=test_module, - launch_file_arguments=args.launch_arguments, - debug=args.verbose - ) - - _logger_.debug("Validating test configuration") - try: - runner.validate() - except Exception as e: - parser.error(e) - - if args.show_args: - print_arguments_of_launch_description( - launch_description=runner.get_launch_description() - ) - sys.exit(0) - - _logger_.debug("Running integration test") - try: - result, postcheck_result = runner.run() - _logger_.debug("Done running integration test") - - if args.xmlpath: - xml_report = unittestResultsToXml( - test_results={ - "active_tests": result, - "after_shutdown_tests": postcheck_result - } - ) - xml_report.write(args.xmlpath, xml_declaration=True) - - if not result.wasSuccessful(): - sys.exit(1) - - if not postcheck_result.wasSuccessful(): - sys.exit(1) - - except Exception as e: - import traceback - traceback.print_exc() - parser.error(e) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/apex_runner.py b/apex_rostest/apex_launchtest/apex_launchtest/apex_runner.py deleted file mode 100644 index 70bbb0f0..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/apex_runner.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright 2019 Apex.AI, 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 inspect -import threading -import unittest - -import launch -from launch import LaunchService -from launch import LaunchDescription -from launch.actions import RegisterEventHandler -from launch.event_handlers import OnProcessExit -from launch.event_handlers import OnProcessIO - -from .io_handler import ActiveIoHandler -from .loader import PostShutdownTestLoader, PreShutdownTestLoader -from .parse_arguments import parse_launch_arguments -from .proc_info_handler import ActiveProcInfoHandler -from .test_result import FailResult, TestResult - - -def _normalize_ld(launch_description_fn): - # A launch description fn can return just a launch description, or a tuple of - # (launch_description, test_context). This wrapper function normalizes things - # so we always get a tuple, sometimes with an empty dictionary for the test_context - def wrapper(*args, **kwargs): - result = launch_description_fn(*args, **kwargs) - if isinstance(result, tuple): - return result - else: - return result, {} - - return wrapper - - -class ApexRunner(object): - - def __init__(self, - gen_launch_description_fn, - test_module, - launch_file_arguments=[], - debug=False): - """ - Create an ApexRunner object. - - :param callable gen_launch_description_fn: A function that returns a ros2 LaunchDesription - for launching the processes under test. This function should take a callable as a - parameter which will be called when the processes under test are ready for the test to - start - """ - self._gen_launch_description_fn = gen_launch_description_fn - self._test_module = test_module - self._launch_service = LaunchService(debug=debug) - self._processes_launched = threading.Event() # To signal when all processes started - self._tests_completed = threading.Event() # To signal when all the tests have finished - self._launch_file_arguments = launch_file_arguments - - # Can't run LaunchService.run on another thread :-( - # See https://github.com/ros2/launch/issues/126 - # Instead, we'll let the tests run on another thread - self._test_tr = threading.Thread( - target=self._run_test, - name="test_runner_thread", - daemon=True - ) - - def get_launch_description(self): - return _normalize_ld(self._gen_launch_description_fn)(lambda: None)[0] - - def run(self): - """ - Launch the processes under test and run the tests. - - :return: A tuple of two unittest.Results - one for tests that ran while processes were - active, and another set for tests that ran after processes were shutdown - """ - test_ld, test_context = _normalize_ld( - self._gen_launch_description_fn - )(lambda: self._processes_launched.set()) - - # Data to squirrel away for post-shutdown tests - self.proc_info = ActiveProcInfoHandler() - self.proc_output = ActiveIoHandler() - self.test_context = test_context - parsed_launch_arguments = parse_launch_arguments(self._launch_file_arguments) - self.test_args = {} - for k, v in parsed_launch_arguments: - self.test_args[k] = v - - # Wrap the test_ld in another launch description so we can bind command line arguments to - # the test and add our own event handlers for process IO and process exit: - launch_description = LaunchDescription([ - launch.actions.IncludeLaunchDescription( - launch.LaunchDescriptionSource(launch_description=test_ld), - launch_arguments=parsed_launch_arguments - ), - RegisterEventHandler( - OnProcessExit(on_exit=lambda info, unused: self.proc_info.append(info)) - ), - RegisterEventHandler( - OnProcessIO( - on_stdout=self.proc_output.append, - on_stderr=self.proc_output.append, - ) - ), - ]) - - self._launch_service.include_launch_description( - launch_description - ) - - self._test_tr.start() # Run the tests on another thread - self._launch_service.run() # This will block until the test thread stops it - - if not self._tests_completed.wait(timeout=0): - # LaunchService.run returned before the tests completed. This can be because the user - # did ctrl+c, or because all of the launched nodes died before the tests completed - print("Processes under test stopped before tests completed") - self._print_process_output_summary() # <-- Helpful to debug why processes died early - # We treat this as a test failure and return some test results indicating such - return FailResult(), FailResult() - - # Now, run the post-shutdown tests - inactive_suite = PostShutdownTestLoader( - injected_attributes={ - "proc_info": self.proc_info, - "proc_output": self.proc_output._io_handler, - "test_args": self.test_args, - }, - injected_args=dict( - self.test_context, - # Add a few more things to the args dictionary: - **{ - "proc_info": self.proc_info, - "proc_output": self.proc_output._io_handler, - "test_args": self.test_args - } - ) - ).loadTestsFromModule(self._test_module) - inactive_results = unittest.TextTestRunner( - verbosity=2, - resultclass=TestResult - ).run(inactive_suite) - - return self._results, inactive_results - - def validate(self): - """Inspect the test configuration for configuration errors.""" - # Make sure the function signature of the launch configuration - # generator is correct - inspect.getcallargs(self._gen_launch_description_fn, lambda: None) - - def _run_test(self): - # Waits for the DUT processes to start (signaled by the _processes_launched - # event) and then runs the tests - - if not self._processes_launched.wait(timeout=15): - # Timed out waiting for the processes to start - print("Timed out waiting for processes to start up") - self._launch_service.shutdown() - return - - try: - # Load the tests - active_suite = PreShutdownTestLoader( - injected_attributes={ - "proc_info": self.proc_info, - "proc_output": self.proc_output, - "test_args": self.test_args, - }, - injected_args=dict( - self.test_context, - # Add a few more things to the args dictionary: - **{ - "proc_info": self.proc_info, - "proc_output": self.proc_output, - "test_args": self.test_args - } - ) - ).loadTestsFromModule(self._test_module) - - # Run the tests - self._results = unittest.TextTestRunner( - verbosity=2, - resultclass=TestResult - ).run(active_suite) - - finally: - self._tests_completed.set() - self._launch_service.shutdown() - - def _print_process_output_summary(self): - failed_procs = [proc for proc in self.proc_info if proc.returncode != 0] - - for process in failed_procs: - print("Process '{}' exited with {}".format(process.process_name, process.returncode)) - print("##### '{}' output #####".format(process.process_name)) - try: - for io in self.proc_output[process.action]: - print("{}".format(io.text.decode('ascii'))) - except KeyError: - pass # Process generated no output - print("#" * (len(process.process_name) + 21)) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/asserts/__init__.py b/apex_rostest/apex_launchtest/apex_launchtest/asserts/__init__.py deleted file mode 100644 index 07e16b8a..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/asserts/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from .assert_exit_codes import assertExitCodes -from .assert_exit_codes import EXIT_OK -from .assert_exit_codes import EXIT_SIGINT -from .assert_exit_codes import EXIT_SIGQUIT -from .assert_exit_codes import EXIT_SIGKILL -from .assert_exit_codes import EXIT_SIGSEGV -from .assert_output import assertInStdout -from .assert_sequential_output import assertSequentialStdout -from .assert_sequential_output import SequentialTextChecker - -from ..util.proc_lookup import NO_CMD_ARGS - -__all__ = [ - 'assertExitCodes', - 'assertInStdout', - 'assertSequentialStdout', - - 'SequentialTextChecker', - - 'NO_CMD_ARGS', - - 'EXIT_OK', - 'EXIT_SIGINT', - 'EXIT_SIGQUIT', - 'EXIT_SIGKILL', - 'EXIT_SIGSEGV', -] diff --git a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_exit_codes.py b/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_exit_codes.py deleted file mode 100644 index 91c96c50..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_exit_codes.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from ..util import resolveProcesses - -EXIT_OK = 0 -EXIT_SIGINT = 130 -EXIT_SIGQUIT = 131 -EXIT_SIGKILL = 137 -EXIT_SIGSEGV = 139 - - -def assertExitCodes(proc_info, - allowable_exit_codes=[EXIT_OK], - process=None, # By default, checks all processes - cmd_args=None, - *, - strict_proc_matching=True): - """ - Check the exit codes of the processes under test. - - :param iterable proc_info: A list of proc_info objects provided by the test framework to be - checked - """ - # Sanity check that the user didn't pass in something crazy for allowable exit codes - for code in allowable_exit_codes: - assert isinstance(code, int), "Provided exit code {} is not an int".format(code) - - to_check = resolveProcesses( - info_obj=proc_info, - process=process, - cmd_args=cmd_args, - strict_proc_matching=strict_proc_matching - ) - - for info in [proc_info[item] for item in to_check]: - assert info.returncode in allowable_exit_codes, "Proc {} exited with code {}".format( - info.process_name, - info.returncode - ) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_output.py b/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_output.py deleted file mode 100644 index b3d26227..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_output.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from ..util import resolveProcesses - - -def assertInStdout(proc_output, - msg, - process, - cmd_args=None, - *, - strict_proc_matching=True): - """ - Assert that 'msg' was found in the standard out of a process. - - :param proc_output: The process output captured by apex_launchtest. This is usually injected - into test cases as self._proc_output - :type proc_output: An apex_launchtest.IoHandler - - :param msg: The message to search for - :type msg: string - - :param process: The process whose output will be searched - :type process: A string (search by process name) or a launch.actions.ExecuteProcess object - - :param cmd_args: Optional. If 'process' is a string, cmd_args will be used to disambiguate - processes with the same name. Pass apex_launchtest.asserts.NO_CMD_ARGS to match a proc without - command arguments - :type cmd_args: string - - :param strict_proc_matching: Optional (default True), If proc is a string and the combination - of proc and cmd_args matches multiple processes, then strict_proc_matching=True will raise - an error. - :type strict_proc_matching: bool - """ - resolved_procs = resolveProcesses( - info_obj=proc_output, - process=process, - cmd_args=cmd_args, - strict_proc_matching=strict_proc_matching - ) - - for proc in resolved_procs: # Nominally just one matching proc - for output in proc_output[proc]: - if msg in output.text.decode(): - return - else: - names = ', '.join(sorted([p.process_details['name'] for p in resolved_procs])) - assert False, "Did not find '{}' in output for any of the matching process {}".format( - msg, - names - ) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_sequential_output.py b/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_sequential_output.py deleted file mode 100644 index 2aa8ce08..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/asserts/assert_sequential_output.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from contextlib import contextmanager - -from ..util import resolveProcesses - - -class SequentialTextChecker: - """Helper class for asserting that text is found in a certain order.""" - - def __init__(self, output): - self._output = output - self._array_index = 0 # Keeps track of how far we are into the output array - self._substring_index = 0 # Keeps track of how far we are into an individual string - - def assertInText(self, msg): - return self.assertInStdout(msg) - - def assertInStdout(self, msg): - - # Keep local copies of the array index and the substring index. We only advance them - # if we find a matching string. - - array_index = self._array_index - substring_index = self._substring_index - - for text in self._output[array_index:]: - found = text.find(msg, substring_index) - - if found != -1: - # We found the string! Update the search state for the next string - substring_index = found + len(msg) - self._array_index = array_index - self._substring_index = substring_index - return - - # We failed to find the string. Go around the loop again - array_index += 1 - substring_index = 0 - - assert False, "{} not found in output".format(msg) - - -@contextmanager -def assertSequentialStdout(proc_output, - process, - cmd_args=None): - """ - Create a context manager used to check stdout occured in a specific order. - - :param proc_output: The captured output from a test run - - :param process: The process whose output will be searched - :type process: A string (search by process name) or a launch.actions.ExecuteProcess object - - :param cmd_args: Optional. If 'proc' is a string, cmd_args will be used to disambiguate - processes with the same name. Pass apex_launchtest.asserts.NO_CMD_ARGS to match a proc without - command arguments - :type cmd_args: string - """ - process = resolveProcesses( - proc_output, - process=process, - cmd_args=cmd_args, - # There's no good way to sequence output from multiple processes reliably, so we won't - # pretend to be able to. Only allow one matching process for the comination of proc and - # cmd_args - strict_proc_matching=True, - )[0] - - # Get all the output from the process. This will be a list of strings. Each string may - # contain multiple lines of output - to_check = [p.text.decode() for p in proc_output[process]] - checker = SequentialTextChecker(to_check) - - try: - yield checker - except Exception: - # Do we need to log this, or give a better message? Need to re-raise so we can - # cause the test to fail - raise - finally: - # We don't need to do anything to finalize the checker here - pass diff --git a/apex_rostest/apex_launchtest/apex_launchtest/decorator.py b/apex_rostest/apex_launchtest/apex_launchtest/decorator.py deleted file mode 100644 index 90d916a6..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/decorator.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2019 Apex.AI, 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 post_shutdown_test(): - """Decorate tests that are meant to run after process shutdown.""" - def decorator(test_item): - if not isinstance(test_item, type): - raise TypeError("postcondition_test should decorate test classes") - test_item.__post_shutdown_test__ = True - return test_item - - return decorator diff --git a/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/__init__.py b/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/__init__.py deleted file mode 100644 index 2fc6f39e..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from .stdout_ready_listener import StdoutReadyListener - -__all__ = [ - # Functions - 'StdoutReadyListener', -] diff --git a/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/stdout_ready_listener.py b/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/stdout_ready_listener.py deleted file mode 100644 index f57bfd67..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/event_handlers/stdout_ready_listener.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from typing import Optional -from typing import Text - -from launch.actions import ExecuteProcess -from launch.event_handlers import OnProcessIO -from launch.some_actions_type import SomeActionsType - - -class StdoutReadyListener(OnProcessIO): - """ - Part of a LaunchDescription that can wait for processes to signal ready with stdout. - - Some processes signal that they're ready by printing a message to stdout. This listener - can be added to a launch description to wait for a particular process to output a particular - bit of text - """ - - def __init__( - self, - *, - target_action: Optional[ExecuteProcess] = None, - ready_txt: Text, - actions: [SomeActionsType] - ): - self.__ready_txt = ready_txt - self.__actions = actions - - super().__init__( - target_action=target_action, - on_stdout=self.__on_stdout - ) - - def __on_stdout(self, process_io): - if self.__ready_txt in process_io.text.decode(): - return self.__actions diff --git a/apex_rostest/apex_launchtest/apex_launchtest/io_handler.py b/apex_rostest/apex_launchtest/apex_launchtest/io_handler.py deleted file mode 100644 index 5d775d21..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/io_handler.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2019 Apex.AI, 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 threading - -from .asserts.assert_output import assertInStdout -from .util import NoMatchingProcessException -from .util import resolveProcesses - - -class IoHandler: - """ - Holds stdout captured from running processes. - - This class provides helper methods to enumerate the captured IO by individual processes - """ - - def __init__(self): - self._sequence_list = [] # A time-ordered list of IO from all processes - self._process_name_dict = {} # A dict of time ordered lists of IO key'd by the process - - def append(self, process_io): - self._sequence_list.append(process_io) - - if process_io.process_name not in self._process_name_dict: - self._process_name_dict[process_io.process_name] = [] - - self._process_name_dict[process_io.process_name].append(process_io) - - def __iter__(self): - return self._sequence_list.__iter__() - - def processes(self): - """ - Get an iterable of unique launch.events.process.RunningProcessEvent objects. - - :returns [launch.actions.ExecuteProcess]: - """ - return list( - [val[0].action for val in self._process_name_dict.values()] - ) - - def process_names(self): - """ - Get the name of all unique processes that generated IO. - - :returns [string]: - """ - return self._process_name_dict.keys() - - def __getitem__(self, key): - """ - Get the output for a given process or process name. - - :param key: The process to get the output for - :type key: String, or launch.actions.ExecuteProcess - """ - if isinstance(key, str): - return list(self._process_name_dict[key]) - else: - return list(self._process_name_dict[key.process_details['name']]) - - -class ActiveIoHandler(IoHandler): - """ - Holds stdout captured from running processes. - - The ActiveIoHandler is meant to be used when capturing is still in progress and provides - additional synchronization, as well as methods to wait on incoming IO - """ - - def __init__(self): - self._sync_lock = threading.Condition() - # Deliberately not calling the 'super' constructor here. We're building this class - # by composition so we can still give out the unsynchronized version - self._io_handler = IoHandler() - - def append(self, process_io): - with self._sync_lock: - self._io_handler.append(process_io) - self._sync_lock.notify() - - def __iter__(self): - with self._sync_lock: - return list(self._io_handler).__iter__() - - def processes(self): - """ - Get an iterable of unique launch.events.process.RunningProcessEvent objects. - - :returns [launch.actions.ExecuteProcess]: - """ - with self._sync_lock: - return list(self._io_handler.processes()) - - def process_names(self): - """ - Get the name of all unique processes that generated IO. - - :returns [string]: - """ - with self._sync_lock: - return list(self._io_handler.process_names()) - - def __getitem__(self, key): - """ - Get the output for a given process or process name. - - :param key: The process to get the output for - :type key: String, or launch.actions.ExecuteProcess - """ - with self._sync_lock: - return self._io_handler[key] - - def assertWaitFor(self, - msg, - process=None, # Will wait for IO from all procs by default - cmd_args=None, - *, - strict_proc_matching=True, - timeout=10): - success = False - - def msg_found(): - try: - assertInStdout( - self._io_handler, # Use unsynchronized, since this is called from a lock - msg=msg, - process=process, - cmd_args=cmd_args, - strict_proc_matching=strict_proc_matching - ) - return True - except NoMatchingProcessException: - # This can happen if no processes have generated any output yet. It's not fatal. - return False - except AssertionError: - return False - - with self._sync_lock: - # TODO(pete.baughman): Searching through all of the IO can be time consuming/wasteful. - # We can optimize this by making a note of where we left off searching and only - # searching new messages when we return from the wait. - success = self._sync_lock.wait_for( - msg_found, - timeout=timeout - ) - - if not success: - # Help the user a little. It's possible that they gave us a bad process name and no - # had no hope of matching anything. - matches = resolveProcesses(self, - process=process, - cmd_args=cmd_args, - strict_proc_matching=False) - if len(matches) == 0: - raise Exception( - "After fimeout, found no processes matching '{}' " - "It either doesn't exist, was never launched, " - "or didn't generate any output".format( - process - ) - ) - - assert success, "Wait for msg '{}' timed out".format(msg) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/junitxml.py b/apex_rostest/apex_launchtest/apex_launchtest/junitxml.py deleted file mode 100644 index c82ffcb8..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/junitxml.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2019 Apex.AI, 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 xml.etree.ElementTree as ET - - -def unittestResultsToXml(*, name="apex_launchtest", test_results={}): - """ - Serialize multiple unittest.TestResult objects into an XML document. - - A testSuites element will be the root element of the document. - """ - # The test_suites element is the top level of the XML result. - # apex_launchtest results contain two test suites - one from tests that ran while processes - # were active, and one from tests that ran after processes were shut down - test_suites = ET.Element('testsuites') - test_suites.set('name', name) - # test_suites.set('time', ????) skipping 'time' attribute - - # To get tests, failures, and errors, we just want to iterate the results once - tests = 0 - failures = 0 - errors = 0 - - for result in test_results.values(): - tests += result.testsRun - failures += len(result.failures) - errors += len(result.errors) - - test_suites.set('tests', str(tests)) - test_suites.set('failures', str(failures)) - test_suites.set('errors', str(errors)) - - for (key, value) in test_results.items(): - test_suites.append(unittestResultToXml(key, value)) - - return ET.ElementTree(test_suites) - - -def unittestResultToXml(name, test_result): - """ - Serialize a single unittest.TestResult to an XML element. - - - . . . - - """ - test_suite = ET.Element('testsuite') - test_suite.set('name', name) - test_suite.set('tests', str(test_result.testsRun)) - test_suite.set('failures', str(len(test_result.failures))) - test_suite.set('errors', str(len(test_result.errors))) - test_suite.set('skipped', str(len(test_result.skipped))) - test_suite.set('time', str(round(sum(test_result.testTimes.values()), 3))) - - for case in test_result.testCases: - test_suite.append(unittestCaseToXml(test_result, case)) - - return test_suite - - -def unittestCaseToXml(test_result, test_case): - """ - Serialize a unittest.TestCase into an XML element. - - - - Note - an ordinary unittest.TestResult does not record time information. The TestResult - class needs to be an apex_launchtest TestResult class - """ - case_xml = ET.Element('testcase') - case_xml.set('name', test_case._testMethodName) - case_xml.set('time', str(round(test_result.testTimes[test_case], 3))) - - for failure in test_result.failures: - # We're enumerating a list of (test_case, failure_string) tuples here - if failure[0] == test_case: - failure_xml = ET.Element('failure') - failure_xml.set('message', failure[1]) - case_xml.append(failure_xml) - - for error in test_result.errors: - # Same as above. (test_case, error_string) tuples - if error[0] == test_case: - error_xml = ET.Element('error') - error_xml.set('message', error[1]) - case_xml.append(error_xml) - - for skip in test_result.skipped: - if skip[0] == test_case: - skip_xml = ET.Element('skipped') - skip_xml.text = skip[1] - case_xml.append(skip_xml) - - return case_xml diff --git a/apex_rostest/apex_launchtest/apex_launchtest/loader.py b/apex_rostest/apex_launchtest/apex_launchtest/loader.py deleted file mode 100644 index 48bfe3b0..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/loader.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2019 Apex.AI, 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 functools -import inspect -import unittest - - -def PreShutdownTestLoader(injected_attributes={}, injected_args={}): - return _make_loader(False, injected_attributes, injected_args) - - -def PostShutdownTestLoader(injected_attributes={}, injected_args={}): - return _make_loader(True, injected_attributes, injected_args) - - -def _make_loader(load_post_shutdown, injected_attributes, injected_args): - - class _loader(unittest.TestLoader): - """TestLoader selectively loads pre-shutdown or post-shutdown tests.""" - - def loadTestsFromTestCase(self, testCaseClass): - - if getattr(testCaseClass, "__post_shutdown_test__", False) == load_post_shutdown: - cases = super(_loader, self).loadTestsFromTestCase(testCaseClass) - - # Inject test attributes into the test as self.whatever. This method of giving - # objects to the test is pretty inferior to injecting them as arguments to the - # test methods - we may deprecate this in favor of everything being an argument - for name, value in injected_attributes.items(): - _give_attribute_to_tests(value, name, cases) - - # Give objects with matching names as arguments to tests. This doesn't have the - # weird scoping and name collision issues that the above method has. In fact, - # we give proc_info and proc_output to the tests as arguments too, so anything - # you can do with test attributes can also be accomplished with test arguments - _bind_test_args_to_tests(injected_args, cases) - - return cases - else: - # Empty test suites will be ignored by the test runner - return self.suiteClass() - - return _loader() - - -def _bind_test_args_to_tests(context, test_suite): - # Look for tests that expect additional arguments and bind items from the context - # to the tests - for test in _iterate_tests_in_test_suite(test_suite): - # Need to reach a little deep into the implementation here to get the test - # method. See unittest.TestCase - test_method = getattr(test, test._testMethodName) - # Replace the test with a functools.partial that has the arguments - # provided by the test context already bound - setattr( - test, - test._testMethodName, - _partially_bind_matching_args(test_method, context) - ) - - test.setUp = _partially_bind_matching_args( - test.setUp, - context - ) - - test.tearDown = _partially_bind_matching_args( - test.tearDown, - context - ) - - for test_class in _iterate_test_classes_in_test_suite(test_suite): - test_class.setUpClass = _partially_bind_matching_args( - test_class.setUpClass, - context - ) - test_class.tearDownClass = _partially_bind_matching_args( - test_class.tearDownClass, - context - ) - - -def _partially_bind_matching_args(unbound_function, arg_candidates): - function_arg_names = inspect.getfullargspec(unbound_function).args - # We only want to bind the part of the context matches the test args - matching_args = {k: v for (k, v) in arg_candidates.items() if k in function_arg_names} - return functools.partial(unbound_function, **matching_args) - - -def _give_attribute_to_tests(data, attr_name, test_suite): - # Test suites can contain other test suites which will eventually contain - # the actual test classes to run. This function will recursively drill down until - # we find the actual tests and give the tests a reference to the process - - # The effect of this is that every test will have `self.attr_name` available to it so that - # it can interact with ROS2 or the process exit coes, or IO or whatever data we want - for test in _iterate_tests_in_test_suite(test_suite): - setattr(test, attr_name, data) - - -def _iterate_test_classes_in_test_suite(test_suite): - classes = [] - for t in _iterate_tests_in_test_suite(test_suite): - if t.__class__ not in classes: - classes.append(t.__class__) - yield t.__class__ - - -def _iterate_tests_in_test_suite(test_suite): - try: - iter(test_suite) - except TypeError: - # Base case - test_suite is not iterable, so it must be an individual test method - yield test_suite - else: - # Otherwise, it's a test_suite, or a list of individual test methods. recurse - for test in test_suite: - yield from _iterate_tests_in_test_suite(test) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/parse_arguments.py b/apex_rostest/apex_launchtest/apex_launchtest/parse_arguments.py deleted file mode 100644 index cab3a813..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/parse_arguments.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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. - -from collections import OrderedDict -from typing import List -from typing import Text -from typing import Tuple - - -# This was copy/pasted from ros2launch.api to avoid a rclpy dependency in apex_launchtest -def parse_launch_arguments(launch_arguments: List[Text]) -> List[Tuple[Text, Text]]: - """Parse the given launch arguments from the command line, into list of tuples for launch.""" - parsed_launch_arguments = OrderedDict() # type: ignore - for argument in launch_arguments: - count = argument.count(':=') - if count == 0 or argument.startswith(':=') or (count == 1 and argument.endswith(':=')): - raise RuntimeError( - "malformed launch argument '{}', expected format ':='" - .format(argument)) - name, value = argument.split(':=', maxsplit=1) - parsed_launch_arguments[name] = value # last one wins is intentional - return parsed_launch_arguments.items() diff --git a/apex_rostest/apex_launchtest/apex_launchtest/print_arguments.py b/apex_rostest/apex_launchtest/apex_launchtest/print_arguments.py deleted file mode 100644 index e7932db3..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/print_arguments.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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. - - -def print_arguments_of_launch_description(*, launch_description): - """Print the arguments of a LaunchDescription to the console.""" - print("Arguments (pass arguments as ':='):") - launch_arguments = launch_description.get_launch_arguments() - any_conditional_arguments = False - for argument_action in launch_arguments: - msg = "\n '" - msg += argument_action.name - msg += "':" - if argument_action._conditionally_included: - any_conditional_arguments = True - msg += '*' - msg += '\n ' - msg += argument_action.description - if argument_action.default_value is not None: - default_str = ' + '.join([token.describe() for token in argument_action.default_value]) - msg += '\n (default: {})'.format(default_str) - print(msg) - - if len(launch_arguments) > 0: - if any_conditional_arguments: - print('\n* argument(s) which are only used if specific conditions occur') - else: - print('\n No arguments.') diff --git a/apex_rostest/apex_launchtest/apex_launchtest/proc_info_handler.py b/apex_rostest/apex_launchtest/apex_launchtest/proc_info_handler.py deleted file mode 100644 index 72e732e2..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/proc_info_handler.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2019 Apex.AI, 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 threading -from launch.actions import ExecuteProcess # noqa - -from .util import resolveProcesses -from .util import NoMatchingProcessException - - -class ProcInfoHandler: - """Captures exit codes from processes when they terminate.""" - - def __init__(self): - self._proc_info = {} - - def append(self, process_info): - self._proc_info[process_info.action] = process_info - - def __iter__(self): - return self._proc_info.values().__iter__() - - def processes(self): - """Get the ExecuteProcess launch actions of all recorded processes.""" - return self._proc_info.keys() - - def process_names(self): - """Get the name of all recorded processes.""" - return map( - lambda x: x.process_details['name'], - self._proc_info.keys() - ) - - def __getitem__(self, key): - """ - Get the ProcessExited event for the specified process. - - :param key: Either a string, or a launch.actions.ExecuteProcess object - :returns launch.events.process.ProcessExited: - """ - if isinstance(key, str): - # Look up by process name - for (launch_action, value) in self._proc_info.items(): - if key in launch_action.process_details['name']: - return value - else: - raise KeyError(key) - else: - return self._proc_info[key] - - -class ActiveProcInfoHandler(ProcInfoHandler): - """Allows tests to wait on process termination before proceeding.""" - - def __init__(self): - self._sync_lock = threading.Condition() - # Deliberately not calling the 'super' constructor here. We're building this class - # by composition so we can still give out the unsynchronized version - self._proc_info_handler = ProcInfoHandler() - - def append(self, process_info): - with self._sync_lock: - self._proc_info_handler.append(process_info) - self._sync_lock.notify() - - def __iter__(self): - with self._sync_lock: - return self._proc_info_handler.__iter__() - - def processes(self): - """ - Get the ExecuteProcess launch actions of all recorded processes. - - :returns [launch.actions.ExecuteProcess]: - """ - with self._sync_lock: - return list(self._proc_info_handler.processes()) - - def process_names(self): - """ - Get the name of all recorded processes. - - :returns [string]: - """ - with self._sync_lock: - return list(self._proc_info_handler.process_names()) - - def __getitem__(self, key): - with self._sync_lock: - return self._proc_info_handler[key] - - def assertWaitForShutdown(self, - process, - cmd_args=None, - *, - timeout=10): - - success = False - - def proc_is_shutdown(): - try: - resolveProcesses( - info_obj=self._proc_info_handler, - process=process, - cmd_args=cmd_args, - strict_proc_matching=True - ) - return True - except NoMatchingProcessException: - return False - - with self._sync_lock: - success = self._sync_lock.wait_for( - proc_is_shutdown, - timeout=timeout - ) - - assert success, "Timed out waiting for process '{}' to finish".format(process) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/ready_aggregator.py b/apex_rostest/apex_launchtest/apex_launchtest/ready_aggregator.py deleted file mode 100644 index e573bd4c..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/ready_aggregator.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2019 Apex.AI, 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 threading - - -class ReadyAggregator: - """Calls a ready_fn parent function on the nth call to a child function.""" - - def __init__(self, ready_fn, num_to_aggregate): - """ - Create a ReadyAggregator. - - :param callable ready_fn: The function to call after n calls to ReadyAggregator.ready_fn - :param int num_to_aggregate: Number of calls to ReadyAggregator.ready_fn necessary for - the parent ready_fn to be called - """ - self._parent_ready_fn = ready_fn - self._count_to_activate = num_to_aggregate - - self._lock = threading.Lock() - - def ready_fn(self): - - # We don't want to call the parent ready function while holding a lock - # in case it does something that will try to acquire the same lock. Instead, - # we'll make a local copy of the count and evaluate it outside of the critical - # section - local_count = 0 - with self._lock: - self._count_to_activate -= 1 - local_count = self._count_to_activate - - if local_count == 0: - self._parent_ready_fn() diff --git a/apex_rostest/apex_launchtest/apex_launchtest/test_result.py b/apex_rostest/apex_launchtest/apex_launchtest/test_result.py deleted file mode 100644 index f2af0c6d..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/test_result.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2019 Apex.AI, 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 time -import unittest - - -class FailResult(unittest.TestResult): - """For test runs that fail when the DUT dies unexpectedly.""" - - @property - def testCases(self): - return [] - - @property - def testTimes(self): - """Get a dict of {test_case: elapsed_time}.""" - return {} - - def wasSuccessful(self): - return False - - -class TestResult(unittest.TextTestResult): - """ - Subclass of unittest.TestResult that collects more information about the tests that ran. - - This class extends TestResult by recording all of the tests that ran, and by recording - start and stop time for the individual test cases - """ - - def __init__(self, stream=None, descriptions=None, verbosity=None): - self.__test_cases = {} - super().__init__(stream, descriptions, verbosity) - - @property - def testCases(self): - return self.__test_cases.keys() - - @property - def testTimes(self): - """Get a dict of {test_case: elapsed_time}.""" - return {k: v['end'] - v['start'] for (k, v) in self.__test_cases.items()} - - def startTest(self, test): - self.__test_cases[test] = { - 'start': time.time(), - 'end': 0 - } - super().startTest(test) - - def stopTest(self, test): - self.__test_cases[test]['end'] = time.time() - super().stopTest(test) diff --git a/apex_rostest/apex_launchtest/apex_launchtest/util/__init__.py b/apex_rostest/apex_launchtest/apex_launchtest/util/__init__.py deleted file mode 100644 index 539b770d..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/util/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from sys import executable as __executable -from launch.actions import ExecuteProcess as __ExecuteProcess - -from .proc_lookup import NO_CMD_ARGS -from .proc_lookup import resolveProcesses -from .proc_lookup import NoMatchingProcessException - - -def KeepAliveProc(): - """ - Generate a dummy launch.actions.ExecuteProcess to keep the launch alive. - - apex_launchtest expects to shut down the launch itself when it's done running tests. If all - of the processes under test are expected to terminate on their own, it's necessary to add - another process to keep the launch service alive while the tests are running. - """ - script = """ -try: - while True: - pass -except KeyboardInterrupt: - pass -""" - return __ExecuteProcess( - cmd=[ - __executable, - '-c', - script - ], - ) - - -__all__ = [ - 'resolveProcesses', - - 'KeepAliveProc', - 'NoMatchingProcessException', - - 'NO_CMD_ARGS', -] diff --git a/apex_rostest/apex_launchtest/apex_launchtest/util/proc_lookup.py b/apex_rostest/apex_launchtest/apex_launchtest/util/proc_lookup.py deleted file mode 100644 index 4d0808c4..00000000 --- a/apex_rostest/apex_launchtest/apex_launchtest/util/proc_lookup.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2019 Apex.AI, 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 launch.actions - -NO_CMD_ARGS = object() - - -class NoMatchingProcessException(Exception): - pass - - -def _proc_to_name_and_args(proc): - # proc is a launch.actions.ExecuteProcess - return "{} {}".format( - proc.process_details['name'], - " ".join(proc.process_details['cmd'][1:]) - ) - - -def _str_name_to_process(info_obj, proc_name, cmd_args): - - def name_match_fn(proc): - return proc_name in proc.process_details['name'] - - def cmd_match_fn(proc): - if cmd_args is None: - return True - elif cmd_args is NO_CMD_ARGS: - return len(proc.process_details['cmd']) == 1 - else: - return cmd_args in proc.process_details['cmd'][1:] - - matches = [proc for proc in info_obj.processes() - if name_match_fn(proc) and cmd_match_fn(proc)] - - return matches - - -def resolveProcesses(info_obj, *, process=None, cmd_args=None, strict_proc_matching=True): - """ - Resolve a process name and cmd arguments to one or more launch.actions.ExecuteProcess. - - :param info_obj: a ProcInfoHandler or an IoHandler that contains processes that could match - - :param process: One or more processes to match. Pass None to match all processes - :type process: A launch.actions.ExecuteProcess object to match a specific process, or a string - to search by process name - - :param cmd_args: Optional. If the process param is a string, the cmd_args will be used to - disambiguate processes with the same name. cmd_args=None will match all command arguments. - cmd_args=apex_launchtest.asserts.NO_CMD_ARGS will match a process without command-line - arguments - - :param strict_proc_matching: Optional. If the process param is a string and matches multiple - processes, strict_proc_matching=True will raise an error - - :returns: A list of one or more matching processes taken from the info_obj. If no processes - in info_obj match, a NoMatchingProcessException will be raised. - """ - if process is None: - # We want to search all processes - all_procs = info_obj.processes() - if len(all_procs) == 0: - raise NoMatchingProcessException("No data recorded for any process") - return all_procs - - if isinstance(process, launch.actions.ExecuteProcess): - # We want to search a specific process - if process in info_obj.processes(): - return [process] - else: - raise NoMatchingProcessException( - "No data recorded for proc {}".format(_proc_to_name_and_args(process)) - ) - - elif isinstance(process, str): - # We want to search one (or more) processes that match a particular string. The "or more" - # part is controlled by the strict_proc_matching argument - matches = _str_name_to_process(info_obj, process, cmd_args) - if len(matches) == 0: - names = ', '.join(sorted([_proc_to_name_and_args(p) for p in info_obj.processes()])) - - raise NoMatchingProcessException( - "Did not find any processes matching name '{}' and args '{}'. Procs: {}".format( - process, - cmd_args, - names - ) - ) - - if strict_proc_matching and len(matches) > 1: - names = ', '.join(sorted([_proc_to_name_and_args(p) for p in info_obj.processes()])) - raise Exception( - "Found multiple processes matching name '{}' and cmd_args '{}'. Procs: {}".format( - process, - cmd_args, - names - ) - ) - return list(matches) - - else: - # Invalid argument passed for 'process' - raise TypeError( - "proc argument must be 'ExecuteProcess' or 'str' not {}".format(type(process)) - ) diff --git a/apex_rostest/apex_launchtest/example_processes/exit_code_proc b/apex_rostest/apex_launchtest/example_processes/exit_code_proc deleted file mode 100755 index e14eed5a..00000000 --- a/apex_rostest/apex_launchtest/example_processes/exit_code_proc +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2019 Apex.AI, 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 sys -import time - - -# This process pretends to do some simple setup, then pretends to do some simple work, -# then shuts itself down automatically -if __name__ == "__main__": - - if "--silent" in sys.argv[1:]: - sys.exit(1) - - if '--exception' in sys.argv[1:]: - raise Exception("Process had a pretend error") - - print("Exiting with a code") - sys.exit(1) diff --git a/apex_rostest/apex_launchtest/example_processes/good_proc b/apex_rostest/apex_launchtest/example_processes/good_proc deleted file mode 100755 index 4632ffcf..00000000 --- a/apex_rostest/apex_launchtest/example_processes/good_proc +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2019 Apex.AI, 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 sys -import time - - -# This is a simple program that generates some stdout, waits for ctrl+c, and exits with -# an exit code of zero -if __name__ == "__main__": - - # if sys.argv[1:]: - # print("Called with arguments {}".format(sys.argv[1:])) - - print("Starting Up") - - loops = 0 - try: - while True: - print("Loop {}".format(loops)) - loops += 1 - time.sleep(1.0) - except KeyboardInterrupt: - pass - - print("Shutting Down") - - sys.exit(0) diff --git a/apex_rostest/apex_launchtest/example_processes/terminating_proc b/apex_rostest/apex_launchtest/example_processes/terminating_proc deleted file mode 100755 index 9dc03cdf..00000000 --- a/apex_rostest/apex_launchtest/example_processes/terminating_proc +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2019 Apex.AI, 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 sys -import time - - -# This process pretends to do some simple setup, then pretends to do some simple work, -# then shuts itself down automatically -if __name__ == "__main__": - - print("Starting Up") - time.sleep(1.0) - print("Ready") - - if sys.argv[1:]: - print("Called with arguments {}".format(sys.argv[1:])) - - if '--exception' in sys.argv[1:]: - raise Exception("Process had a pretend error") - - try: - print("Emulating Work") - time.sleep(1.0) - print("Done") - except KeyboardInterrupt: - pass - - print("Shutting Down") - - sys.exit(0) diff --git a/apex_rostest/apex_launchtest/examples/README.md b/apex_rostest/apex_launchtest/examples/README.md deleted file mode 100644 index 100ab1ff..00000000 --- a/apex_rostest/apex_launchtest/examples/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Examples - -## `good_proc.test.py` - -Usage: -> apex_launchtest examples/good_proc.test.py - -This test checks a process called good_proc (source found in the [example_processes folder](../example_processes)). -good_proc is a simple python process that prints "Loop 1, Loop2, etc. every second until it's terminated with ctrl+c. -The test will launch the process, wait for a few loops to complete by monitoring stdout, then terminate the process -and run some post-shutdown checks. - -The pre-shutdown tests check that "Loop 1, Loop 2, Loop 3, Loop 4" -are all printed to stdout. Once this test finishes, the process under test is shut down - -After shutdown, we run a similar test that checks more output, and also checks the -order of the output. `test_out_of_order` demonstrates that the `assertSequentialStdout` -context manager is able to detect out of order stdout. - -## `args.test.py` - -Usage to view the arguments: ->apex_launchtest examples/args.test.py --show-args - -Usage to run the test: ->apex_launchtest examples/args.test.py dut_arg:=hey - -This example shows how to pass arguments into an apex_launchtest. The arguments are made avilable -in the launch description via a launch.substitutions.LaunchConfiguration. The arguments are made -available to the test cases via a self.test_args dictionary - -This example will fail if no arguments are passed. - -## `example_test_context.test.py` - -Usage: -> apex_launchtest examples/example_test_context.test.py - -This example shows how the `generate_test_description` function can return a tuple where the second -item is a dictionary of objects that will be injected into the individual test cases. Tests that -wish to use elements of the test context can add arguments with names matching the keys of the dictionary. diff --git a/apex_rostest/apex_launchtest/examples/args.test.py b/apex_rostest/apex_launchtest/examples/args.test.py deleted file mode 100644 index f0125a51..00000000 --- a/apex_rostest/apex_launchtest/examples/args.test.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import unittest - -import ament_index_python -import launch -import launch.actions -import launch.substitutions - -import apex_launchtest -import apex_launchtest.util - - -dut_process = launch.actions.ExecuteProcess( - cmd=[ - os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'terminating_proc', - ), - - # Arguments - launch.substitutions.LaunchConfiguration('dut_arg') - ], -) - - -def generate_test_description(ready_fn): - - return launch.LaunchDescription([ - - # This argument can be passed into the test, and can be discovered by running - # apex_launchtest --show-args - launch.actions.DeclareLaunchArgument( - 'dut_arg', - default_value=['default'], - description='Passed to the terminating process', - ), - - dut_process, - - # In tests where all of the procs under tests terminate themselves, it's necessary - # to add a dummy process not under test to keep the launch alive. apex_launchtest - # provides a simple launch action that does this: - apex_launchtest.util.KeepAliveProc(), - - launch.actions.OpaqueFunction(function=lambda context: ready_fn()) - ]) - - -class TestTerminatingProcessStops(unittest.TestCase): - - def test_proc_terminates(self): - self.proc_info.assertWaitForShutdown(process=dut_process, timeout=10) - - -@apex_launchtest.post_shutdown_test() -class TestProcessOutput(unittest.TestCase): - - def test_ran_with_arg(self): - self.assertNotIn( - 'default', - dut_process.process_details['cmd'], - "Try running: apex_launchtest test_with_args.test.py dut_arg:=arg" - ) - - def test_arg_printed_in_output(self): - apex_launchtest.asserts.assertInStdout( - self.proc_output, - self.test_args['dut_arg'], - dut_process - ) - - def test_default_not_printed(self): - with self.assertRaises(AssertionError): - apex_launchtest.asserts.assertInStdout( - self.proc_output, - "default", - dut_process - ) diff --git a/apex_rostest/apex_launchtest/examples/example_test_context.test.py b/apex_rostest/apex_launchtest/examples/example_test_context.test.py deleted file mode 100644 index 898093d5..00000000 --- a/apex_rostest/apex_launchtest/examples/example_test_context.test.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import unittest - -import ament_index_python -import launch -import launch.actions - -import apex_launchtest -from apex_launchtest.asserts import assertSequentialStdout - - -TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'good_proc' -) - - -# This launch description shows the prefered way to let the tests access launch actions. By -# adding them to the test context, it's not necessary to scope them at the module level like in -# the good_proc.test.py example -def generate_test_description(ready_fn): - # This is necessary to get unbuffered output from the process under test - proc_env = os.environ.copy() - proc_env["PYTHONUNBUFFERED"] = "1" - - dut_process = launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH], - env=proc_env, - ) - - ld = launch.LaunchDescription([ - dut_process, - - # Start tests right away - no need to wait for anything - launch.actions.OpaqueFunction(function=lambda context: ready_fn()), - ]) - - # Items in this dictionary will be added to the test cases as an attribute based on - # dictionary key - test_context = { - "dut": dut_process, - "int_val": 10 - } - - return ld, test_context - - -class TestProcOutput(unittest.TestCase): - - def test_process_output(self, dut): - # We can use the 'dut' argument here because it's part of the test context - # returned by `generate_test_description` It's not necessary for every - # test to use every piece of the context - self.proc_output.assertWaitFor("Loop 1", process=dut, timeout=10) - - -@apex_launchtest.post_shutdown_test() -class TestProcessOutput(unittest.TestCase): - - def test_full_output(self, dut): - # Same as the test_process_output test. apex_launchtest binds the value of - # 'dut' from the test_context to the test before it runs - with assertSequentialStdout(self.proc_output, process=dut) as cm: - cm.assertInStdout("Starting Up") - cm.assertInStdout("Shutting Down") - - def test_int_val(self, int_val): - # Arguments don't have to be part of the LaunchDescription. Any object can - # be passed in - self.assertEqual(int_val, 10) - - def test_all_context_objects(self, int_val, dut): - # Multiple arguments are supported - self.assertEqual(int_val, 10) - self.assertIn("good_proc", dut.process_details['name']) - - def test_all_context_objects_different_order(self, dut, int_val): - # Put the arguments in a different order from the above test - self.assertEqual(int_val, 10) - self.assertIn("good_proc", dut.process_details['name']) diff --git a/apex_rostest/apex_launchtest/examples/good_proc.test.py b/apex_rostest/apex_launchtest/examples/good_proc.test.py deleted file mode 100644 index a75cf707..00000000 --- a/apex_rostest/apex_launchtest/examples/good_proc.test.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import unittest - -import ament_index_python -import launch -import launch.actions - -import apex_launchtest -from apex_launchtest.asserts import assertSequentialStdout - - -TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'good_proc' -) - -# This is necessary to get unbuffered output from the process under test -proc_env = os.environ.copy() -proc_env["PYTHONUNBUFFERED"] = "1" - -dut_process = launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH], - env=proc_env, -) - - -def generate_test_description(ready_fn): - - return launch.LaunchDescription([ - dut_process, - - # Start tests right away - no need to wait for anything - launch.actions.OpaqueFunction(function=lambda context: ready_fn()), - ]) - - -# These tests will run concurrently with the dut process. After all these tests are done, -# the launch system will shut down the processes that it started up -class TestGoodProcess(unittest.TestCase): - - def test_count_to_four(self): - # This will match stdout from any process. In this example there is only one process - # running - self.proc_output.assertWaitFor("Loop 1", timeout=10) - self.proc_output.assertWaitFor("Loop 2", timeout=10) - self.proc_output.assertWaitFor("Loop 3", timeout=10) - self.proc_output.assertWaitFor("Loop 4", timeout=10) - - -@apex_launchtest.post_shutdown_test() -class TestProcessOutput(unittest.TestCase): - - def test_exit_code(self): - # Check that all processes in the launch (in this case, there's just one) exit - # with code 0 - apex_launchtest.asserts.assertExitCodes(self.proc_info) - - def test_full_output(self): - # Using the SequentialStdout context manager asserts that the following stdout - # happened in the same order that it's checked - with assertSequentialStdout(self.proc_output, dut_process) as cm: - cm.assertInStdout("Starting Up") - for n in range(4): - cm.assertInStdout("Loop {}".format(n)) - cm.assertInStdout("Shutting Down") - - def test_out_of_order(self): - # This demonstrates that we notice out-of-order IO - with self.assertRaisesRegexp(AssertionError, "Loop 2 not found"): - - with assertSequentialStdout(self.proc_output, dut_process) as cm: - cm.assertInStdout("Loop 1") - cm.assertInStdout("Loop 3") - cm.assertInStdout("Loop 2") # This should raise diff --git a/apex_rostest/apex_launchtest/package.xml b/apex_rostest/apex_launchtest/package.xml deleted file mode 100644 index b14f7631..00000000 --- a/apex_rostest/apex_launchtest/package.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - apex_launchtest - 0.2.0 - A package providing a ROS2 integration test framework. - Pete Baughman - Apache License 2.0 - - launch - - ament_copyright - ament_flake8 - ament_index_python - ament_pep257 - python3-pytest - - - ament_python - - - diff --git a/apex_rostest/apex_launchtest/pytest.ini b/apex_rostest/apex_launchtest/pytest.ini deleted file mode 100644 index 0535da1e..00000000 --- a/apex_rostest/apex_launchtest/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -# Set testpaths, otherwise pytest finds 'tests' in the examples directory -testpaths = test diff --git a/apex_rostest/apex_launchtest/resource/apex_launchtest b/apex_rostest/apex_launchtest/resource/apex_launchtest deleted file mode 100644 index e69de29b..00000000 diff --git a/apex_rostest/apex_launchtest/scripts/apex_launchtest b/apex_rostest/apex_launchtest/scripts/apex_launchtest deleted file mode 100755 index bf887d44..00000000 --- a/apex_rostest/apex_launchtest/scripts/apex_launchtest +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 - -from apex_launchtest.apex_launchtest_main import apex_launchtest_main - -apex_launchtest_main() diff --git a/apex_rostest/apex_launchtest/setup.cfg b/apex_rostest/apex_launchtest/setup.cfg deleted file mode 100644 index 58fe057a..00000000 --- a/apex_rostest/apex_launchtest/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[coverage:run] - # This will let coverage find files with 0% coverage (not hit by tests at all) - source = . - omit = setup.py diff --git a/apex_rostest/apex_launchtest/setup.py b/apex_rostest/apex_launchtest/setup.py deleted file mode 100644 index 9122b1be..00000000 --- a/apex_rostest/apex_launchtest/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup -import glob - -package_name = 'apex_launchtest' - -setup( - name=package_name, - version='0.1', - description='Apex integration test runner and utilities', - - author='Pete Baughman', - author_email='pete.baughman@apex.ai', - - data_files=[ - ('share/ament_index/resource_index/packages', ['resource/apex_launchtest']), - ('lib/' + package_name, glob.glob('example_processes/**')), - ('share/' + package_name + '/examples', glob.glob('examples/[!_]**')), - ('bin', ['scripts/apex_launchtest']), - ], - packages=[ - 'apex_launchtest', - 'apex_launchtest.asserts', - 'apex_launchtest.event_handlers', - 'apex_launchtest.util', - ], - tests_require=["pytest"], - zip_safe=True, -) diff --git a/apex_rostest/apex_launchtest/test/test_apex_runner_validation.py b/apex_rostest/apex_launchtest/test/test_apex_runner_validation.py deleted file mode 100644 index 998464c4..00000000 --- a/apex_rostest/apex_launchtest/test/test_apex_runner_validation.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2019 Apex.AI, 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 unittest - -from apex_launchtest.apex_runner import ApexRunner - - -class TestApexRunnerValidation(unittest.TestCase): - - def test_catches_bad_signature(self): - - dut = ApexRunner( - gen_launch_description_fn=lambda: None, - test_module=None - ) - - with self.assertRaises(TypeError): - dut.validate() - - dut = ApexRunner( - gen_launch_description_fn=lambda fn: None, - test_module=None - ) - - dut.validate() diff --git a/apex_rostest/apex_launchtest/test/test_assert_exit_codes.py b/apex_rostest/apex_launchtest/test/test_assert_exit_codes.py deleted file mode 100644 index 2cd1c5d9..00000000 --- a/apex_rostest/apex_launchtest/test/test_assert_exit_codes.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2019 Apex.AI, 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 unittest - -from launch.events.process import ProcessExited - -from apex_launchtest import ProcInfoHandler -from apex_launchtest.asserts import assertExitCodes - - -class TestExitCodes(unittest.TestCase): - - def setUp(self): - self.dummy_proc_info = ProcInfoHandler() - - for n in range(4): - proc_data = ProcessExited( - action=object(), - name="process_{}".format(n), - cmd=["process"], - pid=n, - returncode=0, - cwd=None, - env=None, - ) - self.dummy_proc_info.append(proc_data) - - def test_assert_exit_codes_no_error(self): - assertExitCodes(self.dummy_proc_info) - - def test_assert_exit_codes_notices_error(self): - self.dummy_proc_info.append( - ProcessExited( - action=object(), - name="test_process_1", - cmd=["test_process"], - pid=10, - returncode=1, - cwd=None, - env=None, - ) - ) - - with self.assertRaises(AssertionError) as cm: - assertExitCodes(self.dummy_proc_info) - - # Check that the process name made it into the error message - self.assertIn("test_process_1", str(cm.exception)) - - def test_assert_exit_code_allows_specific_codes(self): - self.dummy_proc_info.append( - ProcessExited( - action=object(), - name="test_process_1", - cmd=["test_process"], - pid=10, - returncode=131, - cwd=None, - env=None, - ) - ) - - assertExitCodes(self.dummy_proc_info, allowable_exit_codes=[0, 131]) diff --git a/apex_rostest/apex_launchtest/test/test_copyright.py b/apex_rostest/apex_launchtest/test/test_copyright.py deleted file mode 100644 index 0f28dd65..00000000 --- a/apex_rostest/apex_launchtest/test/test_copyright.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2015 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. - -from ament_copyright.main import main - - -def test_copyright(): - rc = main(argv=[]) - assert rc == 0, 'Found errors' diff --git a/apex_rostest/apex_launchtest/test/test_examples.py b/apex_rostest/apex_launchtest/test/test_examples.py deleted file mode 100644 index c2ab4845..00000000 --- a/apex_rostest/apex_launchtest/test/test_examples.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2019 Apex.AI, 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 glob -import os -import subprocess - -import pytest - -import ament_index_python - - -testdata = glob.glob( - os.path.join( - ament_index_python.get_package_share_directory('apex_launchtest'), - 'examples', - '*.test.py' - ) -) - - -# This test will automatically run for any *.test.py file in the examples folder and expect -# it to pass -@pytest.mark.parametrize("example_path", testdata, ids=[os.path.basename(d) for d in testdata]) -def test_examples(example_path): - - proc = ['apex_launchtest', example_path] - - # The args.test.py example is a little special - it is required to run with args - # or else it will fail. Hopefully this is the only example we need to special-case - if 'args.test.py' in example_path: - proc.append('dut_arg:=foobarbaz') - - assert 0 == subprocess.run(args=proc).returncode diff --git a/apex_rostest/apex_launchtest/test/test_flake8.py b/apex_rostest/apex_launchtest/test/test_flake8.py deleted file mode 100644 index 29bf4a41..00000000 --- a/apex_rostest/apex_launchtest/test/test_flake8.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2016 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. - -from ament_flake8.main import main - - -def test_flake8(): - rc = main(argv=[]) - assert rc == 0, 'Found code style errors / warnings' diff --git a/apex_rostest/apex_launchtest/test/test_io_handler_and_assertions.py b/apex_rostest/apex_launchtest/test/test_io_handler_and_assertions.py deleted file mode 100644 index 864dc39d..00000000 --- a/apex_rostest/apex_launchtest/test/test_io_handler_and_assertions.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import unittest - -import ament_index_python -import launch -from launch.actions import RegisterEventHandler -from launch.event_handlers import OnProcessIO - -from apex_launchtest import ActiveIoHandler -from apex_launchtest.asserts import assertInStdout -from apex_launchtest.asserts import NO_CMD_ARGS - - -TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'terminating_proc' -) - - -class TestIoHandlerAndAssertions(unittest.TestCase): - - EXPECTED_TEXT = "Ready" # Expected to be in every test run - NOT_FOUND_TEXT = "Zazzlefraz" # Not expected to be in the output anywhere - - @classmethod - def setUpClass(cls): - # It's easier to actually capture some IO from the launch system than it is to fake it - # but it takes a few seconds. We'll do it once and run tests on the same captured - # IO - - proc_env = os.environ.copy() - proc_env["PYTHONUNBUFFERED"] = "1" - - cls.proc_output = ActiveIoHandler() - - cls.proc_1 = launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH], - env=proc_env - ) - - # This process should be distinguishable by its cmd line args - cls.proc_2 = launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH, '--extra'], - env=proc_env - ) - - # This process should be distinguishable by its different name - cls.proc_3 = launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH, 'node:=different_name'], - env=proc_env - ) - - launch_description = launch.LaunchDescription([ - cls.proc_1, - cls.proc_2, - cls.proc_3, - # This plumbs all the output to our IoHandler just like the ApexRunner does - RegisterEventHandler( - OnProcessIO( - on_stdout=cls.proc_output.append, - on_stderr=cls.proc_output.append, - ) - ) - ]) - - launch_service = launch.LaunchService() - launch_service.include_launch_description(launch_description) - launch_service.run() - - def test_all_processes_had_io(self): - # Should have three processes - self.assertEqual(3, len(self.proc_output.processes())) - - def test_only_one_process_had_arguments(self): - text_lines = [t.text.decode() for t in self.proc_output] - print("All text: {}".format(text_lines)) - - matches = [t for t in text_lines if "Called with arguments" in t] - print("Called with arguments: {}".format(matches)) - - # Two process have args, because thats how process names are passed down - self.assertEqual(2, len(matches)) - - matches_extra = [t for t in matches if "--extra" in t] - self.assertEqual(1, len(matches_extra)) - - matches_proc_name = [t for t in matches if "node:=different_name" in t] - self.assertEqual(1, len(matches_proc_name)) - - def test_assert_wait_for_returns_immediately(self): - # If the output has already been seen, ensure that assertWaitsFor returns right away - self.proc_output.assertWaitFor("Starting Up", timeout=1) - - def test_EXPECTED_TEXT_is_present(self): - # Sanity check - makes sure the EXPECTED_TEXT is somewhere in the test run - text_lines = [t.text.decode() for t in self.proc_output] - contains_ready = [self.EXPECTED_TEXT in t for t in text_lines] - self.assertTrue(any(contains_ready)) - - def test_process_names(self): - self.assertIn("terminating_proc-1", self.proc_output.process_names()) - self.assertIn("terminating_proc-2", self.proc_output.process_names()) - self.assertIn("terminating_proc-3", self.proc_output.process_names()) - - def test_processes(self): - self.assertIn(self.proc_1, self.proc_output.processes()) - self.assertIn(self.proc_2, self.proc_output.processes()) - self.assertIn(self.proc_3, self.proc_output.processes()) - - # ---------- Tests for assertInStdout below this line ---------- - def test_assertInStdout_notices_no_matching_proc(self): - with self.assertRaisesRegex(Exception, "Did not find any process") as cm: - assertInStdout(self.proc_output, self.EXPECTED_TEXT, "bad_proc_name") - - print(cm.exception) - - # Make sure the assertion method lists the names of the process it does have: - self.assertIn("terminating_proc-1", str(cm.exception)) - self.assertIn("terminating_proc-2", str(cm.exception)) - self.assertIn("terminating_proc-3", str(cm.exception)) - - def test_assertInStdout_notices_too_many_matching_procs(self): - with self.assertRaisesRegex(Exception, "Found multiple processes") as cm: - assertInStdout(self.proc_output, self.EXPECTED_TEXT, "terminating_proc") - - # Make sure the assertion method lists the names of the duplicate procs: - self.assertIn("terminating_proc-1", str(cm.exception)) - self.assertIn("terminating_proc-2", str(cm.exception)) - self.assertIn("terminating_proc-3", str(cm.exception)) - - def test_strict_proc_matching_false(self): - assertInStdout( - self.proc_output, - self.EXPECTED_TEXT, - "terminating_proc", - strict_proc_matching=False - ) - - def test_arguments_disambiguate_processes(self): - txt = self.EXPECTED_TEXT - assertInStdout(self.proc_output, txt, "terminating_proc", "--extra") - assertInStdout(self.proc_output, txt, "terminating_proc", "node:=different_name") - assertInStdout(self.proc_output, txt, "terminating_proc", NO_CMD_ARGS) - - def test_asserts_on_missing_text(self): - with self.assertRaisesRegex(AssertionError, self.NOT_FOUND_TEXT): - assertInStdout(self.proc_output, self.NOT_FOUND_TEXT, "terminating", NO_CMD_ARGS) - - def test_asserts_on_missing_text_by_proc(self): - with self.assertRaisesRegex(AssertionError, self.NOT_FOUND_TEXT): - assertInStdout(self.proc_output, self.NOT_FOUND_TEXT, self.proc_2) diff --git a/apex_rostest/apex_launchtest/test/test_pep257.py b/apex_rostest/apex_launchtest/test/test_pep257.py deleted file mode 100644 index a224c250..00000000 --- a/apex_rostest/apex_launchtest/test/test_pep257.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2015 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. - -from ament_pep257.main import main - - -def test_pep257(): - rc = main(argv=[]) - assert rc == 0, 'Found docblock style errors' diff --git a/apex_rostest/apex_launchtest/test/test_print_arguments.py b/apex_rostest/apex_launchtest/test/test_print_arguments.py deleted file mode 100644 index df3ff5aa..00000000 --- a/apex_rostest/apex_launchtest/test/test_print_arguments.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import subprocess - -import ament_index_python - - -# Run the 'args.test.py' example with --show-args and verify the arguments are printed -# but the test does not run -def test_print_args(): - - testpath = os.path.join( - ament_index_python.get_package_share_directory('apex_launchtest'), - 'examples', - 'args.test.py', - ) - - completed_process = subprocess.run( - args=[ - 'apex_launchtest', - testpath, - '--show-args', - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - assert 0 == completed_process.returncode - # Take a look at examples/args.test.py to see where this expected output comes from - assert 'dut_arg' in completed_process.stdout.decode() - assert 'Passed to the terminating process' in completed_process.stdout.decode() - - -def test_no_args_to_print(): - - testpath = os.path.join( - ament_index_python.get_package_share_directory('apex_launchtest'), - 'examples', - 'good_proc.test.py', - ) - - completed_process = subprocess.run( - args=[ - 'apex_launchtest', - testpath, - '--show-args', - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - assert 0 == completed_process.returncode - assert 'No arguments.' in completed_process.stdout.decode() diff --git a/apex_rostest/apex_launchtest/test/test_ready_aggregator.py b/apex_rostest/apex_launchtest/test/test_ready_aggregator.py deleted file mode 100644 index 0b150b9b..00000000 --- a/apex_rostest/apex_launchtest/test/test_ready_aggregator.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -from apex_launchtest import ReadyAggregator - -import unittest - - -class TestReadyAggregator(unittest.TestCase): - - def setUp(self): - self.called = 0 - - def parent_ready_fn(self): - self.called += 1 - - def test_aggregate_one(self): - dut = ReadyAggregator(self.parent_ready_fn, 1) - - self.assertEqual(self.called, 0) - dut.ready_fn() - self.assertEqual(self.called, 1) - - # Make sure subsequent calls don't trigger the parent function - dut.ready_fn() - self.assertEqual(self.called, 1) - - def test_aggregate_multiple(self): - NUM_CALLS = 10 # Maybe make this random? Probably not worth the effort - - dut = ReadyAggregator(self.parent_ready_fn, NUM_CALLS) - - self.assertEqual(self.called, 0) - - for _ in range(9): - dut.ready_fn() - - self.assertEqual(self.called, 0) - dut.ready_fn() - self.assertEqual(self.called, 1) - - # Make sure subsequent calls don't trigger the parent function - dut.ready_fn() - self.assertEqual(self.called, 1) diff --git a/apex_rostest/apex_launchtest/test/test_runner_results.py b/apex_rostest/apex_launchtest/test/test_runner_results.py deleted file mode 100644 index 9af94431..00000000 --- a/apex_rostest/apex_launchtest/test/test_runner_results.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright 2018 Apex.AI, 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 imp -import mock -import os - -import ament_index_python -import launch -import launch.actions - -from apex_launchtest.apex_runner import ApexRunner - - -# Run tests on processes that die early with an exit code and make sure the results returned -# indicate failure -def test_dut_that_shuts_down(capsys): - - def generate_test_description(ready_fn): - TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'terminating_proc' - ) - - return launch.LaunchDescription([ - launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH] - ), - - launch.actions.OpaqueFunction(function=lambda context: ready_fn()), - ]) - - with mock.patch('apex_launchtest.apex_runner.ApexRunner._run_test'): - runner = ApexRunner( - gen_launch_description_fn=generate_test_description, - test_module=None - ) - - pre_result, post_result = runner.run() - - assert not pre_result.wasSuccessful() - assert not post_result.wasSuccessful() - - # This is the negative version of the test below. If no exit code, no extra output - # is generated - out, err = capsys.readouterr() - assert "Starting Up" not in out - - -def test_dut_that_has_exception(capsys): - # This is the same as above, but we also want to check we get extra output from processes - # that had an exit code - - def generate_test_description(ready_fn): - TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'terminating_proc' - ) - - EXIT_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'exit_code_proc' - ) - - return launch.LaunchDescription([ - launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH, '--exception'] - ), - - # This process makes sure we can handle processes that exit with a code but don't - # generate any output. This is a regression test for an issue fixed in PR31 - launch.actions.ExecuteProcess( - cmd=[EXIT_PROC_PATH, '--silent'] - ), - - launch.actions.OpaqueFunction(function=lambda context: ready_fn()), - ]) - - with mock.patch('apex_launchtest.apex_runner.ApexRunner._run_test'): - runner = ApexRunner( - gen_launch_description_fn=generate_test_description, - test_module=None - ) - - pre_result, post_result = runner.run() - - assert not pre_result.wasSuccessful() - assert not post_result.wasSuccessful() - - # Make sure some information about WHY the process died shows up in the output - out, err = capsys.readouterr() - assert "Starting Up" in out - assert "Process had a pretend error" in out # This is the exception text from exception_node - - -# Run some known good tests to check the nominal-good test path -def test_nominally_good_dut(): - - # The following is test setup nonsense to turn a string into a python module that can be - # passed to the apex runner. You can skip over this. It does not add to your understanding - # of the test. - test_code = """ -import unittest -from apex_launchtest import post_shutdown_test - -class PreTest(unittest.TestCase): - def test_pre_ok(self): - pass - -@post_shutdown_test() -class PostTest(unittest.TestCase): - def test_post_ok(self): - pass - """ - module = imp.new_module("test_module") - exec(test_code, module.__dict__) - - # Here's the actual 'test' part of the test: - TEST_PROC_PATH = os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'good_proc' - ) - - def generate_test_description(ready_fn): - return launch.LaunchDescription([ - launch.actions.ExecuteProcess( - cmd=[TEST_PROC_PATH] - ), - - launch.actions.OpaqueFunction(function=lambda context: ready_fn()), - ]) - - runner = ApexRunner( - gen_launch_description_fn=generate_test_description, - test_module=module - ) - - pre_result, post_result = runner.run() - - assert pre_result.wasSuccessful() - - assert pre_result.wasSuccessful() diff --git a/apex_rostest/apex_launchtest/test/test_sequential_output_checker.py b/apex_rostest/apex_launchtest/test/test_sequential_output_checker.py deleted file mode 100644 index ce8cde2e..00000000 --- a/apex_rostest/apex_launchtest/test/test_sequential_output_checker.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2019 Apex.AI, 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 unittest - -from apex_launchtest.asserts import SequentialTextChecker - - -class TestAssertSequentialStdout(unittest.TestCase): - - def setUp(self): - self.to_check = [ - "output 10", - "output 15", - "output 20", - "multi-line 1\nmulti-line 2\nmulti-line 3", - "aaaaa bbbbb ccccc ddddd eeeee fffff ggggg hhhhh iiiii jjjjj", # long line - "xxxxx yyyyy\nsome dummy text\nzzzzz", # mix of long line and multi-line - "output 20", - "!@#$%^&*()", # Help find off by one errors in the substring logic - ] - - self.dut = SequentialTextChecker(self.to_check) - - def test_good_sequential_output(self): - - for output in self.to_check: - self.dut.assertInStdout(output) - - with self.assertRaises(AssertionError): - # This should assert because we've already moved past "output 10" - self.dut.assertInStdout(self.to_check[0]) - - def test_non_matching_output_does_not_advance_state(self): - # Make sure we can match correct output even after failing to match something - - with self.assertRaises(AssertionError): - self.dut.assertInStdout("bad output not found") - - self.test_good_sequential_output() - - def test_multi_line_find(self): - self.dut.assertInStdout("multi-line 1") - self.dut.assertInStdout("multi-line 2") - self.dut.assertInStdout("multi-line 3") - - with self.assertRaises(AssertionError): - self.dut.assertInStdout("multi-line 1") - - def test_long_line_find(self): - self.dut.assertInStdout("ccccc") - self.dut.assertInStdout("ddddd") - self.dut.assertInStdout("eeeee") - - with self.assertRaises(AssertionError): - self.dut.assertInStdout("aaaaa") - - def test_duplicates_advances_state(self): - self.dut.assertInStdout("output 20") - self.dut.assertInStdout("output 20") - - with self.assertRaises(AssertionError): - self.dut.assertInStdout("multi-line 1") - - def test_individual_character_find(self): - self.dut.assertInStdout("!") - self.dut.assertInStdout("@") - self.dut.assertInStdout("#") - self.dut.assertInStdout("$") - - # Skip ahead - self.dut.assertInStdout("*") - self.dut.assertInStdout("(") - - # Check the same character - with self.assertRaises(AssertionError): - self.dut.assertInStdout("(") - - def test_mixed_multi_line(self): - self.dut.assertInStdout("xxxxx") - self.dut.assertInStdout("some dummy text") - - with self.assertRaises(AssertionError): - self.dut.assertInStdout("yyyyy") diff --git a/apex_rostest/apex_launchtest/test/test_stdout_ready_listener.py b/apex_rostest/apex_launchtest/test/test_stdout_ready_listener.py deleted file mode 100644 index 23a8ce30..00000000 --- a/apex_rostest/apex_launchtest/test/test_stdout_ready_listener.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import unittest - -import ament_index_python -import launch -import launch.actions - -from apex_launchtest.event_handlers import StdoutReadyListener -from apex_launchtest.util import KeepAliveProc - - -class TestStdoutReadyListener(unittest.TestCase): - - def setUp(self): - # Set up a launch description for the tests to use - proc_env = os.environ.copy() - proc_env["PYTHONUNBUFFERED"] = "1" - - self.terminating_proc = launch.actions.ExecuteProcess( - cmd=[ - os.path.join( - ament_index_python.get_package_prefix('apex_launchtest'), - 'lib/apex_launchtest', - 'terminating_proc', - ) - ], - env=proc_env - ) - - self.launch_description = launch.LaunchDescription([ - self.terminating_proc, - ]) - - def test_wait_for_ready(self): - data = [] - - self.launch_description.add_entity( - launch.actions.RegisterEventHandler( - StdoutReadyListener( - target_action=self.terminating_proc, - ready_txt="Ready", - actions=[ - launch.actions.OpaqueFunction(function=lambda context: data.append('ok')) - ] - ) - ) - ) - - launch_service = launch.LaunchService() - launch_service.include_launch_description(self.launch_description) - launch_service.run() - - # If the StdoutReadyListener worked, we should see 'ok' in the data - self.assertIn('ok', data) - - def test_wait_for_wrong_process(self): - data = [] - - self.launch_description.add_entity( - launch.actions.RegisterEventHandler( - StdoutReadyListener( - target_action=KeepAliveProc(), # We never launched this process - ready_txt="Ready", - actions=[ - launch.actions.OpaqueFunction(function=lambda context: data.append('ok')) - ] - ) - ) - ) - - launch_service = launch.LaunchService() - launch_service.include_launch_description(self.launch_description) - launch_service.run() - - # We should not get confused by output from another proc - self.assertNotIn('ok', data) - - def test_wait_for_wrong_message(self): - data = [] - - self.launch_description.add_entity( - launch.actions.RegisterEventHandler( - StdoutReadyListener( - target_action=self.terminating_proc, - ready_txt="not_ready", - actions=[ - launch.actions.OpaqueFunction(function=lambda context: data.append('ok')) - ] - ) - ) - ) - - launch_service = launch.LaunchService() - launch_service.include_launch_description(self.launch_description) - launch_service.run() - - # We should not get confused by output that doesn't match the ready_txt - self.assertNotIn('ok', data) diff --git a/apex_rostest/apex_launchtest/test/test_xml_output.py b/apex_rostest/apex_launchtest/test/test_xml_output.py deleted file mode 100644 index 2ecdb017..00000000 --- a/apex_rostest/apex_launchtest/test/test_xml_output.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2019 Apex.AI, 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 os -import subprocess -import tempfile -import unittest -import xml.etree.ElementTree as ET - -import ament_index_python - -from apex_launchtest.test_result import FailResult -from apex_launchtest.junitxml import unittestResultsToXml - - -class TestGoodXmlOutput(unittest.TestCase): - - @classmethod - def setUpClass(cls): - # For performance, we'll run the test once and generate the XML output, then - # have multiple test cases assert on it - - cls.tmpdir = tempfile.TemporaryDirectory() - cls.xml_file = os.path.join(cls.tmpdir.name, 'junit.xml') - - path = os.path.join( - ament_index_python.get_package_share_directory('apex_launchtest'), - 'examples', - 'good_proc.test.py' - ) - - assert 0 == subprocess.run( - args=[ - 'apex_launchtest', - path, - '--junit-xml', os.path.join(cls.tmpdir.name, 'junit.xml'), - ], - ).returncode - - @classmethod - def tearDownClass(cls): - cls.tmpdir.cleanup() - - def test_pre_and_post(self): - tree = ET.parse(self.xml_file) - root = tree.getroot() - - self.assertEqual(len(root.getchildren()), 2) - - # Expecting an element called "active_tests" and "after_shutdown_tests" - child_names = [chld.attrib['name'] for chld in root.getchildren()] - self.assertEqual(set(child_names), set(['active_tests', 'after_shutdown_tests'])) - - -class TestXmlFunctions(unittest.TestCase): - # This are closer to unit tests - just call the functions that generate XML - - def test_fail_results_serialize(self): - xml_tree = unittestResultsToXml( - name="fail_xml", - test_results={ - "active_tests": FailResult() - } - ) - - # Simple sanity check - see that there's a child element called active_tests - child_names = [chld.attrib['name'] for chld in xml_tree.getroot().getchildren()] - self.assertEqual(set(child_names), set(['active_tests'])) diff --git a/apex_rostest/apex_launchtest_cmake/CMakeLists.txt b/apex_rostest/apex_launchtest_cmake/CMakeLists.txt deleted file mode 100644 index 973444a9..00000000 --- a/apex_rostest/apex_launchtest_cmake/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(apex_launchtest_cmake NONE) - -find_package(ament_cmake_core REQUIRED) - -ament_package( - CONFIG_EXTRAS "${PROJECT_NAME}-extras.cmake" -) - -install( - DIRECTORY cmake - DESTINATION share/${PROJECT_NAME} -) - -if(BUILD_TESTING) - find_package(ament_cmake_pytest REQUIRED) - ament_add_pytest_test(apex_launchtest_cmake_pytests test) - - include(cmake/add_apex_launchtest.cmake) - - ament_index_has_resource(APEX_LAUNCHTEST_INSTALL_PREFIX packages apex_launchtest) - if(NOT APEX_LAUNCHTEST_INSTALL_PREFIX) - message(FATAL_ERROR "apex_launchtest package not found") - endif() - - # Test argument passing. This test won't pass unless you give it an argument - add_apex_launchtest( - "${APEX_LAUNCHTEST_INSTALL_PREFIX}/share/apex_launchtest/examples/args.test.py" - ARGS "dut_node_arg:=--anything" - ) -endif() diff --git a/apex_rostest/apex_launchtest_cmake/apex_launchtest_cmake-extras.cmake b/apex_rostest/apex_launchtest_cmake/apex_launchtest_cmake-extras.cmake deleted file mode 100644 index eca74b8c..00000000 --- a/apex_rostest/apex_launchtest_cmake/apex_launchtest_cmake-extras.cmake +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -find_package(ament_cmake_test REQUIRED) - -include("${apex_launchtest_cmake_DIR}/add_apex_launchtest.cmake") diff --git a/apex_rostest/apex_launchtest_cmake/cmake/add_apex_launchtest.cmake b/apex_rostest/apex_launchtest_cmake/cmake/add_apex_launchtest.cmake deleted file mode 100644 index 678bacf6..00000000 --- a/apex_rostest/apex_launchtest_cmake/cmake/add_apex_launchtest.cmake +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2019 Apex.AI, 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. - -# This file contains modified code from the following open source projects -# published under the licenses listed below: - -# Software License Agreement (BSD License) -# -# Copyright (c) 2008, Willow Garage, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# * Neither the name of Willow Garage, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# -# Add an apex_launchtest test -# -# :param file: The apex_launchtest test file containing the test to run -# :type file: string -# :param TIMEOUT: The test timeout in seconds -# :type TIMEOUT: integer -# :param ARGS: Launch arguments to pass to apex_launchtest -# :type ARGS: string -function(add_apex_launchtest file) - - cmake_parse_arguments(_add_apex_launchtest - "" - "TIMEOUT" - "ARGS" - ${ARGN}) - - if(NOT _add_apex_launchtest_TIMEOUT) - set(_add_apex_launchtest_TIMEOUT 60) - endif() - - set(_file_name _file_name-NOTFOUND) - if(IS_ABSOLUTE ${file}) - set(_file_name ${file}) - else() - find_file(_file_name ${file} - PATHS ${CMAKE_CURRENT_SOURCE_DIR} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH) - if(NOT _file_name) - message(FATAL_ERROR "Can't find rostest file \"${file}\"") - endif() - endif() - - # strip PROJECT_SOURCE_DIR and PROJECT_BINARY_DIR from absolute filename to get unique test name (as rostest does it internally) - set(testname ${_file_name}) - rostest__strip_prefix(testname "${PROJECT_SOURCE_DIR}/") - rostest__strip_prefix(testname "${PROJECT_BINARY_DIR}/") - string(REPLACE "/" "_" testname ${testname}) - set(result_file "${AMENT_TEST_RESULTS_DIR}/${PROJECT_NAME}/${testname}.xunit.xml") - - find_program(apex_launchtest_BIN NAMES "apex_launchtest") - if(NOT apex_launchtest_BIN) - message(FATAL_ERROR "apex_add_rostest cmake could not find apex_launchtest script") - endif() - - set(cmd - "${apex_launchtest_BIN}" - "${_file_name}" - "${_add_apex_launchtest_ARGS}" - "--junit-xml=${result_file}" - ) - ament_add_test( - "${testname}" - COMMAND ${cmd} - OUTPUT_FILE "${CMAKE_BINARY_DIR}/apex_launchtest/CHANGEME.txt" - RESULT_FILE "${result_file}" - TIMEOUT "${_add_apex_launchtest_TIMEOUT}" - ) - -endfunction() - -macro(rostest__strip_prefix var prefix) - string(LENGTH ${prefix} prefix_length) - string(LENGTH ${${var}} var_length) - if(${var_length} GREATER ${prefix_length}) - string(SUBSTRING "${${var}}" 0 ${prefix_length} var_prefix) - if("${var_prefix}" STREQUAL "${prefix}") - # passing length -1 does not work for CMake < 2.8.5 - # http://public.kitware.com/Bug/view.php?id=10740 - string(LENGTH "${${var}}" _rest) - math(EXPR _rest "${_rest} - ${prefix_length}") - string(SUBSTRING "${${var}}" ${prefix_length} ${_rest} ${var}) - endif() - endif() -endmacro() diff --git a/apex_rostest/apex_launchtest_cmake/package.xml b/apex_rostest/apex_launchtest_cmake/package.xml deleted file mode 100644 index fda02c8b..00000000 --- a/apex_rostest/apex_launchtest_cmake/package.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - apex_launchtest_cmake - 1.0.0 - A package providing cmake functions for running apex_launchtest from the build. - Pete Baughman - Apache License 2.0 - - ament_cmake_core - - ament_cmake_test - apex_launchtest - - ament_copyright - ament_cmake_pytest - - - ament_cmake - - - diff --git a/apex_rostest/apex_launchtest_cmake/test/test_copyright.py b/apex_rostest/apex_launchtest_cmake/test/test_copyright.py deleted file mode 100644 index 0f28dd65..00000000 --- a/apex_rostest/apex_launchtest_cmake/test/test_copyright.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2015 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. - -from ament_copyright.main import main - - -def test_copyright(): - rc = main(argv=[]) - assert rc == 0, 'Found errors' diff --git a/launch/CHANGELOG.rst b/launch/CHANGELOG.rst deleted file mode 100644 index db5ae9f6..00000000 --- a/launch/CHANGELOG.rst +++ /dev/null @@ -1,114 +0,0 @@ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Changelog for package launch -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -0.7.3 (2018-12-13) ------------------- -* Fixed deprecation warning related to collections.abc (`#158 `_) -* Contributors: William Woodall - -0.7.2 (2018-12-06) ------------------- -* Changed the signit handler os it executes the shutdown event synchronously (`#156 `_) -* Contributors: Jonathan Chapple, Steven! Ragnarök, William Woodall - -0.7.1 (2018-11-16) ------------------- -* Fixed setup.py versions (`#155 `_) -* Contributors: Steven! Ragnarök - -* Merge pull request `#158 `_ from ros2/fix_deprecation_warning - fix deprecation warning related to collections.abc -* fix deprecation warning related to collections.abc - Signed-off-by: William Woodall -* 0.7.2 -* changelogs - Signed-off-by: William Woodall -* signit executes the shutdown event synchronously (`#156 `_) - * signit executes the shutdown event synchronously - * line length - * avoid split parameter indention -* 0.7.1 -* changelogs - Signed-off-by: William Woodall -* Fix setup py versions (`#155 `_) - * Make setup.py version agree with package.xml version. - Drive-by cleanup while I'm doing other things. - * Missed it by that much. - Between creating this branch and submitting it a release happened. -* Contributors: Jonathan Chapple, Steven! Ragnarök, William Woodall - -0.7.0 (2018-11-16) ------------------- -* Fixed a bug to ensure that shutdown event is handled correctly (`#154 `_) - * There was a potential race condition in between when the shutdown event is emitted and the rest of the shutdown handling code. - * This introduces an additional await to ensure that the event is emitted before proceeding. -* Fixed example to always use shell to avoid inconsistency of time being a shell command or executable (`#150 `_) -* Added tests for class_tools module and fix is_a_subclass() (`#142 `_) -* Added tests for the utilities module (`#143 `_) -* Added 'handle_once' property for unregistering an EventHandler after one event (`#141 `_) -* Added UnregisterEventHandler action (`#110 `_) -* Changed LaunchService so that it returns ``1`` on caught exceptions from within launch (`#136 `_) -* Added ability to define and pass launch arguments to launch files (`#123 `_) - * Added self descriptions for substitutions - * Added tracebacks back to the output by default - * Added new actions for declaring launch arguments - * Added new method on LaunchDescription which gets all declared arguments within - * Added ability to pass arguments when including a launch description - * Added description for local variables used in Node action - * Added ability to show and pass launch arguments on the command line - * Added an accessor for the Condition of an Action - * Signed-off-by: William Woodall -* Added UnsetLaunchConfiguration action and tests (`#134 `_) - * Signed-off-by: William Woodall -* Added GroupAction for conditionally including other actions and scoping (`#133 `_) - * Signed-off-by: William Woodall -* Added optional name argument to ExecuteProcess (`#129 `_) - * Signed-off-by: William Woodall -* Added a new pair of actions for pushing and popping launch configurations (`#128 `_) - * Signed-off-by: William Woodall -* Contributors: Dirk Thomas, Jacob Perron, Michael Carroll, William Woodall, dhood - -0.6.0 (2018-08-20) ------------------- -* Added a way to include other Python launch files (`#122 `_) - * Signed-off-by: William Woodall -* Implemented the concept of Action conditions (`#121 `_) - * Signed-off-by: William Woodall -* Added IncludeLaunchDescription action (`#120 `_) - * fixes `#115 `_ - * Signed-off-by: William Woodall -* Contributors: William Woodall - -0.5.2 (2018-07-17) ------------------- -* Made a change to avoid reentrancy of signal handlers (`#99 `_) -* Ignored warning for builtins A003 (`#100 `_) -* Fixed exception when launch process with environment variables (`#96 `_) -* Contributors: Shane Loretz, William Woodall, dhood - -0.5.1 (2018-06-27) ------------------- -* Changed the behavior when signaling SIGINT to subprocesses on Windows, where it now does SIGTERM instead, because SIGINT causes a ValueError about SIGINT being an unsupported signal number. (`#94 `_) -* Fixed a bug by avoiding reentrancy in the SIGINT signal handler. (`#92 `_) -* Various Windows fixes. (`#87 `_) - * LaunchService.run() now returns non-0 when there are exceptions in coroutines. - * Updated ``launch_counters.py`` example for Windows. - * Fixed a bug that would cause mismatched asyncio loops in some futures. - * Addressed the fact that ``signal.SIGKILL`` doesn’t exist on Windows, so emulate it in our Event. - * Fixed an issue that resulted in spurious asyncio errors in LaunchService test. -* Contributors: William Woodall, dhood - -0.5.0 (2018-06-19) ------------------- -* Fixed a bug where unclosed asyncio loops caused a traceback on the terminal on exit, but only in Python 3.5 (`#85 `_) -* Changed to use variable typing in comments to support python 3.5 (`#81 `_) -* New launch API (`#74 `_) - * See pull request for more details and links to architecture documentation and the design doc. -* Moved launch source files into launch.legacy namespace (`#73 `_) - * This was in preparation for the new launch API. -* [for launch.legacy] fixed a flake8 warning (`#72 `_) -* [for launch.legacy] set zip_safe to avoid warning during installation (`#71 `_) -* [for launch.legacy] Fix hang on keyboard interrupt (`#69 `_) - * When keyboard interrupt exception occurs loop.run_forever is called. But there is no loop.stop call. This causes a hang. -* Contributors: Devin, Dirk Thomas, William Woodall, dhood diff --git a/launch/doc/.gitignore b/launch/doc/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/launch/doc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/launch/doc/Makefile b/launch/doc/Makefile deleted file mode 100644 index 8ff07eb9..00000000 --- a/launch/doc/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = launch -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/launch/doc/make.bat b/launch/doc/make.bat deleted file mode 100644 index b71a4a2b..00000000 --- a/launch/doc/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build -set SPHINXPROJ=launch - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/launch/doc/source/architecture.rst b/launch/doc/source/architecture.rst deleted file mode 100644 index ae6f3f1c..00000000 --- a/launch/doc/source/architecture.rst +++ /dev/null @@ -1,208 +0,0 @@ -Architecture of `launch` -======================== - -`launch` is designed to provide core features like describing actions (e.g. executing a process or including another launch description), generating events, introspecting launch descriptions, and executing launch descriptions. -At the same time, it provides extension points so that the set of things that these core features can operate on, or integrate with, can be expanded with additional packages. - -Launch Entities and Launch Descriptions ---------------------------------------- - -The main object in `launch` is the :class:`launch.LaunchDescriptionEntity`, from which other entities that are "launched" inherit. -This class, or more specifically classes derived from this class, are responsible for capturing the system architect's (a.k.a. the user's) intent for how the system should be launched, as well as how `launch` itself should react to asynchronous events in the system during launch. -A launch description entity has its :meth:`launch.LaunchDescriptionEntity.visit` method called during "launching", and has any of the "describe" methods called during "introspection". -It may also provide a :class:`asyncio.Future` with the :meth:`launch.LaunchDescriptionEntity.get_asyncio_future` method, if it has on-going asynchronous activity after returning from visit. - -When visited, entities may yield additional entities to be visited, and this pattern is used from the "root" of the launch, where a special entity called :class:`launch.LaunchDescription` is provided to start the launch process. - -The :class:`launch.LaunchDescription` class encapsulates the intent of the user as a list of discrete :class:`launch.Action`'s, which are also derived from :class:`launch.LaunchDescriptionEntity`. -As "launch description entities" themselves, these "actions" can either be introspected for analysis without performing the side effects, or the actions can be executed, usually in response to an event in the launch system. - -Additionally, launch descriptions, and the actions that they contain, can have references to :class:`launch.Substitution`'s within them. -These substitutions are things that can be evaluated during launch and can be used to do various things like: get a launch configuration, get an environment variable, or evaluate arbitrary Python expressions. - -Launch descriptions, and the actions contained therein, can either be introspected directly or launched by a :class:`launch.LaunchService`. -A launch service is a long running activity that handles the event loop and dispatches actions. - -Actions -------- - -The aforementioned actions allow the user to express various intentions, and the set of available actions to the user can also be extended by other packages, allowing for domain specific actions. - -Actions can have direct side effects (e.g. run a process or set a configuration variable) and as well they can yield additional actions. -The latter can be used to create "syntactic sugar" actions which simply yield more verbose actions. - -Actions may also have arguments, which can affect the behavior of the actions. -These arguments are where :class:`launch.Substitution`'s can be used to provide more flexibility when describing reusable launch descriptions. - -Basic Actions -^^^^^^^^^^^^^ - -`launch` provides the foundational actions on which other more sophisticated actions may be built. -This is a non-exhaustive list of actions that `launch` may provide: - -- :class:`launch.actions.IncludeLaunchDescription` - - - This action will include another launch description as if it had been copy-pasted to the location of the include action. - -- :class:`launch.actions.SetLaunchConfiguration` - - - This action will set a :class:`launch.LaunchConfiguration` to a specified value, creating it if it doesn't already exist. - - These launch configurations can be accessed by any action via a substitution, but are scoped by default. - -- :class:`launch.actions.DeclareLaunchDescriptionArgument` - - - This action will declare a launch description argument, which can have a name, default value, and documentation. - - The argument will be exposed via a command line option for a root launch description, or as action configurations to the include launch description action for the included launch description. - -- :class:`launch.actions.SetEnvironmentVariable` - - - This action will set an environment variable by name. - -- :class:`launch.actions.GroupAction` - - - This action will yield other actions, but can be associated with conditionals (allowing you to use the conditional on the group action rather than on each sub-action individually) and can optionally scope the launch configurations. - -- :class:`launch.actions.TimerAction` - - - This action will yield other actions after a period of time has passed without being canceled. - -- :class:`launch.actions.ExecuteProcess` - - - This action will execute a process given its path and arguments, and optionally other things like working directory or environment variables. - -- :class:`launch.actions.RegisterEventHandler` - - - This action will register an :class:`launch.EventHandler` class, which takes a user defined lambda to handle some event. - - It could be any event, a subset of events, or one specific event. - -- :class:`launch.actions.UnregisterEventHandler` - - - This action will remove a previously registered event. - -- :class:`launch.actions.EmitEvent` - - - This action will emit an :class:`launch.Event` based class, causing all registered event handlers that match it to be called. - -- :class:`launch.actions.LogInfo`: - - - This action will log a user defined message to the logger, other variants (e.g. `LogWarn`) could also exist. - -- :class:`launch.actions.RaiseError` - - - This action will stop execution of the launch system and provide a user defined error message. - -More actions can always be defined via extension, and there may even be additional actions defined by `launch` itself, but they are more situational and would likely be built on top of the above actions anyways. - -Base Action -^^^^^^^^^^^ - -All actions need to inherit from the :class:`launch.Action` base class, so that some common interface is available to the launch system when interacting with actions defined by external packages. -Since the base action class is a first class element in a launch description it also inherits from :class:`launch.LaunchDescriptionEntity`, which is the polymorphic type used when iterating over the elements in a launch description. - -Also, the base action has a few features common to all actions, such as some introspection utilities, and the ability to be associated with a single :class:`launch.Condition`, like the :class:`launch.IfCondition` class or the :class:`launch.UnlessCondition` class. - -The action configurations are supplied when the user uses an action and can be used to pass "arguments" to the action in order to influence its behavior, e.g. this is how you would pass the path to the executable in the execute process action. - -If an action is associated with a condition, that condition is evaluated to determine if the action is executed or not. -Even if the associated action evaluates to false the action will be available for introspection. - -Substitutions -------------- - -A substitution is something that cannot, or should not, be evaluated until it's time to execute the launch description that they are used in. -There are many possible variations of a substitution, but here are some of the core ones implemented by `launch` (all of which inherit from :class:`launch.Substitution`): - -- :class:`launch.substitutions.Text` - - - This substitution simply returns the given string when evaluated. - - It is usually used to wrap literals in the launch description so they can be concatenated with other substitutions. - -- :class:`launch.substitutions.PythonExpression` - - - This substitution will evaluate a python expression and get the result as a string. - -- :class:`launch.substitutions.LaunchConfiguration` - - - This substitution gets a launch configuration value, as a string, by name. - -- :class:`launch.substitutions.LaunchDescriptionArgument` - - - This substitution gets the value of a launch description argument, as a string, by name. - -- :class:`launch.substitutions.LocalSubstitution` - - - This substitution gets a "local" variable out of the context. This is a mechanism that allows a "parent" action to pass information to sub actions. - - As an example, consider this pseudo code example `OnShutdown(actions=LogInfo(msg=["shutdown due to: ", LocalSubstitution(expression='event.reason')]))`, which assumes that `OnShutdown` will put the shutdown event in the locals before `LogInfo` is visited. - -- :class:`launch.substitutions.EnvironmentVariable` - - - This substitution gets an environment variable value, as a string, by name. - -- :class:`launch.substitutions.FindExecutable` - - - This substitution locates the full path to an executable on the PATH if it exists. - -The base substitution class provides some common introspection interfaces (which the specific derived substitutions may influence). - -The Launch Service ------------------- - -The launch service is responsible for processing emitted events, dispatching them to event handlers, and executing actions as needed. -The launch service offers three main services: - -- include a launch description - - - can be called from any thread - -- run event loop -- shutdown - - - cancels any running actions and event handlers - - then breaks the event loop if running - - can be called from any thread - -A typical use case would be: - -- create a launch description (programmatically or based on a markup file) -- create a launch service -- include the launch description in the launch service -- add a signal handler for SIGINT that calls shutdown on the launch service -- run the event loop on the launch service - -Additionally you could host some SOA (like REST, SOAP, ROS Services, etc...) server in another thread, which would provide a variety of services, all of which would end up including a launch description in the launch service asynchronously or calling shutdown on the launch service asynchronously. -Remember that a launch description can contain actions to register event handlers, emit events, run processes, etc. -So being able to include arbitrary launch descriptions asynchronously is the only feature you require to do most things dynamically while the launch service is running. - -Event Handlers --------------- - -Event handlers are represented with the :class:`launch.EventHandler` base class. -Event handlers define two main methods, the :meth:`launch.EventHandler.matches` method and the :meth:`launch.EventHandler.handle` method. -The matches method gets the event as input and must return `True` if the event handler matches that event, or `False` otherwise. -The handle method gets the event and launch context as input, and can optionally (in addition to any side effects) return a list of :class:`launch.LaunchDescriptionEntity` objects to be visited by the launch service. - -Event handlers do not inherit from :class:`launch.LaunchDescriptionEntity`, but can similarly be "visited" for each event processed by the launch service, seeing if `matches` returns `True` and if so following up with a call to `handle`, then visiting each of the actions returned by `handle`, depth-first. - -Extension Points ----------------- - -In order to allow customization of how `launch` is used in specific domains, extension of the core categories of features is provided. -External Python packages, through extension points, may add: - -- new actions - - - must directly or indirectly inherit from :class:`launch.Action` - -- new events - - - must directly or indirectly inherit from :class:`launch.Event` - -- new substitutions - - - must directly or indirectly inherit from :class:`launch.Substitution` - -- kinds of entities in the launch description - - - must directly or indirectly inherit from :class:`launch.LaunchDescriptionEntity` - -In the future, more traditional extensions (like with `setuptools`' `entry_point` feature) may be available via the launch service, e.g. the ability to include some extra entities and event handlers before the launch description is included. diff --git a/launch/doc/source/conf.py b/launch/doc/source/conf.py deleted file mode 100644 index ccc4e183..00000000 --- a/launch/doc/source/conf.py +++ /dev/null @@ -1,191 +0,0 @@ -# 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. - -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/stable/config - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'launch' -copyright = '2018, Open Source Robotics Foundation, Inc.' # noqa -author = 'Open Source Robotics Foundation, Inc.' - -# The short X.Y version -version = '' -# The full version, including alpha/beta/rc tags -release = '0.4.0' - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'launchdoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'launch.tex', 'launch Documentation', - 'Open Source Robotics Foundation, Inc.', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'launch', 'launch Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'launch', 'launch Documentation', - author, 'launch', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Extension configuration ------------------------------------------------- - -# -- Options for intersphinx extension --------------------------------------- - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} - -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True diff --git a/launch/doc/source/index.rst b/launch/doc/source/index.rst deleted file mode 100644 index 55fbf42c..00000000 --- a/launch/doc/source/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. launch documentation master file, created by - sphinx-quickstart on Tue May 22 18:18:17 2018. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to launch's documentation! -================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - architecture - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/launch/examples/counter.py b/launch/examples/counter.py deleted file mode 100755 index 99e6f1be..00000000 --- a/launch/examples/counter.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2015 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. - -"""Script used in examples to demonstrate different behaviors of called processes.""" - -import argparse -import signal -import sys -import time - - -def main(argv=sys.argv[1:]): - """Main.""" - parser = argparse.ArgumentParser( - description=( - 'Simple program outputting a counter. ' - 'Even values go to STDERR, odd values go to STDOUT.')) - parser.add_argument( - '--limit', - type=int, - help='The upper limit of the counter when the program terminates.') - parser.add_argument( - '--limit-return-code', - type=int, - default=0, - help='The return code when the program terminates because of reaching the limit.') - parser.add_argument( - '--sleep', - type=float, - default=1.0, - help='The time to sleep between a counter increment.') - parser.add_argument( - '--ignore-sigint', - action='store_true', - default=False, - help='Ignore SIGINT signal, and continue counting.') - parser.add_argument( - '--ignore-sigterm', - action='store_true', - default=False, - help='Ignore SIGTERM signal, and continue counting.') - - args = parser.parse_args(argv) - - if args.ignore_sigint: - print('will be ignoring SIGINT') - signal.signal(signal.SIGINT, lambda signum, frame: print('ignoring SIGINT')) - - if args.ignore_sigterm: - print('will be ignoring SIGTERM') - signal.signal(signal.SIGTERM, lambda signum, frame: print('ignoring SIGTERM')) - - counter = 1 - while True: - if args.limit is not None and counter > args.limit: - return args.limit_return_code - stream = sys.stdout if counter % 2 else sys.stderr - print('Counter: %d' % counter, file=stream) - time.sleep(args.sleep) - counter += 1 - - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/launch/examples/launch_counters.example.txt b/launch/examples/launch_counters.example.txt deleted file mode 100644 index 20d61ff6..00000000 --- a/launch/examples/launch_counters.example.txt +++ /dev/null @@ -1,84 +0,0 @@ -$ python3 ./launch_counters.py -Starting introspection of launch description... - - -├── LogInfo('Hello World!') -├── LogInfo('Is that you, ' + EnvVarSub('USER') + '?') -├── RegisterEventHandler(''): -│ └── OnProcessIO(matcher='event issubclass of ProcessIO and event.action == ExecuteProcess(0x1036299f8)', handlers={on_stdout: '.on_output at 0x103e93ae8>', on_stderr: '.on_output at 0x103e93ae8>'}) -├── Action('') -├── ExecuteProcess(cmd=[FindExecSub('whoami')], cwd=None, env=None, shell=False) -├── RegisterEventHandler(''): -│ └── OnProcessIO(matcher='event issubclass of ProcessIO and event.action == ExecuteProcess(0x1045ae2e8)', handlers={on_stdout: '. at 0x1045a48c8>'}) -├── Action('') -├── ExecuteProcess(cmd=['python3', '-u', './counter.py'], cwd=None, env=None, shell=False) -├── RegisterEventHandler(''): -│ └── OnProcessIO(matcher='event issubclass of ProcessIO and event.action == ExecuteProcess(0x1045ae4a8)', handlers={on_stdout: '.counter_output_handler at 0x1045ad048>', on_stderr: '.counter_output_handler at 0x1045ad048>'}) -├── ExecuteProcess(cmd=['python3', '-u', './counter.py', '--ignore-sigint'], cwd=None, env=None, shell=False) -├── ExecuteProcess(cmd=['python3', '-u', './counter.py', '--ignore-sigint', '--ignore-sigterm'], cwd=None, env=None, shell=False) -├── RegisterEventHandler(''): - └── OnShutdown(matcher='event issubclass of launch.events.Shutdown', handler=. at 0x1045ad158>) - -Starting launch of launch description... - -[INFO] [launch.user]: Hello World! -[INFO] [launch.user]: Is that you, william? -[INFO] [launch]: process[whoami-1]: started with pid [22435] -[INFO] [launch]: process[whoami-1]: process has finished cleanly -[INFO] [launch]: process[python3-2]: started with pid [22436] -[INFO] [launch]: process[python3-3]: started with pid [22438] -[INFO] [launch]: process[python3-4]: started with pid [22439] -[INFO] [launch.user]: whoami says you are 'william'. -[whoami-1] william -[whoami-1] 0.00 real 0.00 user 0.00 sys -[python3-3] will be ignoring SIGINT -[python3-3] Counter: 1 -[python3-4] will be ignoring SIGINT -[python3-4] will be ignoring SIGTERM -[python3-4] Counter: 1 -[python3-2] Counter: 1 -[python3-3] Counter: 2 -[python3-4] Counter: 2 -[python3-2] Counter: 2 -[python3-3] Counter: 3 -[python3-4] Counter: 3 -[python3-2] Counter: 3 -[python3-3] Counter: 4 -[python3-4] Counter: 4 -[python3-2] Counter: 4 -[INFO] [launch.user]: Launch was asked to shutdown: saw 'Counter: 4' from 'python3-2' -[INFO] [launch]: sending signal 'SIGINT' to process[python3-4] -[INFO] [launch]: sending signal 'SIGINT' to process[python3-3] -[INFO] [launch]: sending signal 'SIGINT' to process[python3-2] -[python3-4] ignoring SIGINT -[python3-3] ignoring SIGINT -[python3-2] Traceback (most recent call last): -[python3-2] File "./counter.py", line 79, in -[python3-2] sys.exit(main()) -[python3-2] File "./counter.py", line 72, in main -[python3-2] time.sleep(args.sleep) -[python3-2] KeyboardInterrupt -[ERROR] [launch]: process[python3-2] process has died [pid 22436, exit code 1, cmd 'python3 -u ./counter.py']. -[python3-3] Counter: 5 -[python3-4] Counter: 5 -[python3-3] Counter: 6 -[python3-4] Counter: 6 -[python3-3] Counter: 7 -[python3-4] Counter: 7 -[python3-3] Counter: 8 -[python3-4] Counter: 8 -[ERROR] [launch]: process[python3-4] failed to terminate 5 seconds after receiving SIGINT, escalating to SIGTERM -[ERROR] [launch]: process[python3-3] failed to terminate 5 seconds after receiving SIGINT, escalating to SIGTERM -[INFO] [launch]: sending signal 'SIGTERM' to process[python3-4] -[INFO] [launch]: sending signal 'SIGTERM' to process[python3-3] -[python3-3] Counter: 9 -[python3-4] ignoring SIGTERM -[python3-4] Counter: 9 -[ERROR] [launch]: process[python3-3] process has died [pid 22438, exit code -15, cmd 'python3 -u ./counter.py --ignore-sigint']. -[python3-4] Counter: 10 -[python3-4] Counter: 11 -[python3-4] Counter: 12 -[python3-4] Counter: 13 -[ERROR] [launch]: process[python3-4] failed to terminate 10.0 seconds after receiving SIGTERM, escalating to SIGKILL -[INFO] [launch]: sending signal 'SIGKILL' to process[python3-4] -[ERROR] [launch]: process[python3-4] process has died [pid 22439, exit code -9, cmd 'python3 -u ./counter.py --ignore-sigint --ignore-sigterm']. diff --git a/launch/examples/launch_counters.py b/launch/examples/launch_counters.py deleted file mode 100755 index aa5935aa..00000000 --- a/launch/examples/launch_counters.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2015 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. - -""" -Script that demonstrates launch and subprocesses of varying behavior. - -For an example of expected output, see the file next to this one called -"launch_counter_good_bad_ugly.example.txt". -""" - -import os -import platform -import sys -from typing import cast -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) # noqa - -import launch -from launch import LaunchDescription -from launch import LaunchIntrospector -from launch import LaunchService -import launch.actions -import launch.events -import launch.substitutions - - -def main(argv=sys.argv[1:]): - """Main.""" - # Any number of actions can optionally be given to the constructor of LaunchDescription. - # Or actions/entities can be added after creating the LaunchDescription. - user_env_var = 'USERNAME' if platform.system() == 'Windows' else 'USER' - ld = LaunchDescription([ - launch.actions.LogInfo(msg='Hello World!'), - launch.actions.LogInfo(msg=( - 'Is that you, ', launch.substitutions.EnvironmentVariable(name=user_env_var), '?' - )), - ]) - - # Setup a custom event handler for all stdout/stderr from processes. - # Later, this will be a configurable, but always present, extension to the LaunchService. - def on_output(event: launch.Event) -> None: - for line in event.text.decode().splitlines(): - print('[{}] {}'.format( - cast(launch.events.process.ProcessIO, event).process_name, line)) - - ld.add_action(launch.actions.RegisterEventHandler(launch.event_handlers.OnProcessIO( - # this is the action ^ and this, the event handler ^ - on_stdout=on_output, - on_stderr=on_output, - ))) - - # Run whoami, and use its output to log the name of the user. - # Prefix just the whoami process with `time`. - ld.add_action(launch.actions.SetLaunchConfiguration('launch-prefix', 'time')) - # Run whoami, but keep handle to action to make a targeted event handler. - if platform.system() == 'Windows': - whoami_cmd = ['echo', '%USERNAME%'] - else: - whoami_cmd = [launch.substitutions.FindExecutable(name='whoami')] - whoami_action = launch.actions.ExecuteProcess( - cmd=whoami_cmd, - shell=True - ) - ld.add_action(whoami_action) - # Make event handler that uses the output. - ld.add_action(launch.actions.RegisterEventHandler(launch.event_handlers.OnProcessIO( - target_action=whoami_action, - # The output of `time` will be skipped since `time`'s output always goes to stderr. - on_stdout=lambda event: launch.actions.LogInfo( - msg="whoami says you are '{}'.".format(event.text.decode().strip()) - ), - ))) - # Unset launch prefix to prevent other process from getting this setting. - ld.add_action(launch.actions.SetLaunchConfiguration('launch-prefix', '')) - - # Run the counting program, with default options. - counter_action = launch.actions.ExecuteProcess(cmd=[sys.executable, '-u', './counter.py']) - ld.add_action(counter_action) - - # Setup an event handler for just this process which will exit when `Counter: 4` is seen. - def counter_output_handler(event): - target_str = 'Counter: 4' - if target_str in event.text.decode(): - return launch.actions.EmitEvent(event=launch.events.Shutdown( - reason="saw '{}' from '{}'".format(target_str, event.process_name) - )) - - ld.add_action(launch.actions.RegisterEventHandler(launch.event_handlers.OnProcessIO( - target_action=counter_action, - on_stdout=counter_output_handler, - on_stderr=counter_output_handler, - ))) - - # Run the counter a few more times, with various options. - ld.add_action(launch.actions.ExecuteProcess( - cmd=[sys.executable, '-u', './counter.py', '--ignore-sigint'] - )) - ld.add_action(launch.actions.ExecuteProcess( - cmd=[sys.executable, '-u', './counter.py', '--ignore-sigint', '--ignore-sigterm'] - )) - - # Add our own message for when shutdown is requested. - ld.add_action(launch.actions.RegisterEventHandler(launch.event_handlers.OnShutdown( - on_shutdown=[launch.actions.LogInfo(msg=[ - 'Launch was asked to shutdown: ', - launch.substitutions.LocalSubstitution('event.reason'), - ])], - ))) - - print('Starting introspection of launch description...') - print('') - - print(LaunchIntrospector().format_launch_description(ld)) - - print('') - print('Starting launch of launch description...') - print('') - - # ls = LaunchService(argv=argv, debug=True) # Use this instead to get more debug messages. - ls = LaunchService(argv=argv) - ls.include_launch_description(ld) - return ls.run() - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/launch/launch/__init__.py b/launch/launch/__init__.py deleted file mode 100644 index 1cdc897b..00000000 --- a/launch/launch/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2015 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. - -"""Main entry point for the `launch` package.""" - -from . import actions -from . import conditions -from . import events -from . import logging -from .action import Action -from .condition import Condition -from .event import Event -from .event_handler import EventHandler -from .launch_context import LaunchContext -from .launch_description import LaunchDescription -from .launch_description_entity import LaunchDescriptionEntity -from .launch_description_source import LaunchDescriptionSource -from .launch_introspector import LaunchIntrospector -from .launch_service import LaunchService -from .some_actions_type import SomeActionsType -from .some_actions_type import SomeActionsType_types_tuple -from .some_substitutions_type import SomeSubstitutionsType -from .some_substitutions_type import SomeSubstitutionsType_types_tuple -from .substitution import Substitution - -__all__ = [ - 'actions', - 'conditions', - 'events', - 'logging', - 'Action', - 'Condition', - 'Event', - 'EventHandler', - 'LaunchContext', - 'LaunchDescription', - 'LaunchDescriptionEntity', - 'LaunchDescriptionSource', - 'LaunchIntrospector', - 'LaunchService', - 'SomeActionsType', - 'SomeActionsType_types_tuple', - 'SomeSubstitutionsType', - 'SomeSubstitutionsType_types_tuple', - 'Substitution', -] diff --git a/launch/launch/action.py b/launch/launch/action.py deleted file mode 100644 index 112c953e..00000000 --- a/launch/launch/action.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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. - -"""Module for Action class.""" - -from typing import cast -from typing import List -from typing import Optional -from typing import Text - -from .condition import Condition -from .launch_context import LaunchContext -from .launch_description_entity import LaunchDescriptionEntity - - -class Action(LaunchDescriptionEntity): - """ - LaunchDescriptionEntity that represents a user intention to do something. - - The action describes the intention to do something, but also can be - executed given a :class:`launch.LaunchContext` at runtime. - """ - - def __init__(self, *, condition: Optional[Condition] = None) -> None: - """ - Constructor. - - If the conditions argument is not None, the condition object will be - evaluated while being visited and the action will only be executed if - the condition evaluates to True. - - :param condition: Either a :py:class:`Condition` or None - """ - self.__condition = condition - - @property - def condition(self) -> Optional[Condition]: - """Getter for condition.""" - return self.__condition - - def describe(self) -> Text: - """Return a description of this Action.""" - return self.__repr__() - - def visit(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEntity]]: - """Override visit from LaunchDescriptionEntity so that it executes.""" - if self.__condition is None or self.__condition.evaluate(context): - try: - return cast(Optional[List[LaunchDescriptionEntity]], self.execute(context)) - finally: - from .events import ExecutionComplete # noqa - event = ExecutionComplete(action=self) - if context.would_handle_event(event): - future = self.get_asyncio_future() - if future is not None: - future.add_done_callback( - lambda _: context.emit_event_sync(event) - ) - else: - context.emit_event_sync(event) - return None - - def execute(self, context: LaunchContext) -> Optional[List['Action']]: - """ - Execute the action. - - Should be overridden by derived class, but by default does nothing. - """ - pass diff --git a/launch/launch/actions/__init__.py b/launch/launch/actions/__init__.py deleted file mode 100644 index a3e40f32..00000000 --- a/launch/launch/actions/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# 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. - -"""actions Module.""" - -from .declare_launch_argument import DeclareLaunchArgument -from .emit_event import EmitEvent -from .execute_process import ExecuteProcess -from .group_action import GroupAction -from .include_launch_description import IncludeLaunchDescription -from .log_info import LogInfo -from .opaque_coroutine import OpaqueCoroutine -from .opaque_function import OpaqueFunction -from .pop_launch_configurations import PopLaunchConfigurations -from .push_launch_configurations import PushLaunchConfigurations -from .register_event_handler import RegisterEventHandler -from .set_launch_configuration import SetLaunchConfiguration -from .shutdown_action import Shutdown -from .timer_action import TimerAction -from .unregister_event_handler import UnregisterEventHandler -from .unset_launch_configuration import UnsetLaunchConfiguration - -__all__ = [ - 'DeclareLaunchArgument', - 'EmitEvent', - 'ExecuteProcess', - 'GroupAction', - 'IncludeLaunchDescription', - 'LogInfo', - 'OpaqueCoroutine', - 'OpaqueFunction', - 'PopLaunchConfigurations', - 'PushLaunchConfigurations', - 'RegisterEventHandler', - 'SetLaunchConfiguration', - 'Shutdown', - 'TimerAction', - 'UnregisterEventHandler', - 'UnsetLaunchConfiguration', -] diff --git a/launch/launch/actions/declare_launch_argument.py b/launch/launch/actions/declare_launch_argument.py deleted file mode 100644 index afa31c9d..00000000 --- a/launch/launch/actions/declare_launch_argument.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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. - -"""Module for the DeclareLaunchArgument action.""" - -from typing import List -from typing import Optional -from typing import Text - -import launch.logging - -from ..action import Action -from ..launch_context import LaunchContext -from ..some_substitutions_type import SomeSubstitutionsType -from ..substitution import Substitution -from ..utilities import normalize_to_list_of_substitutions -from ..utilities import perform_substitutions - - -class DeclareLaunchArgument(Action): - """ - Action that declares a new launch argument. - - A launch arguments are stored in a "launch configuration" of the same name. - See :py:class:`launch.actions.SetLaunchConfiguration` and - :py:class:`launch.substitutions.LaunchConfiguration`. - - Any launch arguments declared within a :py:class:`launch.LaunchDescription` - will be exposed as arguments when that launch description is included, e.g. - as additional parameters in the - :py:class:`launch.actions.IncludeLaunchDescription` action or as - command-line arguments when launched with ``ros2 launch ...``. - - In addition to the name, which is also where the argument result is stored, - launch arguments may have a default value and a description. - If a default value is given, then the argument becomes optional and the - default value is placed in the launch configuration instead. - If no default value is given and no value is given when including the - launch description, then an error occurs. - - The default value may use Substitutions, but the name and description can - only be Text, since they need a meaningful value before launching, e.g. - when listing the command-line arguments. - - Note that declaring a launch argument needs to be in a part of the launch - description that is describable without launching. - For example, if you declare a launch argument with this action from within - a condition group or as a callback to an event handler, then it may not be - possible for a tool like ``ros2 launch`` to know about the argument before - launching the launch description. - In such cases, the argument will not be visible on the command line but - may raise an exception if that argument is not satisfied once visited (and - has no default value). - - Put another way, the post-condition of this action being visited is either - that a launch configuration of the same name is set with a value or an - exception is raised because none is set and there is no default value. - However, the pre-condition does not guarantee that the argument was visible - if behind condition or situational inclusions. - """ - - def __init__( - self, - name: Text, - *, - default_value: Optional[SomeSubstitutionsType] = None, - description: Text = 'no description given', - **kwargs - ) -> None: - """Constructor.""" - super().__init__(**kwargs) - self.__name = name - if default_value is None: - self.__default_value = default_value - else: - self.__default_value = normalize_to_list_of_substitutions(default_value) - self.__description = description - - self.__logger = launch.logging.get_logger(__name__) - - # This is used later to determine if this launch argument will be - # conditionally visited. - # Its value will be read and set at different times and so the value - # may change depending at different times based on the context. - self._conditionally_included = False - - @property - def name(self) -> Text: - """Getter for self.__name.""" - return self.__name - - @property - def default_value(self) -> Optional[List[Substitution]]: - """Getter for self.__default_value.""" - return self.__default_value - - @property - def description(self) -> Text: - """Getter for self.__description.""" - return self.__description - - def execute(self, context: LaunchContext): - """Execute the action.""" - if self.name not in context.launch_configurations: - if self.default_value is None: - # Argument not already set and no default value given, error. - self.__logger.error( - "Required launch argument '{}' (description: '{}') was not provided".format( - self.name, self.description) - ) - raise RuntimeError( - "Required launch argument '{}' was not provided.".format(self.name)) - context.launch_configurations[self.name] = \ - perform_substitutions(context, self.default_value) diff --git a/launch/launch/actions/emit_event.py b/launch/launch/actions/emit_event.py deleted file mode 100644 index ed64eba2..00000000 --- a/launch/launch/actions/emit_event.py +++ /dev/null @@ -1,40 +0,0 @@ -# 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. - -"""Module for the EmitEvent action.""" - -from ..action import Action -from ..event import Event -from ..launch_context import LaunchContext -from ..utilities import is_a_subclass - - -class EmitEvent(Action): - """Action that emits an event when executed.""" - - def __init__(self, *, event: Event, **kwargs) -> None: - """Constructor.""" - super().__init__(**kwargs) - if not is_a_subclass(event, Event): - raise RuntimeError("EmitEvent() expected an event instance, got '{}'.".format(event)) - self.__event = event - - @property - def event(self): - """Getter for self.__event.""" - return self.__event - - def execute(self, context: LaunchContext): - """Execute the action.""" - context.emit_event_sync(self.__event) diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py deleted file mode 100644 index 65032683..00000000 --- a/launch/launch/actions/execute_process.py +++ /dev/null @@ -1,571 +0,0 @@ -# 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. - -"""Module for the ExecuteProcess action.""" - -import asyncio -import os -import platform -import shlex -import signal -import threading -import traceback -from typing import Any # noqa: F401 -from typing import Callable -from typing import cast -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional -from typing import Text -from typing import Tuple # noqa: F401 -from typing import Union - -import launch.logging - -from osrf_pycommon.process_utils import async_execute_process -from osrf_pycommon.process_utils import AsyncSubprocessProtocol - -from .emit_event import EmitEvent -from .opaque_function import OpaqueFunction -from .timer_action import TimerAction - -from ..action import Action -from ..event import Event -from ..event_handler import EventHandler -from ..event_handlers import OnProcessExit -from ..event_handlers import OnProcessIO -from ..event_handlers import OnShutdown -from ..events import matches_action -from ..events import Shutdown -from ..events.process import ProcessExited -from ..events.process import ProcessIO -from ..events.process import ProcessStarted -from ..events.process import ProcessStderr -from ..events.process import ProcessStdin -from ..events.process import ProcessStdout -from ..events.process import ShutdownProcess -from ..events.process import SignalProcess -from ..launch_context import LaunchContext -from ..launch_description import LaunchDescription -from ..some_actions_type import SomeActionsType -from ..some_substitutions_type import SomeSubstitutionsType -from ..substitution import Substitution # noqa: F401 -from ..substitutions import LaunchConfiguration -from ..substitutions import PythonExpression -from ..utilities import create_future -from ..utilities import is_a_subclass -from ..utilities import normalize_to_list_of_substitutions -from ..utilities import perform_substitutions - -_global_process_counter_lock = threading.Lock() -_global_process_counter = 0 # in Python3, this number is unbounded (no rollover) - - -class ExecuteProcess(Action): - """Action that begins executing a process and sets up event handlers for the process.""" - - def __init__( - self, - *, - cmd: Iterable[SomeSubstitutionsType], - name: Optional[SomeSubstitutionsType] = None, - cwd: Optional[SomeSubstitutionsType] = None, - env: Optional[Dict[SomeSubstitutionsType, SomeSubstitutionsType]] = None, - shell: bool = False, - sigterm_timeout: SomeSubstitutionsType = LaunchConfiguration( - 'sigterm_timeout', default=5), - sigkill_timeout: SomeSubstitutionsType = LaunchConfiguration( - 'sigkill_timeout', default=5), - prefix: Optional[SomeSubstitutionsType] = None, - output: Text = 'log', - output_format: Text = '[{this.name}] {line}', - log_cmd: bool = False, - on_exit: Optional[Union[ - SomeActionsType, - Callable[[ProcessExited, LaunchContext], Optional[SomeActionsType]] - ]] = None, - **kwargs - ) -> None: - """ - Construct an ExecuteProcess action. - - Many arguments are passed eventually to :class:`subprocess.Popen`, so - see the documentation for the class for additional details. - - This action, once executed, registers several event handlers for - various process related events and will also emit events asynchronously - when certain events related to the process occur. - - Handled events include: - - - launch.events.process.ShutdownProcess: - - - begins standard shutdown procedure for a running executable - - - launch.events.process.SignalProcess: - - - passes the signal provided by the event to the running process - - - launch.events.process.ProcessStdin: - - - passes the text provided by the event to the stdin of the process - - - launch.events.Shutdown: - - - same as ShutdownProcess - - Emitted events include: - - - launch.events.process.ProcessStarted: - - - emitted when the process starts - - - launch.events.process.ProcessExited: - - - emitted when the process exits - - event contains return code - - - launch.events.process.ProcessStdout and launch.events.process.ProcessStderr: - - - emitted when the process produces data on either the stdout or stderr pipes - - event contains the data from the pipe - - Note that output is just stored in this class and has to be properly - implemented by the event handlers for the process's ProcessIO events. - - :param: cmd a list where the first item is the executable and the rest - are arguments to the executable, each item may be a string or a - list of strings and Substitutions to be resolved at runtime - :param: cwd the directory in which to run the executable - :param: name the label used to represent the process, as a string or a - Substitution to be resolved at runtime, defaults to the basename of - the executable - :param: env dictionary of environment variables to be used - :param: shell if True, a shell is used to execute the cmd - :param: sigterm_timeout time until shutdown should escalate to SIGTERM, - as a string or a list of strings and Substitutions to be resolved - at runtime, defaults to the LaunchConfiguration called - 'sigterm_timeout' - :param: sigkill_timeout time until escalating to SIGKILL after SIGTERM, - as a string or a list of strings and Substitutions to be resolved - at runtime, defaults to the LaunchConfiguration called - 'sigkill_timeout' - :param: prefix a set of commands/arguments to preceed the cmd, used for - things like gdb/valgrind and defaults to the LaunchConfiguration - called 'launch-prefix' - :param: output configuration for process output logging. Default is 'log' i.e. - log both stdout and stderr to launch main log file and stderr to the screen. - See `launch.logging.get_output_loggers()` documentation for further reference - on all available options. - :param: output_format for logging each output line, supporting `str.format()` - substitutions with the following keys in scope: `line` to reference the raw - output line and `this` to reference this action instance. - :param: log_cmd if True, prints the final cmd before executing the - process, which is useful for debugging when substitutions are - involved. - :param: on_exit list of actions to execute upon process exit. - """ - super().__init__(**kwargs) - self.__cmd = [normalize_to_list_of_substitutions(x) for x in cmd] - self.__name = name if name is None else normalize_to_list_of_substitutions(name) - self.__cwd = cwd if cwd is None else normalize_to_list_of_substitutions(cwd) - self.__env = None # type: Optional[List[Tuple[List[Substitution], List[Substitution]]]] - if env is not None: - self.__env = [] - for key, value in env.items(): - self.__env.append(( - normalize_to_list_of_substitutions(key), - normalize_to_list_of_substitutions(value))) - self.__shell = shell - self.__sigterm_timeout = normalize_to_list_of_substitutions(sigterm_timeout) - self.__sigkill_timeout = normalize_to_list_of_substitutions(sigkill_timeout) - self.__prefix = normalize_to_list_of_substitutions( - LaunchConfiguration('launch-prefix', default='') if prefix is None else prefix - ) - self.__output = output - self.__output_format = output_format - - self.__log_cmd = log_cmd - self.__on_exit = on_exit - - self.__process_event_args = None # type: Optional[Dict[Text, Any]] - self._subprocess_protocol = None # type: Optional[Any] - self._subprocess_transport = None - self.__completed_future = None # type: Optional[asyncio.Future] - self.__sigterm_timer = None # type: Optional[TimerAction] - self.__sigkill_timer = None # type: Optional[TimerAction] - self.__shutdown_received = False - - @property - def output(self): - """Getter for output.""" - return self.__output - - @property - def process_details(self): - """Getter for the process details, e.g. name, pid, cmd, etc., or None if not started.""" - return self.__process_event_args - - def _shutdown_process(self, context, *, send_sigint): - if self.__shutdown_received: - # Do not handle shutdown more than once. - return None - self.__shutdown_received = True - if self.__completed_future is None: - # Execution not started so nothing to do, but self.__shutdown_received should prevent - # execution from starting in the future. - return None - if self.__completed_future.done(): - # If already done, then nothing to do. - return None - # Otherwise process is still running, start the shutdown procedures. - context.extend_locals({'process_name': self.process_details['name']}) - actions_to_return = self.__get_shutdown_timer_actions() - if send_sigint: - actions_to_return.append(self.__get_sigint_event()) - return actions_to_return - - def __on_shutdown_process_event( - self, - context: LaunchContext - ) -> Optional[LaunchDescription]: - return self._shutdown_process(context, send_sigint=True) - - def __on_signal_process_event( - self, - context: LaunchContext - ) -> Optional[LaunchDescription]: - typed_event = cast(SignalProcess, context.locals.event) - if not typed_event.process_matcher(self): - # this event whas not intended for this process - return None - if self.process_details is None: - raise RuntimeError('Signal event received before execution.') - if self._subprocess_transport is None: - raise RuntimeError('Signal event received before subprocess transport available.') - if self._subprocess_protocol.complete.done(): - # the process is done or is cleaning up, no need to signal - self.__logger.debug( - "signal '{}' not set to '{}' because it is already closing".format( - typed_event.signal_name, self.process_details['name']), - ) - return None - if platform.system() == 'Windows' and typed_event.signal_name == 'SIGINT': - # TODO(wjwwood): remove this when/if SIGINT is fixed on Windows - self.__logger.warning( - "'SIGINT' sent to process[{}] not supported on Windows, escalating to 'SIGTERM'" - .format(self.process_details['name']), - ) - typed_event = SignalProcess( - signal_number=signal.SIGTERM, - process_matcher=lambda process: True) - self.__logger.info("sending signal '{}' to process[{}]".format( - typed_event.signal_name, self.process_details['name'] - )) - try: - if typed_event.signal_name == 'SIGKILL': - self._subprocess_transport.kill() # works on both Windows and POSIX - return None - self._subprocess_transport.send_signal(typed_event.signal) - return None - except ProcessLookupError: - self.__logger.debug( - "signal '{}' not sent to '{}' because it has closed already".format( - typed_event.signal_name, self.process_details['name'] - ) - ) - - def __on_process_stdin( - self, - event: ProcessIO - ) -> Optional[SomeActionsType]: - self.__logger.warning( - "in ExecuteProcess('{}').__on_process_stdin_event()".format(id(self)), - ) - cast(ProcessStdin, event) - return None - - def __on_process_stdout( - self, event: ProcessIO - ) -> Optional[SomeActionsType]: - for line in event.text.decode(errors='replace').splitlines(): - self.__stdout_logger.info( - self.__output_format.format(line=line, this=self) - ) - - def __on_process_stderr( - self, event: ProcessIO - ) -> Optional[SomeActionsType]: - for line in event.text.decode(errors='replace').splitlines(): - self.__stderr_logger.info( - self.__output_format.format(line=line, this=self) - ) - - def __on_shutdown(self, event: Event, context: LaunchContext) -> Optional[SomeActionsType]: - return self._shutdown_process( - context, - send_sigint=(not cast(Shutdown, event).due_to_sigint), - ) - - def __get_shutdown_timer_actions(self) -> List[Action]: - base_msg = \ - "process[{}] failed to terminate '{}' seconds after receiving '{}', escalating to '{}'" - - def printer(context, msg, timeout_substitutions): - self.__logger.error(msg.format( - context.locals.process_name, - perform_substitutions(context, timeout_substitutions), - )) - - sigterm_timeout = self.__sigterm_timeout - sigkill_timeout = [PythonExpression( - ('float(', *self.__sigterm_timeout, ') + float(', *self.__sigkill_timeout, ')') - )] - # Setup a timer to send us a SIGTERM if we don't shutdown quickly. - self.__sigterm_timer = TimerAction( - period=sigterm_timeout, - actions=[ - OpaqueFunction( - function=printer, - args=(base_msg.format('{}', '{}', 'SIGINT', 'SIGTERM'), sigterm_timeout) - ), - EmitEvent(event=SignalProcess( - signal_number=signal.SIGTERM, - process_matcher=matches_action(self) - )), - ], - cancel_on_shutdown=False, - ) - # Setup a timer to send us a SIGKILL if we don't shutdown after SIGTERM. - self.__sigkill_timer = TimerAction( - period=sigkill_timeout, - actions=[ - OpaqueFunction( - function=printer, - args=(base_msg.format('{}', '{}', 'SIGTERM', 'SIGKILL'), sigkill_timeout) - ), - EmitEvent(event=SignalProcess( - signal_number='SIGKILL', - process_matcher=matches_action(self) - )) - ], - cancel_on_shutdown=False, - ) - return [ - cast(Action, self.__sigterm_timer), - cast(Action, self.__sigkill_timer), - ] - - def __get_sigint_event(self): - return EmitEvent(event=SignalProcess( - signal_number=signal.SIGINT, - process_matcher=matches_action(self), - )) - - def __cleanup(self): - # Cancel any pending timers we started. - if self.__sigterm_timer is not None: - self.__sigterm_timer.cancel() - if self.__sigkill_timer is not None: - self.__sigkill_timer.cancel() - # Close subprocess transport if any. - if self._subprocess_transport is not None: - self._subprocess_transport.close() - # Signal that we're done to the launch system. - self.__completed_future.set_result(None) - - class __ProcessProtocol(AsyncSubprocessProtocol): - def __init__( - self, - action: 'ExecuteProcess', - context: LaunchContext, - process_event_args: Dict, - **kwargs - ) -> None: - super().__init__(**kwargs) - self.__context = context - self.__action = action - self.__process_event_args = process_event_args - self.__logger = launch.logging.get_logger(process_event_args['name']) - - def connection_made(self, transport): - self.__logger.info( - 'process started with pid [{}]'.format(transport.get_pid()), - ) - super().connection_made(transport) - self.__process_event_args['pid'] = transport.get_pid() - self.__action._subprocess_transport = transport - - def on_stdout_received(self, data: bytes) -> None: - self.__context.emit_event_sync(ProcessStdout(text=data, **self.__process_event_args)) - - def on_stderr_received(self, data: bytes) -> None: - self.__context.emit_event_sync(ProcessStderr(text=data, **self.__process_event_args)) - - def __expand_substitutions(self, context): - # expand substitutions in arguments to async_execute_process() - cmd = [perform_substitutions(context, x) for x in self.__cmd] - name = os.path.basename(cmd[0]) if self.__name is None \ - else perform_substitutions(context, self.__name) - cmd = shlex.split(perform_substitutions(context, self.__prefix)) + cmd - with _global_process_counter_lock: - global _global_process_counter - _global_process_counter += 1 - self.__name = '{}-{}'.format(name, _global_process_counter) - cwd = None - if self.__cwd is not None: - cwd = ''.join([context.perform_substitution(x) for x in self.__cwd]) - env = None - if self.__env is not None: - env = {} - for key, value in self.__env: - env[''.join([context.perform_substitution(x) for x in key])] = \ - ''.join([context.perform_substitution(x) for x in value]) - - # store packed kwargs for all ProcessEvent based events - self.__process_event_args = { - 'action': self, - 'name': self.__name, - 'cmd': cmd, - 'cwd': cwd, - 'env': env, - # pid is added to the dictionary in the connection_made() method of the protocol. - } - - async def __execute_process(self, context: LaunchContext) -> None: - process_event_args = self.__process_event_args - if process_event_args is None: - raise RuntimeError('process_event_args unexpectedly None') - cmd = process_event_args['cmd'] - cwd = process_event_args['cwd'] - env = process_event_args['env'] - if self.__log_cmd: - self.__logger.info("process details: cmd=[{}], cwd='{}', custom_env?={}".format( - ', '.join(cmd), cwd, 'True' if env is not None else 'False' - )) - try: - transport, self._subprocess_protocol = await async_execute_process( - lambda **kwargs: self.__ProcessProtocol( - self, context, process_event_args, **kwargs - ), - cmd=cmd, - cwd=cwd, - env=env, - shell=self.__shell, - emulate_tty=False, - ) - except Exception: - self.__logger.error('exception occurred while executing process:\n{}'.format( - traceback.format_exc() - )) - self.__cleanup() - return - - pid = transport.get_pid() - - await context.emit_event(ProcessStarted(**process_event_args)) - - returncode = await self._subprocess_protocol.complete - if returncode == 0: - self.__logger.info('process has finished cleanly [pid {}]'.format(pid)) - else: - self.__logger.error("process has died [pid {}, exit code {}, cmd '{}'].".format( - pid, returncode, ' '.join(cmd) - )) - await context.emit_event(ProcessExited(returncode=returncode, **process_event_args)) - self.__cleanup() - - def execute(self, context: LaunchContext) -> Optional[List['Action']]: - """ - Execute the action. - - This does the following: - - register an event handler for the shutdown process event - - register an event handler for the signal process event - - register an event handler for the stdin event - - configures logging for the IO process event - - create a task for the coroutine that monitors the process - """ - if self.__shutdown_received: - # If shutdown starts before execution can start, don't start execution. - return None - - event_handlers = [ - EventHandler( - matcher=lambda event: is_a_subclass(event, ShutdownProcess), - entities=OpaqueFunction(function=self.__on_shutdown_process_event), - ), - EventHandler( - matcher=lambda event: is_a_subclass(event, SignalProcess), - entities=OpaqueFunction(function=self.__on_signal_process_event), - ), - OnProcessIO( - target_action=self, - on_stdin=self.__on_process_stdin, - on_stdout=self.__on_process_stdout, - on_stderr=self.__on_process_stderr - ), - OnShutdown( - on_shutdown=self.__on_shutdown, - ), - OnProcessExit( - target_action=self, - on_exit=self.__on_exit, - ), - ] - for event_handler in event_handlers: - context.register_event_handler(event_handler) - - try: - self.__completed_future = create_future(context.asyncio_loop) - self.__expand_substitutions(context) - self.__logger = launch.logging.get_logger(self.__name) - self.__stdout_logger, self.__stderr_logger = \ - launch.logging.get_output_loggers(self.__name, self.__output) - context.asyncio_loop.create_task(self.__execute_process(context)) - except Exception: - for event_handler in event_handlers: - context.unregister_event_handler(event_handler) - raise - return None - - def get_asyncio_future(self) -> Optional[asyncio.Future]: - """Return an asyncio Future, used to let the launch system know when we're done.""" - return self.__completed_future - - @property - def name(self): - """Getter for name.""" - return self.__name - - @property - def cmd(self): - """Getter for cmd.""" - return self.__cmd - - @property - def cwd(self): - """Getter for cwd.""" - return self.__cwd - - @property - def env(self): - """Getter for env.""" - return self.__env - - @property - def shell(self): - """Getter for shell.""" - return self.__shell diff --git a/launch/launch/actions/group_action.py b/launch/launch/actions/group_action.py deleted file mode 100644 index 1bee4db6..00000000 --- a/launch/launch/actions/group_action.py +++ /dev/null @@ -1,64 +0,0 @@ -# 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. - -"""Module for the GroupAction action.""" - -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional - -from .pop_launch_configurations import PopLaunchConfigurations -from .push_launch_configurations import PushLaunchConfigurations -from .set_launch_configuration import SetLaunchConfiguration -from ..action import Action -from ..launch_context import LaunchContext -from ..some_substitutions_type import SomeSubstitutionsType - - -class GroupAction(Action): - """ - Action that yields other actions, optionally scoping launch configurations. - - This action is used to nest other actions without including a separate - launch description, while also optionally having a condition (like all - other actions), scoping launch configurations, and/or declaring launch - configurations for just the group and its yielded actions. - """ - - def __init__( - self, - actions: Iterable[Action], - *, - scoped: bool = True, - launch_configurations: Optional[Dict[SomeSubstitutionsType, SomeSubstitutionsType]] = None, - **left_over_kwargs - ) -> None: - """Constructor.""" - super().__init__(**left_over_kwargs) - self.__actions = actions - self.__scoped = scoped - if launch_configurations is not None: - self.__launch_configurations = launch_configurations - else: - self.__launch_configurations = {} - - def execute(self, context: LaunchContext) -> Optional[List[Action]]: - """Execute the action.""" - actions = [] # type: List[Action] - actions += [SetLaunchConfiguration(k, v) for k, v in self.__launch_configurations.items()] - actions += list(self.__actions) - if self.__scoped: - return [PushLaunchConfigurations(), *actions, PopLaunchConfigurations()] - return actions diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py deleted file mode 100644 index c6ca76a2..00000000 --- a/launch/launch/actions/include_launch_description.py +++ /dev/null @@ -1,130 +0,0 @@ -# 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. - -"""Module for the IncludeLaunchDescription action.""" - -import os -from typing import Iterable -from typing import List -from typing import Optional -from typing import Tuple - -from .set_launch_configuration import SetLaunchConfiguration -from ..action import Action -from ..launch_context import LaunchContext -from ..launch_description_entity import LaunchDescriptionEntity -from ..launch_description_source import LaunchDescriptionSource -from ..some_substitutions_type import SomeSubstitutionsType -from ..utilities import normalize_to_list_of_substitutions -from ..utilities import perform_substitutions - - -class IncludeLaunchDescription(Action): - """ - Action that includes a launch description source and yields its entities when visited. - - It is possible to pass arguments to the launch description, which it - declared with the :py:class:`launch.actions.DeclareLaunchArgument` action. - - If any given arguments do not match the name of any declared launch - arguments, then they will still be set as Launch Configurations using the - :py:class:`launch.actions.SetLaunchConfiguration` action. - This is done because it's not always possible to detect all instances of - the declare launch argument class in the given launch description. - - On the other side, an error will sometimes be raised if the given launch - description declares a launch argument and its value is not provided to - this action. - It will only produce this error, however, if the declared launch argument - is unconditional (sometimes the action that declares the launch argument - will only be visited in certain circumstances) and if it does not have a - default value on which to fall back. - - Conditionally included launch arguments that do not have a default value - will eventually raise an error if this best effort argument checking is - unable to see an unsatisfied argument ahead of time. - """ - - def __init__( - self, - launch_description_source: LaunchDescriptionSource, - *, - launch_arguments: Optional[ - Iterable[Tuple[SomeSubstitutionsType, SomeSubstitutionsType]] - ] = None - ) -> None: - """Constructor.""" - super().__init__() - self.__launch_description_source = launch_description_source - self.__launch_arguments = launch_arguments - - @property - def launch_description_source(self) -> LaunchDescriptionSource: - """Getter for self.__launch_description_source.""" - return self.__launch_description_source - - @property - def launch_arguments(self) -> Iterable[Tuple[SomeSubstitutionsType, SomeSubstitutionsType]]: - """Getter for self.__launch_arguments.""" - if self.__launch_arguments is None: - return [] - else: - return self.__launch_arguments - - def _get_launch_file_location(self): - launch_file_location = os.path.abspath(self.__launch_description_source.location) - if os.path.exists(launch_file_location): - launch_file_location = os.path.dirname(launch_file_location) - else: - # If the location does not exist, then it's likely set to '