From 1f997f4054f07e8e4fd2590aecd436aa86eee84c Mon Sep 17 00:00:00 2001 From: "Pasch, Frederik" Date: Tue, 17 Sep 2024 13:04:23 +0200 Subject: [PATCH 1/3] Add increment, decrement action, fix check --- docs/libraries.rst | 41 ++++ .../scenario_execution/actions/base_action.py | 22 +- .../scenario_execution/actions/decrement.py | 39 ++++ .../scenario_execution/actions/increment.py | 39 ++++ .../scenario_execution/lib_osc/helpers.osc | 8 + .../model/model_blackboard.py | 36 ++- .../model/model_to_py_tree.py | 5 +- .../scenario_execution/model/types.py | 47 ++-- scenario_execution/setup.py | 2 + .../test/test_action_decrement.py | 152 +++++++++++++ .../test/test_action_increment.py | 209 ++++++++++++++++++ .../actions/conversions.py | 6 +- .../actions/ros_bag_play.py | 6 +- .../actions/ros_topic_check_data.py | 5 +- .../actions/ros_topic_check_data_external.py | 19 +- .../scenario_execution_ros/lib_osc/ros.osc | 4 +- .../test/test_blackboard_write.py | 4 + .../test/test_expression_with_var.py | 2 +- 18 files changed, 585 insertions(+), 61 deletions(-) create mode 100644 scenario_execution/scenario_execution/actions/decrement.py create mode 100644 scenario_execution/scenario_execution/actions/increment.py create mode 100644 scenario_execution/test/test_action_decrement.py create mode 100644 scenario_execution/test/test_action_increment.py diff --git a/docs/libraries.rst b/docs/libraries.rst index 301bba9d..4f2be592 100644 --- a/docs/libraries.rst +++ b/docs/libraries.rst @@ -285,6 +285,47 @@ Be depressed, always fail. The tickling never ends... + +``decrement()`` +^^^^^^^^^^^^^^^ + +Decrement the value of a variable. + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``target_variable`` + - ``variable`` + - + - Variable to decrement + + +``increment()`` +^^^^^^^^^^^^^^^ + +Increment the value of a variable. + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``target_variable`` + - ``variable`` + - + - Variable to increment + + ``log()`` ^^^^^^^^^ diff --git a/scenario_execution/scenario_execution/actions/base_action.py b/scenario_execution/scenario_execution/actions/base_action.py index 164b68e7..c7c238b1 100644 --- a/scenario_execution/scenario_execution/actions/base_action.py +++ b/scenario_execution/scenario_execution/actions/base_action.py @@ -15,7 +15,6 @@ # SPDX-License-Identifier: Apache-2.0 import py_trees -from scenario_execution.model.types import ParameterDeclaration, ScenarioDeclaration from scenario_execution.model.error import OSC2Error import inspect @@ -55,8 +54,11 @@ def initialise(self): if self.resolve_variable_reference_arguments_in_execute: final_args = self._model.get_resolved_value(self.get_blackboard_client(), skip_keys=self.execute_skip_args) else: - final_args = self._model.get_resolved_value_with_variable_references( - self.get_blackboard_client(), skip_keys=self.execute_skip_args) + try: + final_args = self._model.get_resolved_value_with_variable_references( + self.get_blackboard_client(), skip_keys=self.execute_skip_args) + except ValueError as e: + raise ActionError(f"Error initializing action: {e}", action=self) from e if self._model.actor: final_args["associated_actor"] = self._model.actor.get_resolved_value(self.get_blackboard_client()) @@ -71,24 +73,14 @@ def _set_base_properities(self, name, model, logger): def get_blackboard_client(self): if self.blackboard: return self.blackboard - - def get_blackboard_namespace(node: ParameterDeclaration): - parent = node.get_parent() - while parent is not None and not isinstance(parent, ScenarioDeclaration): - parent = parent.get_parent() - if parent: - return parent.name - else: - return None - - self.blackboard = self.attach_blackboard_client(name=self.name, namespace=get_blackboard_namespace(self._model)) + self.blackboard = self.attach_blackboard_client(name=self.name) return self.blackboard def register_access_to_associated_actor_variable(self, variable_name): if not self._model.actor: raise ActionError("Model does not have 'actor'.", action=self) blackboard = self.get_blackboard_client() - model_blackboard_name = self._model.actor.get_fully_qualified_var_name(include_scenario=False) + model_blackboard_name = self._model.actor.get_qualified_name() model_blackboard_name += "/" + variable_name blackboard.register_key(model_blackboard_name, access=py_trees.common.Access.WRITE) return model_blackboard_name diff --git a/scenario_execution/scenario_execution/actions/decrement.py b/scenario_execution/scenario_execution/actions/decrement.py new file mode 100644 index 00000000..4f74e317 --- /dev/null +++ b/scenario_execution/scenario_execution/actions/decrement.py @@ -0,0 +1,39 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees # pylint: disable=import-error +from scenario_execution.actions.base_action import BaseAction, ActionError +from scenario_execution.model.types import VariableReference + + +class Decrement(BaseAction): + """ + Class to decrement the value of a variable + """ + + def __init__(self): + super().__init__(resolve_variable_reference_arguments_in_execute=False) + self.target_variable = None + + def execute(self, target_variable: object): + if not isinstance(target_variable, VariableReference): + raise ActionError( + f"'target_variable' is expected to be a variable reference but is {type(target_variable).__name__}.", action=self) + self.target_variable = target_variable + + def update(self) -> py_trees.common.Status: + self.target_variable.set_value(self.target_variable.get_value() - 1) + return py_trees.common.Status.SUCCESS diff --git a/scenario_execution/scenario_execution/actions/increment.py b/scenario_execution/scenario_execution/actions/increment.py new file mode 100644 index 00000000..c4e03547 --- /dev/null +++ b/scenario_execution/scenario_execution/actions/increment.py @@ -0,0 +1,39 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees # pylint: disable=import-error +from scenario_execution.actions.base_action import BaseAction, ActionError +from scenario_execution.model.types import VariableReference + + +class Increment(BaseAction): + """ + Class to increment the value of a variable + """ + + def __init__(self): + super().__init__(resolve_variable_reference_arguments_in_execute=False) + self.target_variable = None + + def execute(self, target_variable: object): + if not isinstance(target_variable, VariableReference): + raise ActionError( + f"'target_variable' is expected to be a variable reference but is {type(target_variable).__name__}.", action=self) + self.target_variable = target_variable + + def update(self) -> py_trees.common.Status: + self.target_variable.set_value(self.target_variable.get_value() + 1) + return py_trees.common.Status.SUCCESS diff --git a/scenario_execution/scenario_execution/lib_osc/helpers.osc b/scenario_execution/scenario_execution/lib_osc/helpers.osc index 948fc80b..a632aa74 100644 --- a/scenario_execution/scenario_execution/lib_osc/helpers.osc +++ b/scenario_execution/scenario_execution/lib_osc/helpers.osc @@ -9,6 +9,14 @@ enum signal: [ sigterm = 15 ] +action increment: + # Increment the value of a variable + target_variable: string # variable to increment + +action decrement: + # Decrement the value of a variable + target_variable: string # variable to decrement + action log: # Print out a message msg: string # Message to print diff --git a/scenario_execution/scenario_execution/model/model_blackboard.py b/scenario_execution/scenario_execution/model/model_blackboard.py index de59e7d3..2a02f8ba 100644 --- a/scenario_execution/scenario_execution/model/model_blackboard.py +++ b/scenario_execution/scenario_execution/model/model_blackboard.py @@ -16,7 +16,7 @@ import py_trees -from scenario_execution.model.types import ParameterDeclaration, StructuredDeclaration, VariableDeclaration +from scenario_execution.model.types import ParameterDeclaration, StructuredDeclaration, VariableDeclaration, ScenarioDeclaration, Declaration from scenario_execution.model.model_base_visitor import ModelBaseVisitor from scenario_execution.model.error import OSC2ParsingError @@ -47,12 +47,32 @@ def __init__(self, logger, tree) -> None: self.blackboard = tree.attach_blackboard_client(name="ModelToPyTree") def visit_parameter_declaration(self, node: ParameterDeclaration): - super().visit_parameter_declaration(node) parameter_type = node.get_type()[0] - if isinstance(parameter_type, StructuredDeclaration): - for variable_dec in parameter_type.find_children_of_type(VariableDeclaration): - prefix = node.get_fully_qualified_var_name(include_scenario=True) - blackboard_var_name = prefix + "/" + variable_dec.name - self.blackboard.register_key(blackboard_var_name, access=py_trees.common.Access.WRITE) - setattr(self.blackboard, blackboard_var_name, variable_dec.get_resolved_value()) + if isinstance(parameter_type, StructuredDeclaration) and self.needs_blackboard_entry(node): + self.create_blackboard_entries(parameter_type, node.get_qualified_name()) + + def create_blackboard_entries(self, elem, prefix): + for variable_dec in elem.find_children_of_type(VariableDeclaration): + fqn = prefix + "/" + variable_dec.name + self.blackboard.register_key(fqn, access=py_trees.common.Access.WRITE) + setattr(self.blackboard, fqn, variable_dec.get_resolved_value()) + + for child in elem.find_children_of_type(ParameterDeclaration): + child_type = child.get_type()[0] + if isinstance(child_type, Declaration): + self.create_blackboard_entries(child_type, prefix + "/" + child.name) + + def needs_blackboard_entry(self, node): + current = node.get_parent() + while current: + if isinstance(current, ScenarioDeclaration): + return True + current = current.get_parent() + return False + + def visit_variable_declaration(self, node: VariableDeclaration): + if self.needs_blackboard_entry(node): + blackboard_var_name = node.get_fully_qualified_var_name() + self.blackboard.register_key(blackboard_var_name, access=py_trees.common.Access.WRITE) + setattr(self.blackboard, blackboard_var_name, node.get_resolved_value()) diff --git a/scenario_execution/scenario_execution/model/model_to_py_tree.py b/scenario_execution/scenario_execution/model/model_to_py_tree.py index f41a30e5..c6457e61 100644 --- a/scenario_execution/scenario_execution/model/model_to_py_tree.py +++ b/scenario_execution/scenario_execution/model/model_to_py_tree.py @@ -104,7 +104,7 @@ def update(self): class ExpressionBehavior(BaseAction): # py_trees.behaviour.Behaviour): def __init__(self, name: "ExpressionBehavior", expression: Expression, model, logger): - super().__init__() + super().__init__(resolve_variable_reference_arguments_in_execute=False) self._set_base_properities(name, model, logger) self.expression = expression @@ -146,8 +146,7 @@ def visit_scenario_declaration(self, node: ScenarioDeclaration): self.__cur_behavior.name = scenario_name self.blackboard = self.__cur_behavior.attach_blackboard_client( - name="ModelToPyTree", - namespace=scenario_name) + name="ModelToPyTree") super().visit_scenario_declaration(node) diff --git a/scenario_execution/scenario_execution/model/types.py b/scenario_execution/scenario_execution/model/types.py index 599e2d60..253cb9d7 100644 --- a/scenario_execution/scenario_execution/model/types.py +++ b/scenario_execution/scenario_execution/model/types.py @@ -968,16 +968,6 @@ def accept(self, visitor): else: return visitor.visit_children(self) - def get_fully_qualified_var_name(self, include_scenario): - name = self.name - parent = self.get_parent() - while parent and not isinstance(parent, ScenarioDeclaration): - name = parent.name + "/" + name - parent = parent.get_parent() - if include_scenario and parent and parent.name: - name = parent.name + "/" + name - return name - class ParameterReference(ModelElement): @@ -1277,6 +1267,14 @@ def accept(self, visitor): else: return visitor.visit_children(self) + def get_fully_qualified_var_name(self): + name = self.name + parent = self.get_parent() + while parent: + name = parent.name + "/" + name + parent = parent.get_parent() + return name + class KeepConstraintDeclaration(Declaration): @@ -2245,20 +2243,29 @@ def get_type_string(self): return self.ref.get_type_string() def get_blackboard_reference(self, blackboard): - if not isinstance(self.ref, list) or len(self.ref) == 0: - raise ValueError("Variable Reference only supported if reference is list with at least one element") - if not isinstance(self.ref[0], ParameterDeclaration): - raise ValueError("Variable Reference only supported if reference is part of a parameter declaration") - fqn = self.ref[0].get_fully_qualified_var_name(include_scenario=False) - if blackboard is None: - raise ValueError("Variable Reference found, but no blackboard client available.") - for sub_elem in self.ref[1:]: - fqn += "/" + sub_elem.name + if isinstance(self.ref, list): + if len(self.ref) == 0: + raise ValueError("Variable Reference only supported if reference is list with at least one element") + if not isinstance(self.ref[0], ParameterDeclaration): + raise ValueError("Variable Reference only supported if reference is part of a parameter declaration") + fqn = self.ref[0].get_qualified_name() + var_found = False + for elem in self.ref[1:]: + if isinstance(elem, VariableDeclaration): + var_found = True + fqn += "/" + elem.name + if not var_found: + raise ValueError(f"No variable found in '{fqn}.") + elif isinstance(self.ref, VariableDeclaration): + fqn = self.ref.get_fully_qualified_var_name() + else: + raise ValueError(f"Only references to VariableDeclaration supported, not '{type(self.ref).__name__}'.") + blackboard.register_key(fqn, access=py_trees.common.Access.WRITE) return VariableReference(blackboard, fqn) def get_variable_reference(self, blackboard): - if isinstance(self.ref, list) and any(isinstance(x, VariableDeclaration) for x in self.ref): + if (isinstance(self.ref, list) and any(isinstance(x, VariableDeclaration) for x in self.ref)) or isinstance(self.ref, VariableDeclaration): return self.get_blackboard_reference(blackboard) else: return None diff --git a/scenario_execution/setup.py b/scenario_execution/setup.py index cdcd6ec4..afa6da22 100644 --- a/scenario_execution/setup.py +++ b/scenario_execution/setup.py @@ -70,6 +70,8 @@ 'scenario_execution = scenario_execution.scenario_execution_base:main', ], 'scenario_execution.actions': [ + 'increment = scenario_execution.actions.increment:Increment', + 'decrement = scenario_execution.actions.decrement:Decrement', 'log = scenario_execution.actions.log:Log', 'run_process = scenario_execution.actions.run_process:RunProcess', ], diff --git a/scenario_execution/test/test_action_decrement.py b/scenario_execution/test/test_action_decrement.py new file mode 100644 index 00000000..974e328f --- /dev/null +++ b/scenario_execution/test/test_action_decrement.py @@ -0,0 +1,152 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import unittest +import py_trees +from scenario_execution.actions.base_action import ActionError +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.model.model_blackboard import create_py_tree_blackboard +from scenario_execution.utils.logging import Logger +from antlr4.InputStream import InputStream + + +class TestScenarioExecutionSuccess(unittest.TestCase): + # pylint: disable=missing-function-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.scenario_execution = ScenarioExecution(debug=False, + log_model=False, + live_tree=False, + scenario_file='test', + output_dir='') + self.tree = py_trees.composites.Sequence(name="", memory=True) + + def execute(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + create_py_tree_blackboard(model, self.tree, self.parser.logger, False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_fail_non_variable(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(3s) + my_var: int = 0 + do serial: + decrement(my_var) +""" + self.assertRaises(ActionError, self.execute, scenario_content) + self.assertFalse(self.scenario_execution.process_results()) + + def test_fail_unknown_variable(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(3s) + do serial: + decrement(UNKNOWN) +""" + self.assertRaises(ValueError, self.execute, scenario_content) + + def test_success_direct_var(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(5s) + var my_var: int = 1 + do parallel: + serial: + decrement(my_var) + decrement(my_var) + wait my_var == -2 + serial: + wait elapsed(3s) + decrement(my_var) +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_fail_actor_non_variable(self): + scenario_content = """ +import osc.helpers + +actor test_actor: + my_var: int = 42 + +scenario test: + timeout(3s) + my_actor: test_actor + do serial: + decrement(my_actor.my_var) +""" + self.assertRaises(ActionError, self.execute, scenario_content) + self.assertFalse(self.scenario_execution.process_results()) + + def test_success_actor_var(self): + scenario_content = """ +import osc.helpers + +actor test_actor: + var my_var: int = 0 + +scenario test: + timeout(5s) + my_actor: test_actor + do parallel: + serial: + decrement(my_actor.my_var) + decrement(my_actor.my_var) + wait my_actor.my_var == -3 + serial: + wait elapsed(3s) + decrement(my_actor.my_var) +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_success_var_in_actor_param_repeat(self): + scenario_content = """ +import osc.helpers + +struct test_stru: + var my_sub_var: int = 16 + +actor test_actor: + my_struct: test_stru + +scenario test_scenario: + timeout(10s) + my_actor: test_actor + do parallel: + serial: + repeat() + decrement(my_actor.my_struct.my_sub_var) + wait elapsed(0.5s) + serial: + wait my_actor.my_struct.my_sub_var == 10 + emit end +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) diff --git a/scenario_execution/test/test_action_increment.py b/scenario_execution/test/test_action_increment.py new file mode 100644 index 00000000..251ecd4e --- /dev/null +++ b/scenario_execution/test/test_action_increment.py @@ -0,0 +1,209 @@ +# Copyright (C) 2024 Intel Corporation +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 + +import unittest +import py_trees +from scenario_execution.actions.base_action import ActionError +from scenario_execution import ScenarioExecution +from scenario_execution.model.osc2_parser import OpenScenario2Parser +from scenario_execution.model.model_to_py_tree import create_py_tree +from scenario_execution.model.model_blackboard import create_py_tree_blackboard +from scenario_execution.utils.logging import Logger +from antlr4.InputStream import InputStream + + +class TestScenarioExecutionSuccess(unittest.TestCase): + # pylint: disable=missing-function-docstring + + def setUp(self) -> None: + self.parser = OpenScenario2Parser(Logger('test', False)) + self.scenario_execution = ScenarioExecution(debug=False, + log_model=False, + live_tree=False, + scenario_file='test', + output_dir='') + self.tree = py_trees.composites.Sequence(name="", memory=True) + + def execute(self, scenario_content): + parsed_tree = self.parser.parse_input_stream(InputStream(scenario_content)) + model = self.parser.create_internal_model(parsed_tree, self.tree, "test.osc", False) + create_py_tree_blackboard(model, self.tree, self.parser.logger, False) + self.tree = create_py_tree(model, self.tree, self.parser.logger, False) + self.scenario_execution.tree = self.tree + self.scenario_execution.run() + + def test_fail_non_variable(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(3s) + my_var: int = 0 + do serial: + increment(my_var) +""" + self.assertRaises(ActionError, self.execute, scenario_content) + self.assertFalse(self.scenario_execution.process_results()) + + def test_fail_unknown_variable(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(3s) + do serial: + increment(UNKNOWN) +""" + self.assertRaises(ValueError, self.execute, scenario_content) + + def test_success_direct_var_init(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(3s) + var my_var: int = 0 + do serial: + wait my_var == 0 +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_success_direct_var_uninitialized(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(2s) + var my_var: int + do serial: + wait my_var == 42 +""" + self.execute(scenario_content) + self.assertFalse(self.scenario_execution.process_results()) + + def test_success_direct_var(self): + scenario_content = """ +import osc.helpers + +scenario test: + timeout(5s) + var my_var: int = 0 + do parallel: + serial: + increment(my_var) + increment(my_var) + wait my_var == 3 + serial: + wait elapsed(3s) + increment(my_var) +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_fail_actor_non_variable(self): + scenario_content = """ +import osc.helpers + +actor test_actor: + my_var: int = 42 + +scenario test: + timeout(3s) + my_actor: test_actor + do serial: + increment(my_actor.my_var) +""" + self.assertRaises(ActionError, self.execute, scenario_content) + self.assertFalse(self.scenario_execution.process_results()) + + def test_success_actor_var_init(self): + scenario_content = """ +import osc.helpers + +actor test_actor: + var my_var: int = 42 + +scenario test: + timeout(5s) + my_actor: test_actor + do serial: + wait my_actor.my_var == 42 +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_success_actor_var(self): + scenario_content = """ +import osc.helpers + +actor test_actor: + var my_var: int = 0 + +scenario test: + timeout(5s) + my_actor: test_actor + do parallel: + serial: + increment(my_actor.my_var) + increment(my_actor.my_var) + wait my_actor.my_var == 3 + serial: + wait elapsed(3s) + increment(my_actor.my_var) +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_success_var_in_actor_param(self): + scenario_content = """ +import osc.helpers + +struct test_stru: + var my_sub_var: int = 22 + +actor test_actor: + my_struct: test_stru + +scenario test_scenario: + timeout(3s) + my_actor: test_actor + do serial: + increment(my_actor.my_struct.my_sub_var) + wait my_actor.my_struct.my_sub_var == 23 +""" + self.execute(scenario_content) + self.assertTrue(self.scenario_execution.process_results()) + + def test_fail_var_in_actor_param(self): + scenario_content = """ +import osc.helpers + +struct test_stru: + my_sub_var: int = 22 + +actor test_actor: + my_struct: test_stru + +scenario test_scenario: + timeout(3s) + my_actor: test_actor + do serial: + increment(my_actor.my_struct.my_sub_var) + wait my_actor.my_struct.my_sub_var == 23 +""" + self.assertRaises(ActionError, self.execute, scenario_content) + self.assertFalse(self.scenario_execution.process_results()) diff --git a/scenario_execution_ros/scenario_execution_ros/actions/conversions.py b/scenario_execution_ros/scenario_execution_ros/actions/conversions.py index a18744c0..b50eea61 100644 --- a/scenario_execution_ros/scenario_execution_ros/actions/conversions.py +++ b/scenario_execution_ros/scenario_execution_ros/actions/conversions.py @@ -18,7 +18,7 @@ import operator import importlib -from rclpy.qos import QoSPresetProfiles +from rclpy.qos import QoSPresetProfiles, ReliabilityPolicy def get_ros_message_type(message_type_string): @@ -46,6 +46,10 @@ def get_qos_preset_profile(qos_profile): return QoSPresetProfiles.SERVICE_DEFAULT.value # pylint: disable=no-member elif qos_profile[0] == 'system_default': return QoSPresetProfiles.SYSTEM_DEFAULT.value + elif qos_profile[0] == 'system_default_reliable': + profile = QoSPresetProfiles.SYSTEM_DEFAULT.value + profile.reliability = ReliabilityPolicy.RELIABLE + return profile else: raise ValueError(f"Invalid qos_profile: {qos_profile}") diff --git a/scenario_execution_ros/scenario_execution_ros/actions/ros_bag_play.py b/scenario_execution_ros/scenario_execution_ros/actions/ros_bag_play.py index 80ef4957..cde4771a 100644 --- a/scenario_execution_ros/scenario_execution_ros/actions/ros_bag_play.py +++ b/scenario_execution_ros/scenario_execution_ros/actions/ros_bag_play.py @@ -35,7 +35,7 @@ def setup(self, **kwargs): raise ActionError("input_dir not defined.", action=self) self.input_dir = kwargs['input_dir'] - def execute(self, source: str, topics: list, publish_clock: bool, publish_clock_rate: float): # pylint: disable=arguments-differ,arguments-renamed + def execute(self, source: str, topics: list, publish_clock: bool, publish_clock_rate: float, start_offset: float): # pylint: disable=arguments-differ,arguments-renamed super().execute(wait_for_shutdown=True) self.source = source bag_dir = '' @@ -46,9 +46,11 @@ def execute(self, source: str, topics: list, publish_clock: bool, publish_clock_ if not os.path.exists(bag_dir): raise ActionError(f"Specified rosbag directory '{bag_dir}' does not exist", action=self) - self.command = ["ros2", "bag", "play"] + self.command = ["ros2", "bag", "play", "--disable-keyboard-controls"] if publish_clock: self.command.extend(["--clock", str(publish_clock_rate)]) + if start_offset: + self.command.extend(["--start-offset", str(start_offset)]) if topics: topics_string = " ".join(topics) self.command.append(f"--topics '{topics_string}'") diff --git a/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data.py b/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data.py index e7289485..66bbdb57 100644 --- a/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data.py +++ b/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data.py @@ -86,9 +86,8 @@ def execute(self, self.fail_if_no_data = fail_if_no_data self.fail_if_bad_comparison = fail_if_bad_comparison self.wait_for_first_message = wait_for_first_message - if wait_for_first_message: - self.found = False - else: + self.found = None + if not wait_for_first_message: self.check_data(self.last_msg) if self.found is True: self.feedback_message = f"Found expected value in previously received message." # pylint: disable= attribute-defined-outside-init diff --git a/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data_external.py b/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data_external.py index 7e373b5e..5f84079a 100644 --- a/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data_external.py +++ b/scenario_execution_ros/scenario_execution_ros/actions/ros_topic_check_data_external.py @@ -37,7 +37,7 @@ def __init__(self, file_path: str, function_name: str): super().__init__() - self.check_data = None + self.check_function = None self.topic_name = topic_name self.topic_type = topic_type self.qos_profile = qos_profile @@ -81,7 +81,7 @@ def setup(self, **kwargs): spec.loader.exec_module(module) try: - self.check_data = getattr(module, self.function_name) + self.check_function = getattr(module, self.function_name) except AttributeError as e: raise ActionError(f"Check function '{self.function_name}' not found in file '{self.file_path}'.", action=self) from e @@ -101,10 +101,9 @@ def execute(self, self.fail_if_no_data = fail_if_no_data self.fail_if_bad_comparison = fail_if_bad_comparison self.wait_for_first_message = wait_for_first_message - if wait_for_first_message: - self.found = False - else: - self.found = self.check_data(self.last_msg) + self.found = None + if not wait_for_first_message: + self.check_data(self.last_msg) if self.found is True: self.feedback_message = f"Found expected value in previously received message." # pylint: disable= attribute-defined-outside-init @@ -119,12 +118,18 @@ def update(self) -> py_trees.common.Status: def _callback(self, msg): self.last_msg = msg - self.found = self.check_data(msg) + self.check_data(msg) if self.found is True: self.feedback_message = f"Found expected value in received message." else: self.feedback_message = f"Received message does not contain expected value." + def check_data(self, msg): + if msg is None: + return + + self.found = self.check_function(msg) + def set_expected_value(self, expected_value_string): if not isinstance(expected_value_string, str): raise ActionError("Only string allowed as expected_value.", action=self) diff --git a/scenario_execution_ros/scenario_execution_ros/lib_osc/ros.osc b/scenario_execution_ros/scenario_execution_ros/lib_osc/ros.osc index 9a952218..7f664a80 100644 --- a/scenario_execution_ros/scenario_execution_ros/lib_osc/ros.osc +++ b/scenario_execution_ros/scenario_execution_ros/lib_osc/ros.osc @@ -14,7 +14,8 @@ enum qos_preset_profiles: [ parameter_events, sensor_data, services_default, - system_default + system_default, + system_default_reliable ] enum lifecycle_state: [ @@ -76,6 +77,7 @@ action bag_play: topics: list of string # topics to publish, if empty all topics are published publish_clock: bool = false # wether to publish to /clock publish_clock_rate: float = 1.0 # if publish_clock is true, publish to /clock at the specified frequency in Hz, to act as a ROS Time Source. + start_offset: float = 0.0 # start the playback this many seconds into the bag file action bag_record: # Record a ros bag, stored in output_dir defined by command-line parameter (default: '.') diff --git a/test/scenario_execution_test/test/test_blackboard_write.py b/test/scenario_execution_test/test/test_blackboard_write.py index 42f94c6e..1bec9b88 100644 --- a/test/scenario_execution_test/test/test_blackboard_write.py +++ b/test/scenario_execution_test/test/test_blackboard_write.py @@ -44,6 +44,8 @@ def execute(self, scenario_content): def test_success(self): scenario_content = """ +import osc.helpers + action store_action: file_path: string value: string @@ -55,11 +57,13 @@ def test_success(self): value: string scenario test_scenario: + timeout(3s) foo: test_actor do serial: foo.set_value("two") store_action('""" + self.tmp_file.name + """', foo.test) + wait foo.test == "two" """ self.execute(scenario_content) self.assertTrue(self.scenario_execution.process_results()) diff --git a/test/scenario_execution_test/test/test_expression_with_var.py b/test/scenario_execution_test/test/test_expression_with_var.py index 4e358aba..2799ae25 100644 --- a/test/scenario_execution_test/test/test_expression_with_var.py +++ b/test/scenario_execution_test/test/test_expression_with_var.py @@ -61,7 +61,7 @@ def test_success(self): do parallel: serial: wait elapsed(0.2s) - set_blackboard_var("current/val", 2) + set_blackboard_var("test_scenario/current/val", 2) wait elapsed(10s) serial: wait current.val * 2 + 4 - 4 / 2 == 6 From b42bedfafc75a879dab638c26bf520fd16ef26e1 Mon Sep 17 00:00:00 2001 From: "Pasch, Frederik" Date: Tue, 24 Sep 2024 10:09:13 +0200 Subject: [PATCH 2/3] fix test --- test/scenario_execution_test/test/test_expression_with_var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scenario_execution_test/test/test_expression_with_var.py b/test/scenario_execution_test/test/test_expression_with_var.py index 2799ae25..79d2df45 100644 --- a/test/scenario_execution_test/test/test_expression_with_var.py +++ b/test/scenario_execution_test/test/test_expression_with_var.py @@ -88,7 +88,7 @@ def test_success_not(self): do parallel: serial: wait elapsed(0.2s) - set_blackboard_var("current/val", true) + set_blackboard_var("test_scenario/current/val", true) wait elapsed(0.2s) serial: wait current.val and not current.val2 From b0ecb2a59c3cfd8519ef004ae653e6d04dc0aed0 Mon Sep 17 00:00:00 2001 From: fred-labs Date: Tue, 24 Sep 2024 10:51:11 +0200 Subject: [PATCH 3/3] Update libraries.rst Signed-off-by: fred-labs --- docs/libraries.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/libraries.rst b/docs/libraries.rst index 4f2be592..617f1fbe 100644 --- a/docs/libraries.rst +++ b/docs/libraries.rst @@ -965,6 +965,10 @@ Play back a ROS bag. - ``float`` - ``1.0`` - if ``publish_clock`` is true, publish to ``/clock`` at the specified frequency in Hz, to act as a ROS Time Source. + * - ``start_offset`` + - ``float`` + - ``0.0`` + - start the playback this many seconds into the bag file ``bag_record()``