From 7ea653daddef75af1a691aab9d7977a2453d3992 Mon Sep 17 00:00:00 2001 From: "Kostadin Ivanov (BD/TBC-BG)" Date: Mon, 28 Oct 2024 17:59:27 +0200 Subject: [PATCH] Add support for handling of RegularExpression definitions for string based properties Signed-off-by: Kostadin Ivanov (BD/TBC-BG) --- .../exporters/samm/helpers/samm_concepts.py | 2 + .../samm/helpers/ttl_builder_helper.py | 19 ++++- .../exporters/samm/helpers/vss_helper.py | 14 +--- src/vss_tools/model.py | 40 +++++++++ .../test_datatypes_pattern.py | 84 +++++++++++++++++++ .../test_pattern_no_allowed_array_match.vspec | 13 +++ .../test_pattern_no_allowed_match.vspec | 12 +++ .../test_pattern_no_default_array_match.vspec | 12 +++ .../test_pattern_no_default_match.vspec | 11 +++ .../test_pattern_ok.vspec | 11 +++ .../test_pattern_wrong_datatype.vspec | 11 +++ 11 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 tests/vspec/test_datatypes_pattern/test_datatypes_pattern.py create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_array_match.vspec create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_match.vspec create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_no_default_array_match.vspec create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_no_default_match.vspec create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_ok.vspec create mode 100644 tests/vspec/test_datatypes_pattern/test_pattern_wrong_datatype.vspec diff --git a/src/vss_tools/exporters/samm/helpers/samm_concepts.py b/src/vss_tools/exporters/samm/helpers/samm_concepts.py index 9a8a13d6..85b4b277 100644 --- a/src/vss_tools/exporters/samm/helpers/samm_concepts.py +++ b/src/vss_tools/exporters/samm/helpers/samm_concepts.py @@ -53,6 +53,7 @@ class SammConcepts(Enum): DESCRIPTION = "description" ENTITY = "Entity" EVENTS = "events" + VALUE = "value" EXAMPLE_VALUE = "exampleValue" NAME = "name" OPERATIONS = "operations" @@ -92,6 +93,7 @@ class SammCConcepts(Enum): MIN_VALUE = "minValue" QUANTIFIABLE = "Quantifiable" RANGE_CONSTRAINT = "RangeConstraint" + REG_EXP_CONSTRAINT = "RegularExpressionConstraint" SINGLE_ENTITY = "SingleEntity" STATE = "State" TIMESTAMP = "Timestamp" diff --git a/src/vss_tools/exporters/samm/helpers/ttl_builder_helper.py b/src/vss_tools/exporters/samm/helpers/ttl_builder_helper.py index c4f22614..806ef245 100644 --- a/src/vss_tools/exporters/samm/helpers/ttl_builder_helper.py +++ b/src/vss_tools/exporters/samm/helpers/ttl_builder_helper.py @@ -330,7 +330,14 @@ def add_node_leaf_constraint(graph: Graph, node_char_name: str, node_char_uri: U constraint_name = str_to_uc_first_camel_case(vss_node.ttl_name + "Constraint") constraint_node_uri = get_vspec_uri(constraint_name) - __add_node_tuple(graph, constraint_node_uri, RDF.type, SammCConcepts.RANGE_CONSTRAINT.uri) + # Default Constraint URI is for Range (min/max) constraints + constraint_uri = SammCConcepts.RANGE_CONSTRAINT.uri + + if hasattr(vss_node.data, "pattern") and vss_node.data.pattern is not None: + # Pattern property is used for Regular Expression constraints of STRING based data nodes + constraint_uri = SammCConcepts.REG_EXP_CONSTRAINT.uri + + __add_node_tuple(graph, constraint_node_uri, RDF.type, constraint_uri) __add_node_tuple(graph, constraint_node_uri, SammConcepts.NAME.uri, Literal(constraint_name)) # Workaround since doubles are serialized as scientific numbers @@ -354,6 +361,16 @@ def add_node_leaf_constraint(graph: Graph, node_char_name: str, node_char_uri: U Literal(vss_node.data.min, datatype=data_type), # type: ignore ) + if vss_node.data.pattern is not None: # type: ignore + __add_node_tuple( + graph, + constraint_node_uri, + SammConcepts.VALUE.uri, + Literal(vss_node.data.pattern, datatype=data_type), # type: ignore + ) + + # Set the RegExp value for constraint_node_uri + base_c_name = str_to_uc_first_camel_case(vss_node.ttl_name + "BaseCharacteristic") base_c_uri = get_vspec_uri(base_c_name) diff --git a/src/vss_tools/exporters/samm/helpers/vss_helper.py b/src/vss_tools/exporters/samm/helpers/vss_helper.py index 711136e2..2b56bce5 100644 --- a/src/vss_tools/exporters/samm/helpers/vss_helper.py +++ b/src/vss_tools/exporters/samm/helpers/vss_helper.py @@ -13,7 +13,7 @@ from rdflib import URIRef from vss_tools import log from vss_tools.datatypes import Datatypes -from vss_tools.model import NodeType, VSSDataBranch +from vss_tools.model import NodeType, VSSDataBranch, VSSDataDatatype from vss_tools.tree import VSSNode from ..config import config as cfg @@ -260,15 +260,9 @@ def get_node_description(vss_node: VSSNode) -> str: def has_constraints(vss_node: VSSNode) -> bool: - return ( - hasattr(vss_node.data, "type") - and vss_node.data.type in [NodeType.ACTUATOR, NodeType.SENSOR] - and ( - hasattr(vss_node.data, "max") - and vss_node.data.max is not None - or hasattr(vss_node.data, "min") - and vss_node.data.min is not None - ) + return bool( + isinstance(vss_node.data, VSSDataDatatype) + and (vss_node.data.max is not None or vss_node.data.min is not None or vss_node.data.pattern is not None) ) diff --git a/src/vss_tools/model.py b/src/vss_tools/model.py index b25bd886..0b604676 100644 --- a/src/vss_tools/model.py +++ b/src/vss_tools/model.py @@ -178,6 +178,10 @@ class VSSDataDatatype(VSSData): arraysize: int | None = None min: int | float | None = None max: int | float | None = None + # Field, used to allow definition of Regular Expression constraints + # for string based property nodes. + # Example: VSS - VehicleIdentification.VIN property + pattern: str | None = None unit: str | None = None allowed: list[str | int | float | bool] | None = None default: list[str | int | float | bool] | str | int | float | bool | None = None @@ -277,6 +281,42 @@ def check_datatype_matching_allowed_unit_datatypes(self) -> Self: ), f"'{self.datatype}' is not allowed for unit '{self.unit}'" return self + @model_validator(mode="after") + def check_datatype_pattern(self) -> Self: + """ + Checks that regular expression datatype 'pattern' field is: + + 1. defined only for string typed nodes i.e., STRING and STRING_ARRAY + 2. if default value(s) is provided, each default matching the specified pattern + 3. if allowed value(s) is provided, each allowed matching the specified pattern + """ + if self.pattern: + # Datatypes.TUPLE[0] is the string name of the type. + allowed_for = f"Allowed types: {[Datatypes.STRING[0], Datatypes.STRING_ARRAY[0]]}" + assert Datatypes.get_type(self.datatype) in [ + Datatypes.STRING, + Datatypes.STRING_ARRAY, + ], f"Field 'pattern' is not allowed for type: '{self.datatype}'. {allowed_for}" + + def check_value_match(value_to_check: Any, value_type: str, reg_exp: str) -> None: + check_values = [value_to_check] + + if type(value_to_check) is list: + check_values = value_to_check + + for def_val in check_values: + assert re.match( + reg_exp, def_val + ), f"Specified '{value_type}' value: '{def_val}' must match defined pattern: '{self.pattern}'" + + if self.default: + check_value_match(self.default, "default", self.pattern) + + if self.allowed: + check_value_match(self.allowed, "allowed", self.pattern) + + return self + class VSSDataProperty(VSSDataDatatype): pass diff --git a/tests/vspec/test_datatypes_pattern/test_datatypes_pattern.py b/tests/vspec/test_datatypes_pattern/test_datatypes_pattern.py new file mode 100644 index 00000000..f0354e16 --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_datatypes_pattern.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +import subprocess +from pathlib import Path + +HERE = Path(__file__).resolve().parent +TEST_UNITS = HERE / ".." / "test_units.yaml" +TEST_QUANT = HERE / ".." / "test_quantities.yaml" + + +# TODO-Kosta: +# Test that: +# +# FAILS when other than string and string[] datatypes have defined pattern +# +# FAILS when pattern is defined and default is defined and default value does not match pattern +# - there should be test for default and allowed values, +# where allowed is mainly for string[] datatypes +# +# Add above three fail tests and 1 success test + + +def test_datatype_pattern_wrong_type(tmp_path): + spec = HERE / "test_pattern_wrong_datatype.vspec" + output = tmp_path / "out.json" + log = tmp_path / "log.txt" + cmd = ( + f"vspec --log-file {log} export json --pretty -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}" + ) + process = subprocess.run(cmd.split()) + assert process.returncode != 0 + + print(process.stdout) + assert "Field 'pattern' is not allowed for type: 'uint16'. Allowed types: ['string', 'string[]']" in log.read_text() + + +def test_datatype_pattern_no_match(tmp_path): + def get_log_msg(value_type: str, value: str): + return f"Specified '{value_type}' value: '{value}' must match defined pattern: '^[a-z]+$'" + + no_pattern_match_tests_to_run = { + # Test #1: test no match of defined DEFAULT value + "test_pattern_no_default_match.vspec": get_log_msg("default", "WrongLabel1"), + # Test #2: test no match of defined DEFAULT list of values + "test_pattern_no_default_array_match.vspec": get_log_msg("default", "Red"), + # Test #3: test no match of defined ALLOWED value + "test_pattern_no_allowed_match.vspec": get_log_msg("allowed", "Red"), + # Test #4: test no match of defined ALLOWED list of values + "test_pattern_no_allowed_array_match.vspec": get_log_msg("allowed", "Red"), + } + + def run_no_match_pattern_test(test_name: str, log_message: str): + spec = HERE / test_name + output = tmp_path / "out.json" + log = tmp_path / "log.txt" + cmd = f"vspec --log-file {log} export json --pretty -u {TEST_UNITS} -q {TEST_QUANT}" + cmd += f" --vspec {spec} --output {output} --strict" + process = subprocess.run(cmd.split()) + + # Tested command should fail + assert process.returncode != 0 + # Check logged error message + assert log_message in log.read_text() + + # Run no_pattern_match_tests_to_run + for tst_name, tst_msg in no_pattern_match_tests_to_run.items(): + run_no_match_pattern_test(tst_name, tst_msg) + + +def test_datatype_pattern_ok(tmp_path): + # Test #1: test no match of defined DEFAULT value + spec = HERE / "test_pattern_ok.vspec" + output = tmp_path / "out.json" + log = tmp_path / "log.txt" + cmd = f"vspec --log-file {log} export json --pretty -u {TEST_UNITS} -q {TEST_QUANT}" + cmd += f" --vspec {spec} --output {output} --strict" + process = subprocess.run(cmd.split()) + assert process.returncode == 0 diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_array_match.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_array_match.vspec new file mode 100644 index 00000000..3984eb01 --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_array_match.vspec @@ -0,0 +1,13 @@ +# +A: + type: branch + description: Branch A - used to test no match of 'allowed' values with specified 'pattern' field. + +A.Colors: + datatype: string[] + type: attribute + description: Simple Label for colors, which should be a simple collection of all lower case strings. + Should fail on the 'allowed' 'Red' color label. + pattern: ^[a-z]+$ + allowed: [white, green, Red] + default: white diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_match.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_match.vspec new file mode 100644 index 00000000..d22ad987 --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_no_allowed_match.vspec @@ -0,0 +1,12 @@ +# +A: + type: branch + description: Branch A - used to test no match of 'allowed' value with specified 'pattern' field. + +A.ErrorColor: + datatype: string + type: attribute + description: Simple Label for ErrorColor, which should be a simple all lower case string. + Should fail on the 'allowed' (only) 'Red' color label. + pattern: ^[a-z]+$ + allowed: Red diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_no_default_array_match.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_no_default_array_match.vspec new file mode 100644 index 00000000..5715151d --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_no_default_array_match.vspec @@ -0,0 +1,12 @@ +# +A: + type: branch + description: Branch A - used to test no match of 'default' values with specified 'pattern' field. + +A.Colors: + datatype: string[] + type: attribute + description: Simple Label for colors, which should be a simple collection of all lower case strings. + Should fail on the 'Red' color label. + pattern: ^[a-z]+$ + default: [white, green, Red] diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_no_default_match.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_no_default_match.vspec new file mode 100644 index 00000000..638e23e6 --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_no_default_match.vspec @@ -0,0 +1,11 @@ +# +A: + type: branch + description: Branch A - used to test no match of 'default' value with specified 'pattern' field. + +A.Label: + datatype: string + type: attribute + description: Simple Label, which should be a simple all lower case string. + pattern: ^[a-z]+$ + default: WrongLabel1 diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_ok.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_ok.vspec new file mode 100644 index 00000000..d590019c --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_ok.vspec @@ -0,0 +1,11 @@ +# +A: + type: branch + description: Branch A - used to test correct attribute (property) with defined 'pattern' field. + +A.Label: + datatype: string + type: attribute + description: Simple Label, which should be a simple all lower case string with a number. + pattern: ^[a-z0-9]+$ + default: label1 diff --git a/tests/vspec/test_datatypes_pattern/test_pattern_wrong_datatype.vspec b/tests/vspec/test_datatypes_pattern/test_pattern_wrong_datatype.vspec new file mode 100644 index 00000000..2ae3f10b --- /dev/null +++ b/tests/vspec/test_datatypes_pattern/test_pattern_wrong_datatype.vspec @@ -0,0 +1,11 @@ +# +A: + type: branch + description: Branch A - used to test wrong usage of 'pattern' property for other than a string datatype. + +A.Year: + datatype: uint16 + type: attribute + description: Unsigned Integer (number) for an Year attribute. + Should fail for defined 'pattern' field because 'pattern' is allowed only for string types. + pattern: ^[0-9]+$