Skip to content

Commit bc27324

Browse files
author
Roger Strain
committed
Adjust for launch_ros modifications, add unit tests
Distro A; OPSEC #4584 Signed-off-by: Roger Strain <[email protected]>
1 parent 4ece931 commit bc27324

File tree

7 files changed

+202
-40
lines changed

7 files changed

+202
-40
lines changed

launch/launch/actions/execute_local.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def __init__(
9090
'sigkill_timeout', default=5),
9191
emulate_tty: bool = False,
9292
output: Text = 'log',
93-
output_format: Text = '[{this.name}] {line}',
93+
output_format: Text = '[{this.process_description.name}] {line}',
9494
log_cmd: bool = False,
9595
on_exit: Optional[Union[
9696
SomeActionsType,
@@ -607,7 +607,7 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
607607

608608
if self.__executed:
609609
raise RuntimeError(
610-
f"ExecuteProcess action '{name}': executed more than once: {self.describe()}"
610+
f"ExecuteLocal action '{name}': executed more than once: {self.describe()}"
611611
)
612612
self.__executed = True
613613

launch/launch/actions/execute_process.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ def __init__(
144144
Defaults to 'False'.
145145
:param: respawn_delay a delay time to relaunch the died process if respawn is 'True'.
146146
"""
147-
self.__executable = Executable(cmd=cmd, prefix=prefix, name=name, cwd=cwd, env=env,
148-
additional_env=additional_env)
149-
super().__init__(process_description=self.__executable, **kwargs)
147+
executable = Executable(cmd=cmd, prefix=prefix, name=name, cwd=cwd, env=env,
148+
additional_env=additional_env)
149+
super().__init__(process_description=executable, **kwargs)
150150

151151
@classmethod
152152
def _parse_cmdline(
@@ -287,32 +287,32 @@ def parse(
287287
@property
288288
def name(self):
289289
"""Getter for name."""
290-
if self.__executable.final_name is not None:
291-
return self.__executable.final_name
292-
return self.__executable.name
290+
if self.process_description.final_name is not None:
291+
return self.process_description.final_name
292+
return self.process_description.name
293293

294294
@property
295295
def cmd(self):
296296
"""Getter for cmd."""
297-
if self.__executable.final_cmd is not None:
298-
return self.__executable.final_cmd
299-
return self.__executable.cmd
297+
if self.process_description.final_cmd is not None:
298+
return self.process_description.final_cmd
299+
return self.process_description.cmd
300300

301301
@property
302302
def cwd(self):
303303
"""Getter for cwd."""
304-
if self.__executable.final_cwd is not None:
305-
return self.__executable.final_cwd
306-
return self.__executable.cwd
304+
if self.process_description.final_cwd is not None:
305+
return self.process_description.final_cwd
306+
return self.process_description.cwd
307307

308308
@property
309309
def env(self):
310310
"""Getter for env."""
311-
if self.__executable.final_env is not None:
312-
return self.__executable.final_env
313-
return self.__executable.env
311+
if self.process_description.final_env is not None:
312+
return self.process_description.final_env
313+
return self.process_description.env
314314

315315
@property
316316
def additional_env(self):
317317
"""Getter for additional_env."""
318-
return self.__executable.additional_env
318+
return self.process_description.additional_env

launch/launch/descriptions/executable.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@
3131
from typing import Optional
3232
from typing import Tuple
3333

34-
from launch.some_substitutions_type import SomeSubstitutionsType
35-
from launch.substitution import Substitution
36-
from launch.substitutions import LaunchConfiguration
37-
from launch.launch_context import LaunchContext
38-
from launch.utilities import normalize_to_list_of_substitutions
39-
from launch.utilities import perform_substitutions
34+
from ..action import Action
35+
from ..launch_context import LaunchContext
36+
from ..some_substitutions_type import SomeSubstitutionsType
37+
from ..substitution import Substitution
38+
from ..substitutions import LaunchConfiguration
39+
from ..utilities import normalize_to_list_of_substitutions
40+
from ..utilities import perform_substitutions
4041

4142
_executable_process_counter_lock = threading.Lock()
4243
_executable_process_counter = 0 # in Python3, this number is unbounded (no rollover)
@@ -166,7 +167,7 @@ def __expand_substitutions(self, context):
166167
with _executable_process_counter_lock:
167168
global _executable_process_counter
168169
_executable_process_counter += 1
169-
self.__final_name = f"{name}-{_executable_process_counter}"
170+
self.__final_name = f'{name}-{_executable_process_counter}'
170171
cwd = None
171172
if self.__cwd is not None:
172173
cwd = ''.join([context.perform_substitution(x) for x in self.__cwd])

launch/launch/event_handlers/on_process_exit.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ..some_actions_type import SomeActionsType
3232

3333
if TYPE_CHECKING:
34-
from ..actions import ExecuteProcess # noqa: F401
34+
from ..actions import ExecuteLocal # noqa: F401
3535

3636

3737
class OnProcessExit(BaseEventHandler):
@@ -45,15 +45,15 @@ class OnProcessExit(BaseEventHandler):
4545
def __init__(
4646
self,
4747
*,
48-
target_action: 'ExecuteProcess' = None,
48+
target_action: 'ExecuteLocal' = None,
4949
on_exit: Union[SomeActionsType,
5050
Callable[[ProcessExited, LaunchContext], Optional[SomeActionsType]]],
5151
**kwargs
5252
) -> None:
5353
"""Create an OnProcessExit event handler."""
54-
from ..actions import ExecuteProcess # noqa
55-
if not isinstance(target_action, (ExecuteProcess, type(None))):
56-
raise TypeError("OnProcessExit requires an 'ExecuteProcess' action as the target")
54+
from ..actions import ExecuteLocal # noqa
55+
if not isinstance(target_action, (ExecuteLocal, type(None))):
56+
raise TypeError("OnProcessExit requires an 'ExecuteLocal' action as the target")
5757
super().__init__(
5858
matcher=(
5959
lambda event: (
@@ -109,6 +109,6 @@ def matcher_description(self) -> Text:
109109
"""Return the string description of the matcher."""
110110
if self.__target_action is None:
111111
return 'event == ProcessExited'
112-
return 'event == ProcessExited and event.action == ExecuteProcess({})'.format(
112+
return 'event == ProcessExited and event.action == ExecuteLocal({})'.format(
113113
hex(id(self.__target_action))
114114
)

launch/launch/event_handlers/on_process_io.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ class OnProcessIO(BaseEventHandler):
3838
def __init__(
3939
self,
4040
*,
41-
target_action: Optional['ExecuteProcess'] = None,
41+
target_action: Optional['ExecuteLocal'] = None,
4242
on_stdin: Callable[[ProcessIO], Optional[SomeActionsType]] = None,
4343
on_stdout: Callable[[ProcessIO], Optional[SomeActionsType]] = None,
4444
on_stderr: Callable[[ProcessIO], Optional[SomeActionsType]] = None,
4545
**kwargs
4646
) -> None:
4747
"""Create an OnProcessIO event handler."""
48-
from ..actions import ExecuteProcess # noqa
49-
if not isinstance(target_action, (ExecuteProcess, type(None))):
50-
raise TypeError("OnProcessIO requires an 'ExecuteProcess' action as the target")
48+
from ..actions import ExecuteLocal # noqa
49+
if not isinstance(target_action, (ExecuteLocal, type(None))):
50+
raise TypeError("OnProcessIO requires an 'ExecuteLocal' action as the target")
5151
super().__init__(matcher=self._matcher, **kwargs)
5252
self.__target_action = target_action
5353
self.__on_stdin = on_stdin
@@ -95,6 +95,6 @@ def matcher_description(self) -> Text:
9595
"""Return the string description of the matcher."""
9696
if self.__target_action is None:
9797
return 'event issubclass of ProcessIO'
98-
return 'event issubclass of ProcessIO and event.action == ExecuteProcess({})'.format(
98+
return 'event issubclass of ProcessIO and event.action == ExecuteLocal({})'.format(
9999
hex(id(self.__target_action))
100100
)

launch/test/launch/test_executable.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,28 +20,59 @@
2020
#
2121
# This notice must appear in all copies of this file and its derivatives.
2222

23+
import os
24+
2325
from launch.descriptions.executable import Executable
2426
from launch.launch_context import LaunchContext
27+
from launch.substitutions import EnvironmentVariable
2528

2629

2730
def test_executable():
28-
exe = Executable(cmd="test")
31+
exe = Executable(cmd='test')
2932
assert exe is not None
3033

3134

3235
def test_cmd_string_in_list():
3336
exe = Executable(cmd=['ls "my/subdir/with spaces/"'])
3437
exe.apply_context(LaunchContext())
35-
assert all([a == b for a, b in zip(exe.final_cmd, ['ls "my/subdir/with spaces/"'])])
38+
assert all(a == b for a, b in zip(exe.final_cmd, ['ls "my/subdir/with spaces/"']))
3639

3740

3841
def test_cmd_strings_in_list():
3942
exe = Executable(cmd=['ls', '"my/subdir/with spaces/"'])
4043
exe.apply_context(LaunchContext())
41-
assert all([a == b for a, b in zip(exe.final_cmd, ['ls', '"my/subdir/with spaces/"'])])
44+
assert all(a == b for a, b in zip(exe.final_cmd, ['ls', '"my/subdir/with spaces/"']))
4245

4346

4447
def test_cmd_multiple_arguments_in_string():
4548
exe = Executable(cmd=['ls', '-opt1', '-opt2', '-opt3'])
4649
exe.apply_context(LaunchContext())
47-
assert all([a == b for a, b in zip(exe.final_cmd, ['ls', '-opt1', '-opt2', '-opt3'])])
50+
assert all(a == b for a, b in zip(exe.final_cmd, ['ls', '-opt1', '-opt2', '-opt3']))
51+
52+
def test_passthrough_properties():
53+
name = 'name'
54+
cwd = 'cwd'
55+
env = {'a': '1'}
56+
exe = Executable(cmd=['test'], name=name, cwd=cwd, env=env)
57+
exe.apply_context(LaunchContext())
58+
assert exe.final_name.startswith(name)
59+
assert exe.final_cwd == cwd
60+
assert exe.final_env == env
61+
62+
def test_substituted_properties():
63+
os.environ['EXECUTABLE_TEST_NAME'] = 'name'
64+
os.environ['EXECUTABLE_TEST_CWD'] = 'cwd'
65+
os.environ['EXECUTABLE_TEST_ENVVAR'] = 'var'
66+
os.environ['EXECUTABLE_TEST_ENVVAL'] = 'value'
67+
name = EnvironmentVariable('EXECUTABLE_TEST_NAME')
68+
cwd = EnvironmentVariable('EXECUTABLE_TEST_CWD')
69+
env = {EnvironmentVariable('EXECUTABLE_TEST_ENVVAR'): EnvironmentVariable('EXECUTABLE_TEST_ENVVAL')}
70+
exe = Executable(cmd=['test'], name=name, cwd=cwd, env=env)
71+
exe.apply_context(LaunchContext())
72+
assert exe.final_name.startswith('name')
73+
assert exe.final_cwd == 'cwd'
74+
assert exe.final_env == {'var': 'value'}
75+
del os.environ['EXECUTABLE_TEST_NAME']
76+
del os.environ['EXECUTABLE_TEST_CWD']
77+
del os.environ['EXECUTABLE_TEST_ENVVAR']
78+
del os.environ['EXECUTABLE_TEST_ENVVAL']
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Copyright 2021 Southwest Research Institute, All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# DISTRIBUTION A. Approved for public release; distribution unlimited.
16+
# OPSEC #4584.
17+
#
18+
# Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS
19+
# Part 252.227-7013 or 7014 (Feb 2014).
20+
#
21+
# This notice must appear in all copies of this file and its derivatives.
22+
23+
"""Tests for the ExecuteLocal Action."""
24+
25+
import os
26+
import sys
27+
28+
from launch import LaunchDescription
29+
from launch import LaunchService
30+
from launch.actions import ExecuteLocal
31+
from launch.actions import OpaqueFunction
32+
from launch.actions import Shutdown
33+
from launch.actions import TimerAction
34+
from launch.descriptions import Executable
35+
36+
import pytest
37+
38+
@pytest.mark.parametrize('test_input,expected', [
39+
(None, [True, False]),
40+
({'TEST_NEW_ENV': '2'}, [False, True])
41+
])
42+
def test_execute_process_with_env(test_input, expected):
43+
"""Test launching a process with an environment variable."""
44+
os.environ['TEST_CHANGE_CURRENT_ENV'] = '1'
45+
additional_env = {'TEST_PROCESS_WITH_ENV': 'Hello World'}
46+
executable = ExecuteLocal(
47+
process_description=Executable(
48+
cmd=[sys.executable, 'TEST_PROCESS_WITH_ENV'],
49+
env=test_input,
50+
additional_env=additional_env
51+
),
52+
output='screen'
53+
)
54+
ld = LaunchDescription([executable])
55+
ls = LaunchService()
56+
ls.include_launch_description(ld)
57+
assert 0 == ls.run()
58+
env = executable.process_details['env']
59+
assert env['TEST_PROCESS_WITH_ENV'] == 'Hello World'
60+
assert ('TEST_CHANGE_CURRENT_ENV' in env) is expected[0]
61+
if expected[0]:
62+
assert env['TEST_CHANGE_CURRENT_ENV'] == '1'
63+
assert ('TEST_NEW_ENV' in env) is expected[1]
64+
if expected[1]:
65+
assert env['TEST_NEW_ENV'] == '2'
66+
67+
68+
def test_execute_process_with_on_exit_behavior():
69+
"""Test a process' on_exit callback and actions are processed."""
70+
def on_exit_callback(event, context):
71+
on_exit_callback.called = True
72+
on_exit_callback.called = False
73+
74+
executable_with_on_exit_callback = ExecuteLocal(
75+
process_description=Executable(cmd=[sys.executable, '-c', "print('callback')"]),
76+
output='screen', on_exit=on_exit_callback
77+
)
78+
assert len(executable_with_on_exit_callback.get_sub_entities()) == 0
79+
80+
def on_exit_function(context):
81+
on_exit_function.called = True
82+
on_exit_function.called = False
83+
on_exit_action = OpaqueFunction(function=on_exit_function)
84+
executable_with_on_exit_action = ExecuteLocal(
85+
process_description=Executable(cmd=[sys.executable, '-c', "print('callback')"]),
86+
output='screen', on_exit=[on_exit_action]
87+
)
88+
assert executable_with_on_exit_action.get_sub_entities() == [on_exit_action]
89+
90+
ld = LaunchDescription([
91+
executable_with_on_exit_callback,
92+
executable_with_on_exit_action
93+
])
94+
ls = LaunchService()
95+
ls.include_launch_description(ld)
96+
assert 0 == ls.run()
97+
assert on_exit_callback.called
98+
assert on_exit_function.called
99+
100+
101+
def test_execute_process_with_respawn():
102+
"""Test launching a process with a respawn and respawn_delay attribute."""
103+
def on_exit_callback(event, context):
104+
on_exit_callback.called_count = on_exit_callback.called_count + 1
105+
on_exit_callback.called_count = 0
106+
107+
respawn_delay = 2.0
108+
shutdown_time = 3.0 # to shutdown the launch service, so that the process only respawn once
109+
expected_called_count = 2 # normal exit and respawn exit
110+
111+
def generate_launch_description():
112+
return LaunchDescription([
113+
114+
ExecuteLocal(
115+
process_description=Executable(cmd=[sys.executable, '-c', "print('action')"]),
116+
respawn=True, respawn_delay=respawn_delay, on_exit=on_exit_callback
117+
),
118+
119+
TimerAction(
120+
period=shutdown_time,
121+
actions=[
122+
Shutdown(reason='Timer expired')
123+
]
124+
)
125+
])
126+
127+
ls = LaunchService()
128+
ls.include_launch_description(generate_launch_description())
129+
assert 0 == ls.run()
130+
assert expected_called_count == on_exit_callback.called_count

0 commit comments

Comments
 (0)