diff --git a/qc_openscenario/checks/reference_checker/__init__.py b/qc_openscenario/checks/reference_checker/__init__.py index 52dc939..bb05131 100644 --- a/qc_openscenario/checks/reference_checker/__init__.py +++ b/qc_openscenario/checks/reference_checker/__init__.py @@ -12,3 +12,5 @@ from . import ( valid_actor_reference_in_private_actions as valid_actor_reference_in_private_actions, ) +from . import resolvable_entity_references as resolvable_entity_references +from . import resolvable_variable_reference as resolvable_variable_reference diff --git a/qc_openscenario/checks/reference_checker/reference_checker.py b/qc_openscenario/checks/reference_checker/reference_checker.py index 22d8b23..ecbd12b 100644 --- a/qc_openscenario/checks/reference_checker/reference_checker.py +++ b/qc_openscenario/checks/reference_checker/reference_checker.py @@ -13,6 +13,8 @@ resolvable_signal_id_in_traffic_signal_state_action, resolvable_traffic_signal_controller_by_traffic_signal_controller_ref, valid_actor_reference_in_private_actions, + resolvable_entity_references, + resolvable_variable_reference, ) @@ -58,6 +60,8 @@ def run_checks(checker_data: models.CheckerData) -> None: resolvable_signal_id_in_traffic_signal_state_action.check_rule, resolvable_traffic_signal_controller_by_traffic_signal_controller_ref.check_rule, valid_actor_reference_in_private_actions.check_rule, + resolvable_entity_references.check_rule, + resolvable_variable_reference.check_rule, ] for rule in rule_list: diff --git a/qc_openscenario/checks/reference_checker/resolvable_entity_references.py b/qc_openscenario/checks/reference_checker/resolvable_entity_references.py new file mode 100644 index 0000000..7cb68c6 --- /dev/null +++ b/qc_openscenario/checks/reference_checker/resolvable_entity_references.py @@ -0,0 +1,98 @@ +import os, logging + +from dataclasses import dataclass +from typing import List + +from lxml import etree + +from qc_baselib import Configuration, Result, IssueSeverity + +from qc_openscenario import constants +from qc_openscenario.schema import schema_files +from qc_openscenario.checks import utils, models + +from qc_openscenario.checks.reference_checker import reference_constants +from collections import deque, defaultdict + +MIN_RULE_VERSION = "1.2.0" +RULE_SEVERITY = IssueSeverity.ERROR + + +def check_rule(checker_data: models.CheckerData) -> None: + """ + Rule ID: asam.net:xosc:1.2.0:reference_control.resolvable_entity_references + + Description: A named reference in the EntityRef must be resolvable. Checking all EntityRef's in the document. + Severity: ERROR + + Version range: [1.2.0, ) + + Remark: + None + + More info at + - https://github.com/asam-ev/qc-openscenarioxml/issues/15 + """ + logging.info("Executing resolvable_entity_references check") + + schema_version = checker_data.schema_version + if schema_version is None: + logging.info(f"- Version not found in the file. Skipping check") + return + + if utils.compare_versions(schema_version, MIN_RULE_VERSION) < 0: + logging.info( + f"- Version {schema_version} is less than minimum required version {MIN_RULE_VERSION}. Skipping check" + ) + return + + rule_uid = checker_data.result.register_rule( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + emanating_entity="asam.net", + standard="xosc", + definition_setting=MIN_RULE_VERSION, + rule_full_name="reference_control.resolvable_entity_references", + ) + + root = checker_data.input_file_xml_root + + entities_node = root.find("Entities") + if entities_node is None: + logging.error("Cannot find Entities node in provided XOSC file. Skipping check") + return + + defined_entities = set() + for entity_node in list(entities_node): + current_name = entity_node.get("name") + if current_name is not None: + defined_entities.add(current_name) + + storyboard_node = root.find("Storyboard") + if storyboard_node is None: + logging.error( + "Cannot find Storyboard node in provided XOSC file. Skipping check" + ) + return + + nodes_with_entity_ref = storyboard_node.xpath(".//*[@entityRef]") + + for node_with_entity_ref in nodes_with_entity_ref: + current_name = node_with_entity_ref.get("entityRef") + if current_name is not None and current_name not in defined_entities: + xpath = root.getpath(node_with_entity_ref) + + issue_id = checker_data.result.register_issue( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + description="Issue flagging when an entity is referred in a entityRef attribute but it is not declared among Entities", + level=RULE_SEVERITY, + rule_uid=rule_uid, + ) + checker_data.result.add_xml_location( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + issue_id=issue_id, + xpath=xpath, + description=f"Entity at {xpath} with id {current_name} not found among defined Entities ", + ) diff --git a/qc_openscenario/checks/reference_checker/resolvable_variable_reference.py b/qc_openscenario/checks/reference_checker/resolvable_variable_reference.py new file mode 100644 index 0000000..102ced4 --- /dev/null +++ b/qc_openscenario/checks/reference_checker/resolvable_variable_reference.py @@ -0,0 +1,104 @@ +import os, logging + +from dataclasses import dataclass +from typing import List + +from lxml import etree + +from qc_baselib import Configuration, Result, IssueSeverity + +from qc_openscenario import constants +from qc_openscenario.schema import schema_files +from qc_openscenario.checks import utils, models + +from qc_openscenario.checks.reference_checker import reference_constants +from collections import deque, defaultdict + +MIN_RULE_VERSION = "1.2.0" +RULE_SEVERITY = IssueSeverity.ERROR + + +def check_rule(checker_data: models.CheckerData) -> None: + """ + Rule ID: asam.net:xosc:1.2.0:reference_control.resolvable_variable_reference + + Description: The VariableDeclaration according to the "variableRef" property must exist within the ScenarioDefinition. + Severity: ERROR + + Version range: [1.2.0, ) + + Remark: + None + + More info at + - https://github.com/asam-ev/qc-openscenarioxml/issues/18 + """ + logging.info("Executing resolvable_variable_reference check") + + schema_version = checker_data.schema_version + if schema_version is None: + logging.info(f"- Version not found in the file. Skipping check") + return + + if utils.compare_versions(schema_version, MIN_RULE_VERSION) < 0: + logging.info( + f"- Version {schema_version} is less than minimum required version {MIN_RULE_VERSION}. Skipping check" + ) + return + + rule_uid = checker_data.result.register_rule( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + emanating_entity="asam.net", + standard="xosc", + definition_setting=MIN_RULE_VERSION, + rule_full_name="reference_control.resolvable_variable_reference", + ) + + root = checker_data.input_file_xml_root + + parameter_declaration_nodes = root.find("ParameterDeclarations") + variable_declaration_nodes = root.find("VariableDeclarations") + + # Get parameters and variables declarations + defined_param_variables = set() + if parameter_declaration_nodes is not None: + for declaration_node in list(parameter_declaration_nodes): + current_name = declaration_node.get("name") + if current_name is not None: + defined_param_variables.add(current_name) + + if variable_declaration_nodes is not None: + for declaration_node in list(variable_declaration_nodes): + current_name = declaration_node.get("name") + if current_name is not None: + defined_param_variables.add(current_name) + + storyboard_node = root.find("Storyboard") + if storyboard_node is None: + logging.error( + "Cannot find Storyboard node in provided XOSC file. Skipping check" + ) + return + + nodes_with_variable_ref = storyboard_node.xpath(".//*[@variableRef]") + + for node_with_variable_ref in nodes_with_variable_ref: + current_name = node_with_variable_ref.get("variableRef") + if current_name is not None and current_name not in defined_param_variables: + xpath = root.getpath(node_with_variable_ref) + + issue_id = checker_data.result.register_issue( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + description="Issue flagging when a variable is referred in a variableRef attribute but it is not found within ScenarioDefinition", + level=RULE_SEVERITY, + rule_uid=rule_uid, + ) + checker_data.result.add_xml_location( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + issue_id=issue_id, + xpath=xpath, + description=f"Variable with id {current_name} not found within ScenarioDefinition", + ) diff --git a/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.multiple.xosc b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.multiple.xosc new file mode 100644 index 0000000..69ebfe6 --- /dev/null +++ b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.multiple.xosc @@ -0,0 +1,85 @@ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.xosc b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.xosc new file mode 100644 index 0000000..ae1d99b --- /dev/null +++ b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.negative.xosc @@ -0,0 +1,42 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.positive.xosc b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.positive.xosc new file mode 100644 index 0000000..dafd821 --- /dev/null +++ b/tests/data/resolvable_entity_references/reference_control.resolvable_entity_references.positive.xosc @@ -0,0 +1,41 @@ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.multiple.xosc b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.multiple.xosc new file mode 100644 index 0000000..13a1ff5 --- /dev/null +++ b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.multiple.xosc @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.xosc b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.xosc new file mode 100644 index 0000000..3605f4f --- /dev/null +++ b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.negative.xosc @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.positive.xosc b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.positive.xosc new file mode 100644 index 0000000..bc434cf --- /dev/null +++ b/tests/data/resolvable_variable_reference/reference_control.resolvable_variable_reference.positive.xosc @@ -0,0 +1,48 @@ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_reference_checks.py b/tests/test_reference_checks.py index b44121e..ea07003 100644 --- a/tests/test_reference_checks.py +++ b/tests/test_reference_checks.py @@ -397,3 +397,179 @@ def test_valid_actor_reference_in_private_actions_negative( assert len(reference_issues) == 1 assert reference_issues[0].level == IssueSeverity.ERROR test_utils.cleanup_files() + + +def test_resolvable_entity_reference_positive( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_entity_references/" + target_file_name = f"reference_control.resolvable_entity_references.positive.xosc" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + assert ( + len( + result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_entity_references" + ) + ) + == 0 + ) + + +def test_resolvable_entity_reference_negative( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_entity_references/" + target_file_name = f"reference_control.resolvable_entity_references.negative.xosc" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + + reference_issues = result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_entity_references" + ) + assert len(reference_issues) == 1 + assert reference_issues[0].level == IssueSeverity.ERROR + test_utils.cleanup_files() + + +def test_resolvable_entity_reference_negative_multiple( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_entity_references/" + target_file_name = ( + f"reference_control.resolvable_entity_references.negative.multiple.xosc" + ) + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + + reference_issues = result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_entity_references" + ) + assert len(reference_issues) == 2 + assert reference_issues[0].level == IssueSeverity.ERROR + assert reference_issues[1].level == IssueSeverity.ERROR + assert "Vehicle 3" in reference_issues[0].locations[0].description + assert "Vehicle 4" in reference_issues[1].locations[0].description + test_utils.cleanup_files() + + +def test_resolvable_variable_reference_positive( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_variable_reference/" + target_file_name = f"reference_control.resolvable_variable_reference.positive.xosc" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + assert ( + len( + result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_variable_reference" + ) + ) + == 0 + ) + + +def test_resolvable_variable_reference_negative( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_variable_reference/" + target_file_name = f"reference_control.resolvable_variable_reference.negative.xosc" + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + + reference_issues = result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_variable_reference" + ) + assert len(reference_issues) == 1 + assert reference_issues[0].level == IssueSeverity.ERROR + test_utils.cleanup_files() + + +def test_resolvable_variable_reference_multiple( + monkeypatch, +) -> None: + base_path = "tests/data/resolvable_variable_reference/" + target_file_name = ( + f"reference_control.resolvable_variable_reference.negative.multiple.xosc" + ) + target_file_path = os.path.join(base_path, target_file_name) + + test_utils.create_test_config(target_file_path) + + test_utils.launch_main(monkeypatch) + + result = Result() + result.load_from_file(test_utils.REPORT_FILE_PATH) + + _ = result.get_checker_result( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + ) + + reference_issues = result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.resolvable_variable_reference" + ) + assert len(reference_issues) == 2 + assert reference_issues[0].level == IssueSeverity.ERROR + assert reference_issues[1].level == IssueSeverity.ERROR + assert ( + "unknownReferenceForYPosition" in reference_issues[0].locations[0].description + ) + assert "foo" in reference_issues[1].locations[0].description + test_utils.cleanup_files()