From 68e39a61ef570db98e7048903241f0fe332988a0 Mon Sep 17 00:00:00 2001 From: romanodanilo <62891297+romanodanilo@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:29:01 +0200 Subject: [PATCH] Add unique name checks and tests (#11) Signed-off-by: romanodanilo Signed-off-by: romanodanilo <62891297+romanodanilo@users.noreply.github.com> --- main.py | 4 + .../checks/reference_checker/__init__.py | 5 + .../reference_checker/reference_checker.py | 67 +++++++ .../reference_checker/reference_constants.py | 1 + .../uniquely_resolvable_entity_references.py | 116 ++++++++++++ .../checks/schema_checker/valid_schema.py | 14 +- qc_openscenario/checks/utils.py | 32 ++++ .../long_catalog_negative.xosc | 34 ++++ .../long_catalog_positive.xosc | 34 ++++ .../vehicle_catalog_negative.xosc | 45 +++++ .../vehicle_catalog_negative_v10.xosc | 45 +++++ .../vehicle_catalog_positive1.xosc | 26 +++ .../vehicle_catalog_positive2.xosc | 26 +++ tests/test_reference_checks.py | 172 ++++++++++++++++++ 14 files changed, 614 insertions(+), 7 deletions(-) create mode 100644 qc_openscenario/checks/reference_checker/__init__.py create mode 100644 qc_openscenario/checks/reference_checker/reference_checker.py create mode 100644 qc_openscenario/checks/reference_checker/reference_constants.py create mode 100644 qc_openscenario/checks/reference_checker/uniquely_resolvable_entity_references.py create mode 100644 tests/data/uniquely_resolvable_entity_references/long_catalog_negative.xosc create mode 100644 tests/data/uniquely_resolvable_entity_references/long_catalog_positive.xosc create mode 100644 tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative.xosc create mode 100644 tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative_v10.xosc create mode 100644 tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive1.xosc create mode 100644 tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive2.xosc create mode 100644 tests/test_reference_checks.py diff --git a/main.py b/main.py index 82d3769..1e90ecd 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ from qc_openscenario import constants from qc_openscenario.checks.schema_checker import schema_checker from qc_openscenario.checks.basic_checker import basic_checker +from qc_openscenario.checks.reference_checker import reference_checker logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.INFO) @@ -51,6 +52,9 @@ def main(): # 2. Run xml checks schema_checker.run_checks(checker_data) + # 3. Run reference checks + reference_checker.run_checks(checker_data) + result.write_to_file( config.get_checker_bundle_param( checker_bundle_name=constants.BUNDLE_NAME, param_name="resultFile" diff --git a/qc_openscenario/checks/reference_checker/__init__.py b/qc_openscenario/checks/reference_checker/__init__.py new file mode 100644 index 0000000..fe0249f --- /dev/null +++ b/qc_openscenario/checks/reference_checker/__init__.py @@ -0,0 +1,5 @@ +from . import reference_constants as reference_constants +from . import reference_checker as reference_checker +from . import ( + uniquely_resolvable_entity_references as uniquely_resolvable_entity_references, +) diff --git a/qc_openscenario/checks/reference_checker/reference_checker.py b/qc_openscenario/checks/reference_checker/reference_checker.py new file mode 100644 index 0000000..b373fb2 --- /dev/null +++ b/qc_openscenario/checks/reference_checker/reference_checker.py @@ -0,0 +1,67 @@ +import logging + +from lxml import etree + +from qc_baselib import Configuration, Result, StatusType + +from qc_openscenario import constants +from qc_openscenario.checks import utils, models + +from qc_openscenario.checks.reference_checker import ( + reference_constants, + uniquely_resolvable_entity_references, +) + + +def run_checks(checker_data: models.CheckerData) -> None: + logging.info("Executing reference checks") + + checker_data.result.register_checker( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + description="Check if xml properties of input file are properly set", + summary="", + ) + + # Skip if basic checks fail + if checker_data.input_file_xml_root is None: + logging.error( + f"Invalid xml input file. Checker {reference_constants.CHECKER_ID} skipped" + ) + checker_data.result.set_checker_status( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + status=StatusType.SKIPPED, + ) + return + + # Skip if schema checks are skipped + if ( + checker_data.result.get_checker_result("xoscBundle", "schema_xosc").status + is StatusType.SKIPPED + ): + logging.error( + f"Schema checks have been skipped. Checker {reference_constants.CHECKER_ID} skipped" + ) + checker_data.result.set_checker_status( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + status=StatusType.SKIPPED, + ) + return + + rule_list = [uniquely_resolvable_entity_references.check_rule] + + for rule in rule_list: + rule(checker_data=checker_data) + + logging.info( + f"Issues found - {checker_data.result.get_checker_issue_count(checker_bundle_name=constants.BUNDLE_NAME, checker_id=reference_constants.CHECKER_ID)}" + ) + + # TODO: Add logic to deal with error or to skip it + checker_data.result.set_checker_status( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + status=StatusType.COMPLETED, + ) diff --git a/qc_openscenario/checks/reference_checker/reference_constants.py b/qc_openscenario/checks/reference_checker/reference_constants.py new file mode 100644 index 0000000..ed0cc9b --- /dev/null +++ b/qc_openscenario/checks/reference_checker/reference_constants.py @@ -0,0 +1 @@ +CHECKER_ID = "reference_xosc" diff --git a/qc_openscenario/checks/reference_checker/uniquely_resolvable_entity_references.py b/qc_openscenario/checks/reference_checker/uniquely_resolvable_entity_references.py new file mode 100644 index 0000000..4e22a44 --- /dev/null +++ b/qc_openscenario/checks/reference_checker/uniquely_resolvable_entity_references.py @@ -0,0 +1,116 @@ +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" + + +def get_catalogs(root: etree._ElementTree) -> List[etree._ElementTree]: + catalogs = [] + + for catalog in root.iter("Catalog"): + catalogs.append(catalog) + + return catalogs + + +def get_xpath(root: etree._ElementTree, element: etree._ElementTree) -> str: + return root.getpath(element) + + +def check_rule(checker_data: models.CheckerData) -> None: + """ + Implements a rule to check if referenced entities are unique + + More info at + - https://github.com/asam-ev/qc-openscenarioxml/issues/8 + """ + logging.info("Executing uniquely_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 + + rule_severity = IssueSeverity.WARNING + 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.uniquely_resolvable_entity_references", + ) + + root = checker_data.input_file_xml_root + + # List to store problematic nodes + errors = [] + + # Iterate over each 'Catalog' node + for catalog_node in root.findall(".//Catalog"): + # Dictionary to track child nodes by 'name' attribute + child_names = {} + + # Iterate catalog children within current 'Catalog' node + # Currently the checks verifies that the same name is not used in the same + # catalog regardless the tag type + # E.g. Catalog children: + # # [Vehicle name="abc", Maneuver name="abc"] + # Will trigger the issue + for child_node in catalog_node.iterchildren(): + name_attr = child_node.attrib.get("name") + if name_attr: + if name_attr in child_names: + errors.append( + { + "name": name_attr, + "tag": child_node.tag, + "first_xpath": get_xpath(root, child_names[name_attr]), + "duplicate_xpath": get_xpath(root, child_node), + } + ) + else: + child_names[name_attr] = child_node + + if len(errors) > 0: + issue_id = checker_data.result.register_issue( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + description="Issue flagging when referenced names are not unique", + level=rule_severity, + rule_uid=rule_uid, + ) + + for error in errors: + error_name = error["name"] + error_first_xpath = error["first_xpath"] + error_duplicate_xpath = error["duplicate_xpath"] + error_msg = f"Duplicate name {error_name}. First occurrence at {error_first_xpath} duplicate at {error_duplicate_xpath}" + logging.error(f"- Error: {error_msg}") + logging.error(f"- Duplicate xpath: {error_duplicate_xpath}") + checker_data.result.add_xml_location( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=reference_constants.CHECKER_ID, + issue_id=issue_id, + xpath=error_duplicate_xpath, + description=error_msg, + ) diff --git a/qc_openscenario/checks/schema_checker/valid_schema.py b/qc_openscenario/checks/schema_checker/valid_schema.py index 7c7d6dc..36c4dba 100644 --- a/qc_openscenario/checks/schema_checker/valid_schema.py +++ b/qc_openscenario/checks/schema_checker/valid_schema.py @@ -75,15 +75,15 @@ def check_rule(checker_data: models.CheckerData) -> None: ) if not schema_compliant: - issue_id = checker_data.result.register_issue( - checker_bundle_name=constants.BUNDLE_NAME, - checker_id=schema_constants.CHECKER_ID, - description="Issue flagging when input file does not follow its version schema", - level=IssueSeverity.ERROR, - rule_uid=rule_uid, - ) for error in errors: + issue_id = checker_data.result.register_issue( + checker_bundle_name=constants.BUNDLE_NAME, + checker_id=schema_constants.CHECKER_ID, + description="Issue flagging when input file does not follow its version schema", + level=IssueSeverity.ERROR, + rule_uid=rule_uid, + ) checker_data.result.add_file_location( checker_bundle_name=constants.BUNDLE_NAME, checker_id=schema_constants.CHECKER_ID, diff --git a/qc_openscenario/checks/utils.py b/qc_openscenario/checks/utils.py index afc4a77..7ce8ddf 100644 --- a/qc_openscenario/checks/utils.py +++ b/qc_openscenario/checks/utils.py @@ -9,3 +9,35 @@ def get_standard_schema_version(root: etree._ElementTree) -> Union[str, None]: header_attrib = header.attrib version = f"{header_attrib['revMajor']}.{header_attrib['revMinor']}.0" return version + + +def compare_versions(version1: str, version2: str) -> int: + """Compare two version strings like "X.x.x" + This function is to avoid comparing version string basing on lexicographical order + that could cause problem. E.g. + 1.10.0 > 1.2.0 but lexicographical comparison of string would return the opposite + + Args: + version1 (str): First string to compare + version2 (str): Second string to compare + + Returns: + int: 1 if version1 is bigger than version2. 0 if the version are the same. -1 otherwise + """ + v1_components = list(map(int, version1.split("."))) + v2_components = list(map(int, version2.split("."))) + + # Compare each component until one is greater or they are equal + for v1, v2 in zip(v1_components, v2_components): + if v1 < v2: + return -1 + elif v1 > v2: + return 1 + + # If all components are equal, compare based on length + if len(v1_components) < len(v2_components): + return -1 + elif len(v1_components) > len(v2_components): + return 1 + else: + return 0 diff --git a/tests/data/uniquely_resolvable_entity_references/long_catalog_negative.xosc b/tests/data/uniquely_resolvable_entity_references/long_catalog_negative.xosc new file mode 100644 index 0000000..30ef6de --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/long_catalog_negative.xosc @@ -0,0 +1,34 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/uniquely_resolvable_entity_references/long_catalog_positive.xosc b/tests/data/uniquely_resolvable_entity_references/long_catalog_positive.xosc new file mode 100644 index 0000000..f29cf82 --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/long_catalog_positive.xosc @@ -0,0 +1,34 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative.xosc b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative.xosc new file mode 100644 index 0000000..6a7a413 --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative.xosc @@ -0,0 +1,45 @@ + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative_v10.xosc b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative_v10.xosc new file mode 100644 index 0000000..5a153cc --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_negative_v10.xosc @@ -0,0 +1,45 @@ + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive1.xosc b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive1.xosc new file mode 100644 index 0000000..9d73e92 --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive1.xosc @@ -0,0 +1,26 @@ + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive2.xosc b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive2.xosc new file mode 100644 index 0000000..49d9285 --- /dev/null +++ b/tests/data/uniquely_resolvable_entity_references/vehicle_catalog_positive2.xosc @@ -0,0 +1,26 @@ + + + + + + +
+ + + + + + + + + + + + + + + diff --git a/tests/test_reference_checks.py b/tests/test_reference_checks.py new file mode 100644 index 0000000..dd8ae62 --- /dev/null +++ b/tests/test_reference_checks.py @@ -0,0 +1,172 @@ +import os +import pytest +import test_utils +from qc_openscenario import constants +from qc_openscenario.checks.reference_checker import reference_constants +from qc_baselib import Result, IssueSeverity + + +def test_uniquely_resolvable_positive1( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"vehicle_catalog_positive1.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.uniquely_resolvable_entity_references" + ) + ) + == 0 + ) + + test_utils.cleanup_files() + + +def test_uniquely_resolvable_positive2( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"vehicle_catalog_positive2.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.uniquely_resolvable_entity_references" + ) + ) + == 0 + ) + + test_utils.cleanup_files() + + +def test_uniquely_resolvable_negative( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"vehicle_catalog_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) + + reference_issues = result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.uniquely_resolvable_entity_references" + ) + assert len(reference_issues) == 1 + assert reference_issues[0].level == IssueSeverity.WARNING + test_utils.cleanup_files() + + +def test_long_catalog_negative( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"long_catalog_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.uniquely_resolvable_entity_references" + ) + assert len(reference_issues) == 1 + assert reference_issues[0].level == IssueSeverity.WARNING + test_utils.cleanup_files() + + +def test_long_catalog_positive( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"long_catalog_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.uniquely_resolvable_entity_references" + ) + ) + == 0 + ) + + +def test_minimum_version( + monkeypatch, +) -> None: + base_path = "tests/data/uniquely_resolvable_entity_references/" + target_file_name = f"vehicle_catalog_negative_v10.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, + ) + # 0 issues because minumum version is not met and the check is not performed + # (even if it is a negative sample) + assert ( + len( + result.get_issues_by_rule_uid( + "asam.net:xosc:1.2.0:reference_control.uniquely_resolvable_entity_references" + ) + ) + == 0 + )