Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit 855f612

Browse files
author
fred-labs
authored
add additional modifiers (#143)
1 parent d7750f7 commit 855f612

File tree

10 files changed

+188
-45
lines changed

10 files changed

+188
-45
lines changed

.github/workflows/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ RUN --mount=type=bind,source=.,target=/scenario_execution \
1313
tk \
1414
libgl1 && \
1515
xargs -a /scenario_execution/deb_requirements.txt apt-get install -y --no-install-recommends && \
16+
xargs -a /scenario_execution/libs/scenario_execution_kubernetes/deb_requirements.txt apt-get install -y --no-install-recommends && \
1617
rosdep update --rosdistro=${ROS_DISTRO} && \
1718
rosdep install --rosdistro=${ROS_DISTRO} --from-paths /scenario_execution/ --ignore-src -r -y && \
1819
rm -rf /var/lib/apt/lists/*

docs/libraries.rst

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,36 @@ Modifier to set a timeout for a sub-tree.
310310
-
311311
- Maximum number of permitted failures
312312

313+
``failure_is_running()``
314+
""""""""""""""""""""""""
315+
316+
Don't stop running.
317+
318+
``failure_is_success()``
319+
""""""""""""""""""""""""
320+
321+
Be positive, always succeed.
322+
323+
``running_is_failure()``
324+
""""""""""""""""""""""""
325+
326+
Got to be snappy! We want results...yesterday.
327+
328+
``running_is_success()``
329+
""""""""""""""""""""""""
330+
331+
Don't hang around...
332+
333+
``success_is_failure()``
334+
""""""""""""""""""""""""
335+
336+
Be depressed, always fail.
337+
338+
``success_is_running()``
339+
""""""""""""""""""""""""
340+
341+
The tickling never ends...
342+
313343

314344
Kubernetes
315345
----------
@@ -712,7 +742,7 @@ Checks for the state of a `lifecycle-managed <https://design.ros2.org/articles/n
712742
``assert_tf_moving()``
713743
""""""""""""""""""""""
714744

715-
Checks that a tf ``frame_id`` keeps moving in respect to a ``parent_frame_id``. If there is no movement within ``timeout`` the action ends, depending on ``fail_on_finish``, either with success or failure. Speeds below ``threshold_translation`` and ``threshold_rotation`` are discarded. By default the action waits for the first transform to get available before starting the timeout timer. This can be changed by setting ``wait_for_first_transform`` to ``false``. If the tf topics are not available on ``/tf`` and ``/tf_static`` you can specify a namespace by setting ``tf_topic_namespace``.
745+
Checks that a tf ``frame_id`` keeps moving in respect to a ``parent_frame_id``. If there is no movement within ``timeout`` the action with failure. Speeds below ``threshold_translation`` and ``threshold_rotation`` are discarded. By default the action waits for the first transform to get available before starting the timeout timer. This can be changed by setting ``wait_for_first_transform`` to ``false``. If the tf topics are not available on ``/tf`` and ``/tf_static`` you can specify a namespace by setting ``tf_topic_namespace``.
716746

717747
.. list-table::
718748
:widths: 15 15 5 65
@@ -743,10 +773,6 @@ Checks that a tf ``frame_id`` keeps moving in respect to a ``parent_frame_id``.
743773
- ``angular_rate``
744774
- ``0.01radps``
745775
- Rotational speed below this threshold is skipped.
746-
* - ``fail_on_finish``
747-
- ``bool``
748-
- ``true``
749-
- If true and there is no movement, the action fails. Otherwise it succeeds.
750776
* - ``wait_for_first_transform``
751777
- ``bool``
752778
- ``true``
@@ -763,7 +789,7 @@ Checks that a tf ``frame_id`` keeps moving in respect to a ``parent_frame_id``.
763789
``assert_topic_latency()``
764790
""""""""""""""""""""""""""
765791

766-
Check the latency of the specified topic (in system time). If the check with ``comparison_operator`` gets true, the action ends, depending on ``fail_on_finish``, either with success or failure.
792+
Check the latency of the specified topic (in system time). If the check with ``comparison_operator`` gets true, the action ends with failure.
767793

768794
.. list-table::
769795
:widths: 15 15 5 65
@@ -786,10 +812,6 @@ Check the latency of the specified topic (in system time). If the check with ``c
786812
- ``comparison_operator``
787813
- ``comparison_operator!le``
788814
- operator to compare latency time.
789-
* - ``fail_on_finish``
790-
- ``bool``
791-
- ``true``
792-
- If false action success, if comparison is true.
793815
* - ``rolling_average_count``
794816
- ``int``
795817
- ``1``

scenario_execution/scenario_execution/lib_osc/helpers.osc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,21 @@ modifier retry:
4444
modifier timeout:
4545
# modifier to set a timeout for a subtree
4646
duration: time # time to wait
47+
48+
modifier failure_is_running
49+
# don't stop running
50+
51+
modifier failure_is_success
52+
# be positive, always succeed
53+
54+
modifier running_is_failure
55+
# got to be snappy! We want results...yesterday
56+
57+
modifier running_is_success
58+
# don't hang around...
59+
60+
modifier success_is_failure
61+
# be depressed, always fail
62+
63+
modifier success_is_running
64+
# the tickling never ends...

scenario_execution/scenario_execution/model/model_to_py_tree.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,21 +204,34 @@ def compare_method_arguments(self, method, expected_args, behavior_name, node):
204204
return method_args, error_string
205205

206206
def create_decorator(self, node: ModifierDeclaration, resolved_values):
207-
available_modifiers = ["repeat", "inverter", "timeout", "retry"]
207+
available_modifiers = ["repeat", "inverter", "timeout", "retry", "failure_is_running", "failure_is_success",
208+
"running_is_failure", "running_is_success", "success_is_failure", "success_is_running"]
208209
if node.name not in available_modifiers:
209210
raise OSC2ParsingError(
210211
msg=f'Unknown modifier "{node.name}". Available modifiers {available_modifiers}.', context=node.get_ctx())
211212
parent = self.__cur_behavior.parent
212213
if parent:
213214
parent.children.remove(self.__cur_behavior)
214215
if node.name == "repeat":
215-
instance = py_trees.decorators.Repeat(name="Repeat", child=self.__cur_behavior, num_success=resolved_values["count"])
216+
instance = py_trees.decorators.Repeat(name="repeat", child=self.__cur_behavior, num_success=resolved_values["count"])
216217
elif node.name == "inverter":
217-
instance = py_trees.decorators.Inverter(name="Inverter", child=self.__cur_behavior)
218+
instance = py_trees.decorators.Inverter(name="inverter", child=self.__cur_behavior)
218219
elif node.name == "timeout":
219-
instance = py_trees.decorators.Timeout(name="Timeout", child=self.__cur_behavior, duration=resolved_values["duration"])
220+
instance = py_trees.decorators.Timeout(name="timeout", child=self.__cur_behavior, duration=resolved_values["duration"])
220221
elif node.name == "retry":
221-
instance = py_trees.decorators.Retry(name="Retry", child=self.__cur_behavior, num_failures=resolved_values["count"])
222+
instance = py_trees.decorators.Retry(name="retry", child=self.__cur_behavior, num_failures=resolved_values["count"])
223+
elif node.name == "failure_is_running":
224+
instance = py_trees.decorators.FailureIsRunning(name="failure_is_running", child=self.__cur_behavior)
225+
elif node.name == "failure_is_success":
226+
instance = py_trees.decorators.FailureIsSuccess(name="failure_is_success", child=self.__cur_behavior)
227+
elif node.name == "running_is_failure":
228+
instance = py_trees.decorators.RunningIsFailure(name="running_is_failure", child=self.__cur_behavior)
229+
elif node.name == "running_is_success":
230+
instance = py_trees.decorators.RunningIsSuccess(name="running_is_success", child=self.__cur_behavior)
231+
elif node.name == "success_is_failure":
232+
instance = py_trees.decorators.SuccessIsFailure(name="success_is_failure", child=self.__cur_behavior)
233+
elif node.name == "success_is_running":
234+
instance = py_trees.decorators.SuccessIsRunning(name="success_is_running", child=self.__cur_behavior)
222235
else:
223236
raise ValueError('unknown.')
224237

scenario_execution/test/test_scenario_modifier.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,103 @@ def test_modifier_in_scenario_last_fails(self):
376376
"""
377377
self.execute(scenario_content)
378378
self.assertFalse(self.scenario_execution.process_results())
379+
380+
def test_modifier_failure_is_running(self):
381+
scenario_content = """
382+
import osc.helpers
383+
384+
scenario test:
385+
do parallel:
386+
serial:
387+
run_process("false") with:
388+
failure_is_running()
389+
emit fail
390+
serial:
391+
wait elapsed(2s)
392+
emit end
393+
"""
394+
self.execute(scenario_content)
395+
self.assertTrue(self.scenario_execution.process_results())
396+
397+
def test_modifier_failure_is_success(self):
398+
scenario_content = """
399+
import osc.helpers
400+
401+
scenario test:
402+
do parallel:
403+
serial:
404+
run_process("false") with:
405+
failure_is_success()
406+
emit end
407+
serial:
408+
wait elapsed(2s)
409+
emit fail
410+
"""
411+
self.execute(scenario_content)
412+
self.assertTrue(self.scenario_execution.process_results())
413+
414+
def test_modifier_running_is_failure(self):
415+
scenario_content = """
416+
import osc.helpers
417+
418+
scenario test:
419+
do parallel:
420+
serial:
421+
run_process("sleep 10") with:
422+
running_is_failure()
423+
serial:
424+
wait elapsed(5s)
425+
emit end
426+
"""
427+
self.execute(scenario_content)
428+
self.assertFalse(self.scenario_execution.process_results())
429+
430+
def test_modifier_running_is_success(self):
431+
scenario_content = """
432+
import osc.helpers
433+
434+
scenario test:
435+
do parallel:
436+
serial:
437+
run_process("sleep 10") with:
438+
running_is_success()
439+
emit end
440+
serial:
441+
wait elapsed(5s)
442+
emit fail
443+
"""
444+
self.execute(scenario_content)
445+
self.assertTrue(self.scenario_execution.process_results())
446+
447+
def test_modifier_success_is_failure(self):
448+
scenario_content = """
449+
import osc.helpers
450+
451+
scenario test:
452+
do parallel:
453+
serial:
454+
run_process("true") with:
455+
success_is_failure()
456+
serial:
457+
wait elapsed(5s)
458+
emit end
459+
"""
460+
self.execute(scenario_content)
461+
self.assertFalse(self.scenario_execution.process_results())
462+
463+
def test_modifier_success_is_running(self):
464+
scenario_content = """
465+
import osc.helpers
466+
467+
scenario test:
468+
do parallel:
469+
serial:
470+
run_process("true") with:
471+
success_is_running()
472+
emit fail
473+
serial:
474+
wait elapsed(5s)
475+
emit end
476+
"""
477+
self.execute(scenario_content)
478+
self.assertTrue(self.scenario_execution.process_results())

scenario_execution_ros/scenario_execution_ros/actions/assert_tf_moving.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,11 @@
2727

2828
class AssertTfMoving(BaseAction):
2929

30-
def __init__(self, frame_id: str, parent_frame_id: str, timeout: int, threshold_translation: float, threshold_rotation: float, fail_on_finish: bool, wait_for_first_transform: bool, tf_topic_namespace: str, use_sim_time: bool):
30+
def __init__(self, frame_id: str, parent_frame_id: str, timeout: int, threshold_translation: float, threshold_rotation: float, wait_for_first_transform: bool, tf_topic_namespace: str, use_sim_time: bool):
3131
super().__init__()
3232
self.frame_id = frame_id
3333
self.parent_frame_id = parent_frame_id
3434
self.timeout = timeout
35-
self.fail_on_finish = fail_on_finish
3635
self.threshold_translation = threshold_translation
3736
self.threshold_rotation = threshold_rotation
3837
self.wait_for_first_transform = wait_for_first_transform
@@ -93,13 +92,9 @@ def update(self) -> py_trees.common.Status:
9392
if not self.start_timeout:
9493
self.timer = time.time()
9594
self.start_timeout = True
96-
elif now - self.timer > self.timeout and self.fail_on_finish:
95+
elif now - self.timer > self.timeout:
9796
self.feedback_message = f"Timeout: No movement detected for {self.timeout} seconds." # pylint: disable= attribute-defined-outside-init
9897
result = py_trees.common.Status.FAILURE
99-
elif now - self.timer > self.timeout and not self.fail_on_finish:
100-
self.logger.error("Timeout: No movement detected for {} seconds.".format(self.timeout))
101-
self.feedback_message = f"Timeout: No movement detected for {self.timeout} seconds." # pylint: disable= attribute-defined-outside-init
102-
result = py_trees.common.Status.SUCCESS
10398
else:
10499
self.feedback_message = "Frame is not moving." # pylint: disable= attribute-defined-outside-init
105100
result = py_trees.common.Status.RUNNING

scenario_execution_ros/scenario_execution_ros/actions/assert_topic_latency.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,13 @@
2626

2727
class AssertTopicLatency(BaseAction):
2828

29-
def __init__(self, topic_name: str, topic_type: str, latency: float, comparison_operator: bool, fail_on_finish: bool, rolling_average_count: int, wait_for_first_message: bool):
29+
def __init__(self, topic_name: str, topic_type: str, latency: float, comparison_operator: bool, rolling_average_count: int, wait_for_first_message: bool):
3030
super().__init__()
3131
self.topic_name = topic_name
3232
self.topic_type = topic_type
3333
self.latency = latency
3434
self.comparison_operator_feedback = comparison_operator[0]
3535
self.comparison_operator = get_comparison_operator(comparison_operator)
36-
self.fail_on_finish = fail_on_finish
3736
self.rolling_average_count = rolling_average_count
3837
self.rolling_average_count_queue = deque(maxlen=rolling_average_count)
3938
self.wait_for_first_message = wait_for_first_message
@@ -86,12 +85,9 @@ def update(self) -> py_trees.common.Status:
8685
if self.comparison_operator(self.average_latency, self.latency):
8786
result = py_trees.common.Status.RUNNING
8887
self.feedback_message = f'Latency within range: expected {self.comparison_operator_feedback} {self.latency} s, actual {self.average_latency} s' # pylint: disable= attribute-defined-outside-init
89-
if not self.comparison_operator(self.average_latency, self.latency) and self.fail_on_finish:
88+
else:
9089
result = py_trees.common.Status.FAILURE
9190
self.feedback_message = f'Latency not within range: expected {self.comparison_operator_feedback} {self.latency} s, actual {self.average_latency} s' # pylint: disable= attribute-defined-outside-init
92-
elif not self.comparison_operator(self.average_latency, self.latency):
93-
result = py_trees.common.Status.SUCCESS
94-
self.feedback_message = f'Latency not within range: expected {self.comparison_operator_feedback} {self.latency} s, actual {self.average_latency} s' # pylint: disable= attribute-defined-outside-init
9591
else:
9692
result = py_trees.common.Status.RUNNING
9793
return result

scenario_execution_ros/scenario_execution_ros/lib_osc/ros.osc

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,24 @@ action assert_lifecycle_state:
4242
keep_running: bool = true # if true, the action keeps running while the last state in the state_sequence remains
4343

4444
action assert_tf_moving:
45-
# Checks that a tf frame_id keeps moving in respect to a parent_frame_id. If there is no movement within timeout the action ends,
46-
# depending on fail_on_finish, either with success or failure. Speeds below threshold_translation and threshold_rotation
47-
# are discarded. By default the action waits for the first transform to get available before starting the timeout timer.
48-
# This can be changed by setting wait_for_first_transform to false. If the tf topics are not available on /tf and /tf_static
49-
# you can specify a namespace by setting tf_topic_namespace.
45+
# Checks that a tf frame_id keeps moving in respect to a parent_frame_id. If there is no movement within timeout the action ends with failure.
46+
# Speeds below threshold_translation and threshold_rotation are discarded. By default the action waits for the first transform to get available
47+
# before starting the timeout timer. This can be changed by setting wait_for_first_transform to false. If the tf topics are not available
48+
# on /tf and /tf_static you can specify a namespace by setting tf_topic_namespace.
5049
frame_id: string # frame_id to check for movement
5150
parent_frame_id: string = "map" # parent frame_id against which the movement is checked
5251
timeout: time # timeout without movement
5352
threshold_translation: speed = 0.01mps # translation speed, below this threshold is skipped
5453
threshold_rotation: angular_rate = 0.01radps # rotational speed, below this threshold is skipped
55-
fail_on_finish: bool = true # if false, the action should success if no movement
5654
wait_for_first_transform: bool = true # start measuring with the first received message
5755
tf_topic_namespace: string = '' # if set, it's used as namespace
5856
use_sim_time: bool = false # in simulation, we need to look up the transform at a different time as the scenario execution node is not allowed to use the sim time
5957

6058
action assert_topic_latency:
61-
# Check the latency of the specified topic (in system time). If the check with comparison_operator gets true, the action ends, depending on fail_on_finish, either with success or failure.
59+
# Check the latency of the specified topic (in system time). If the check with comparison_operator gets true, the action ends with failure.
6260
topic_name: string # topic name to wait for message
6361
latency: time # the time to compare against
6462
comparison_operator: comparison_operator = comparison_operator!le # the comparison is done using the python operator module
65-
fail_on_finish: bool = true # if false, the action should succeed if comparison is true
6663
rolling_average_count: int = 1 # the check is done aganist the rolling average over x elements
6764
wait_for_first_message: bool = true # start measuring with the first received message
6865
topic_type: string # class of message type, only required when wait_for_first_message is set to false (e.g. std_msgs.msg.String)

scenario_execution_ros/test/test_assert_tf_moving.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ def tearDown(self):
123123
# DEFAULT VALUES
124124
# threshold_translation: 0.01 mps (meters per second)
125125
# threshold_rotation: 0.01 radps (radians per second)
126-
# fail_on_finish: True
127126
# wait_for_first_transform: True
128127
# tf_topic_namespace: (optional)
129128
# use_sim_time: (optional)
@@ -140,7 +139,7 @@ def tearDown(self):
140139
# Case 4: Test with threshold_translation set to 1 mps. The test fails with timeout as the average threshold of the robot_moving frame is less than 1 mps (meters per second).
141140
# Case 5: Test with threshold_rotation set to 5 radps. The test fails with timeout as the average threshold of the robot_rotating frame is less than 5 radps (radians per second).
142141

143-
# 3. fail_on_finish: False
142+
# 3. failure_is_success
144143
# Case 6: Test succeeds if no movement is observed between frames.
145144

146145
# 4. wait_for_first_transform: False
@@ -161,6 +160,7 @@ def test_case_1(self):
161160
self.execute(scenario_content)
162161
self.assertFalse(self.scenario_execution_ros.process_results())
163162

163+
@unittest.skip(reason="unstable on CI")
164164
def test_case_2(self):
165165
scenario_content = """
166166
import osc.ros
@@ -222,13 +222,14 @@ def test_case_5(self):
222222

223223
def test_case_6(self):
224224
scenario_content = """
225+
import osc.helpers
225226
import osc.ros
226227
scenario test_assert_tf_moving:
227228
do serial:
228229
assert_tf_moving(
229230
frame_id: 'robot',
230-
timeout: 2s,
231-
fail_on_finish: false)
231+
timeout: 2s) with:
232+
failure_is_success()
232233
emit end
233234
"""
234235
self.execute(scenario_content)

0 commit comments

Comments
 (0)