Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/internals/extensions/rst_filebased_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ One or more Sphinx-Needs directives needed for the
**Example:**

#CHECK: check_options
#EXPECT: std_wp__test__abcd: is missing required option: `status`.
#EXPECT: std_wp__test__abcd: is missing required attribute: `status`.

.. std_wp:: Test requirement
:id: std_wp__test__abcd

This example verifies that the warning message
*std_wp__test__abcd: is missing required option: \`status\`*
*std_wp__test__abcd: is missing required attribute: \`status\`*
is shown during the Sphinx build. Only the *check_options* check is enabled.
28 changes: 15 additions & 13 deletions src/extensions/score_metamodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,22 @@ def _resolve_linkable_types(
link_value: str,
current_need_type: ScoreNeedType,
needs_types: list[ScoreNeedType],
) -> list[ScoreNeedType]:
) -> list[ScoreNeedType | str]:
needs_types_dict = {nt["directive"]: nt for nt in needs_types}
link_values = [v.strip() for v in link_value.split(",")]
linkable_types: list[ScoreNeedType] = []
linkable_types: list[ScoreNeedType | str] = []
for v in link_values:
target_need_type = needs_types_dict.get(v)
if target_need_type is None:
logger.error(
f"In metamodel.yaml: {current_need_type['directive']}, "
f"link '{link_name}' references unknown type '{v}'."
)
if v.startswith("^"):
linkable_types.append(v) # keep regex as-is
else:
linkable_types.append(target_need_type)
target_need_type = needs_types_dict.get(v)
if target_need_type is None:
logger.error(
f"In metamodel.yaml: {current_need_type['directive']}, "
f"link '{link_name}' references unknown type '{v}'."
)
else:
linkable_types.append(target_need_type)
return linkable_types


Expand All @@ -219,10 +222,9 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]):
for link_name, link_value in link_dict.items():
assert isinstance(link_value, str) # so far all of them are strings

if not link_value.startswith("^"):
link_dict[link_name] = _resolve_linkable_types( # pyright: ignore[reportArgumentType]
link_name, link_value, need_type, needs_types_list
)
link_dict[link_name] = _resolve_linkable_types( # pyright: ignore[reportArgumentType]
link_name, link_value, need_type, needs_types_list
)


def setup(app: Sphinx) -> dict[str, str | bool]:
Expand Down
202 changes: 81 additions & 121 deletions src/extensions/score_metamodel/checks/check_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
default_options,
local_check,
)
from score_metamodel.metamodel_types import AllowedLinksType
from sphinx.application import Sphinx
from sphinx_needs.data import NeedsInfoType

Expand All @@ -30,14 +31,21 @@
raise ValueError(f"Need type {directive} not found in needs_types")


def _normalize_values(raw_value: str | list[str] | None) -> list[str]:
def _get_normalized(
need: NeedsInfoType, key: str, remove_prefix: bool = False
) -> list[str]:
"""Normalize a raw value into a list of strings."""
if raw_value is None:
raw_value = need.get(key, None)
if not raw_value:
return []
if isinstance(raw_value, str):
if remove_prefix:
return [_remove_namespace_prefix_(raw_value)]
return [raw_value]
if isinstance(raw_value, list) and all(isinstance(v, str) for v in raw_value):

Check warning on line 45 in src/extensions/score_metamodel/checks/check_options.py

View workflow job for this annotation

GitHub Actions / lint

Type of "v" is unknown (reportUnknownVariableType)
if remove_prefix:
return [_remove_namespace_prefix_(v) for v in raw_value]

Check warning on line 47 in src/extensions/score_metamodel/checks/check_options.py

View workflow job for this annotation

GitHub Actions / lint

Type of "v" is unknown (reportUnknownVariableType)
return raw_value

Check warning on line 48 in src/extensions/score_metamodel/checks/check_options.py

View workflow job for this annotation

GitHub Actions / lint

Return type, "list[Unknown]", is partially unknown (reportUnknownVariableType)
raise ValueError


Expand All @@ -53,118 +61,90 @@
"""
try:
return re.match(pattern, value) is not None
except TypeError as e:
except Exception as e:
raise TypeError(
f"Error in metamodel.yaml at {need['type']}->{field}: "
f"pattern `{pattern}` is not a valid regex pattern."
) from e


def _log_option_warning(
need: NeedsInfoType,
def _remove_namespace_prefix_(word: str) -> str:
# If the word starts with uppercase letters followed by an underscore, remove them.
return re.sub(r"^[A-Z]+_", "", word)


def validate_options(
log: CheckLogger,
field_type: str,
allowed_directives: list[ScoreNeedType] | None,
field: str,
value: str | list[str],
allowed_value: str | list[str],
required: bool,
):
if field_type == "link":
if allowed_directives:
dirs = " or ".join(
f"{d['title']} ({d['directive']})" for d in allowed_directives
)
msg = f"but it must reference {dirs}."
else:
msg = f"which does not follow pattern `{allowed_value}`."

# warning_for_option will print all the values. This way the specific
# problematic value is highlighted in the message.
# This is especially useful if multiple values are given.
msg = f"references '{value}' as '{field}', {msg}"
log.warning_for_need(
need,
msg,
# TODO: Errors in optional links are non fatal for now
is_new_check=not required,
)
else:
msg = f"does not follow pattern `{allowed_value}`."
log.warning_for_option(
need,
field,
msg,
is_new_check=False,
)


def validate_fields(
need_type: ScoreNeedType,
need: NeedsInfoType,
log: CheckLogger,
fields: dict[str, str] | dict[str, list[ScoreNeedType]],
required: bool,
field_type: str,
allowed_prefixes: list[str],
):
"""
Validates that fields (options or links) in a need match their expected patterns.

:param need: The need object containing the data.
:param log: Logger for warnings.
:param fields: A dictionary of field names and their regex patterns.
:param required: Whether the fields are required (True) or optional (False).
:param field_type: A string indicating the field type ('option' or 'link').
Validates that options in a need match their expected patterns.
"""

def remove_prefix(word: str, prefixes: list[str]) -> str:
# Memory and allocation wise better to use a generator here.
# Removes any prefix allowed by configuration, if prefix is there.
return [word.removeprefix(prefix) for prefix in prefixes][0]

for field, allowed_value in fields.items():
raw_value: str | list[str] | None = need.get(field, None)
if raw_value in [None, [], ""]:
if required:
def _validate(attributes_to_allowed_values: dict[str, str], mandatory: bool):
for attribute, allowed_regex in attributes_to_allowed_values.items():
values = _get_normalized(need, attribute)
if mandatory and not values:
log.warning_for_need(
need, f"is missing required {field_type}: `{field}`."
need, f"is missing required attribute: `{attribute}`."
)
continue # Nothing to validate if not present

values = _normalize_values(raw_value)

# Links can be configured to reference other need types instead of regex.
# However, in order to not "load" the other need, we'll check the regex as
# it does encode the need type (at least in S-CORE metamodel).
# Therefore this can remain a @local_check!
# TypedDicts cannot be used with isinstance, so check for dict and required keys
if isinstance(allowed_value, list):
assert field_type == "link" # sanity check
# patterns holds a list of allowed need types
allowed_directives = allowed_value
allowed_value = (
"("
+ "|".join(d["mandatory_options"]["id"] for d in allowed_directives)
+ ")"

for value in values:
if not _validate_value_pattern(value, allowed_regex, need, attribute):
log.warning_for_option(
need, attribute, f"does not follow pattern `{allowed_regex}`."
)

_validate(need_type["mandatory_options"], True)
_validate(need_type["optional_options"], False)


def validate_links(
log: CheckLogger,
need_type: ScoreNeedType,
need: NeedsInfoType,
):
"""
Validates that links in a need match the expected types or regexes.
"""

def _validate(
attributes_to_allowed_values: AllowedLinksType,
mandatory: bool,
treat_as_info: bool = False,
):
for attribute, allowed_values in attributes_to_allowed_values.items():
values = _get_normalized(need, attribute, remove_prefix=True)
if mandatory and not values:
log.warning_for_need(need, f"is missing required link: `{attribute}`.")

allowed_regex = "|".join(
[
v if isinstance(v, str) else v["mandatory_options"]["id"]
for v in allowed_values
]
)
else:
allowed_directives = None

# regex based validation
for value in values:
if allowed_prefixes:
value = remove_prefix(value, allowed_prefixes)
if not _validate_value_pattern(value, allowed_value, need, field):
_log_option_warning(
need,
log,
field_type,
allowed_directives,
field,
value,
allowed_value,
required,
)

# regex based validation
for value in values:
if not _validate_value_pattern(value, allowed_regex, need, attribute):
log.warning_for_link(
need,
attribute,
value,
[
av
if isinstance(av, str)
else f"{av['title']} ({av['directive']})"
for av in allowed_values
],
allowed_regex,
is_new_check=treat_as_info,
)

_validate(need_type["mandatory_links"], True)
_validate(need_type["optional_links"], False, treat_as_info=True)


# req-Id: tool_req__docs_req_attr_reqtype
Expand All @@ -185,28 +165,8 @@
"""
need_type = get_need_type(app.config.needs_types, need["type"])

# If undefined this is an empty list
allowed_prefixes = app.config.allowed_external_prefixes

# Validate Options and Links
field_validations: list[
tuple[str, dict[str, str] | dict[str, list[ScoreNeedType]], bool]
] = [
("option", need_type["mandatory_options"], True),
("option", need_type["optional_options"], False),
("link", need_type["mandatory_links"], True),
("link", need_type["optional_links"], False),
]

for field_type, field_values, is_required in field_validations:
validate_fields(
need,
log,
field_values,
required=is_required,
field_type=field_type,
allowed_prefixes=allowed_prefixes,
)
validate_options(log, need_type, need)
validate_links(log, need_type, need)


@local_check
Expand Down
20 changes: 20 additions & 0 deletions src/extensions/score_metamodel/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ def warning_for_option(
location = CheckLogger._location(need, self._prefix)
self._log_message(full_msg, location, is_new_check)

def warning_for_link(
self,
need: NeedsInfoType,
option: str,
problematic_value: str,
allowed_values: list[str],
allowed_regex: str,
is_new_check: bool = False,
):
msg = (
f"references '{problematic_value}' as '{option}', "
f"but it must reference {' or '.join(allowed_values)}."
)
# Sometimes printing this helps, but most often it just clutters the log.
# Not sure yet.
# if allowed_regex:
# msg += f" (allowed pattern: `{allowed_regex}`)"

self.warning_for_need(need, msg, is_new_check=is_new_check)

def warning_for_need(
self, need: NeedsInfoType, msg: str, is_new_check: bool = False
):
Expand Down
12 changes: 6 additions & 6 deletions src/extensions/score_metamodel/metamodel.yaml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have discussed the usage of satisfies from process requirements to workflows again, see updated image hier https://github.com/eclipse-score/process_description/pull/184/files#diff-7c454b5e2e89ac0ce4216be34bbb05b5c7b5e1b63835a78aba14e72a44b74590
Also the template for process needs is updated accordingly, we can discuss that again on the next process meeting, 07.10, but if agreed, it would have impact on the metamodel again for additional check

Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ needs_types:
# req-Id: tool_req__docs_req_link_satisfies_allowed
# TODO: fix once process_description is fixed
satisfies: workflow
complies: ^std_req__(iso26262|isosae21434|isopas8926|aspice_40)__.*$
complies: std_req
Comment thread
AlexanderLanin marked this conversation as resolved.
tags:
- requirement
parts: 2
Expand All @@ -139,23 +139,23 @@ needs_types:
mandatory_options:
status: ^(valid|draft)$
optional_links:
complies: ^std_req__(iso26262|isodae21434|isopas8926|aspice_40)__.*$
complies: std_req
parts: 2

gd_chklst:
title: Process Checklist
mandatory_options:
status: ^(valid|draft)$
optional_links:
complies: ^std_req__(iso26262|isodae21434|isopas8926|aspice_40)__.*$
complies: std_req
parts: 2

gd_guidl:
title: Process Guideline
mandatory_options:
status: ^(valid|draft)$
optional_links:
complies: ^std_req__(iso26262|isosae21434|isopas8926|aspice_40)__.*$
complies: std_req
parts: 2

gd_method:
Expand All @@ -164,7 +164,7 @@ needs_types:
mandatory_options:
status: ^(valid|draft)$
optional_links:
complies: ^std_req__(iso26262|isosae21434|isopas8926|aspice_40)__.*$
complies: std_req
parts: 2

# S-CORE Workproduct
Expand All @@ -174,7 +174,7 @@ needs_types:
mandatory_options:
status: ^(valid|draft)$
optional_links:
complies: ^std_(wp__iso26262|wp__isosae21434|wp__isopas8926|iic_aspice_40)__.*$
complies: std_wp, ^std_req__aspice_40__iic.*$
Comment thread
AlexanderLanin marked this conversation as resolved.
parts: 2

# Role
Expand Down
Loading
Loading