Skip to content

Commit 0c58ed6

Browse files
authored
Merge pull request crs4#141 from kikkomep/fix/reporting-inherited-profiles-issues
fix: resolve inherited profiles reporting issues and restore check skipping
2 parents e01ee92 + a5d9d0a commit 0c58ed6

6 files changed

Lines changed: 148 additions & 24 deletions

File tree

rocrate_validator/cli/commands/validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def validate(ctx,
277277
"profile_identifier": profile_identifier,
278278
"requirement_severity": requirement_severity,
279279
"requirement_severity_only": requirement_severity_only,
280-
"enable_profile_inheritance": not disable_profile_inheritance,
280+
"disable_inherited_profiles_issue_reporting": disable_profile_inheritance,
281281
"rocrate_uri": rocrate_uri,
282282
"rocrate_relative_root_path": relative_root_path,
283283
"abort_on_first": fail_fast,

rocrate_validator/models.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,26 +1007,43 @@ def _do_validate_(self, context: ValidationContext) -> bool:
10071007

10081008
logger.debug("Running %s checks for Requirement '%s'", len(self._checks), self.name)
10091009
all_passed = True
1010-
for check in [_ for _ in self._checks
1011-
if not context.settings.skip_checks
1012-
or _.identifier not in context.settings.skip_checks]:
1013-
1010+
checks_to_perform = [
1011+
_ for _ in self._checks
1012+
if not context.settings.skip_checks
1013+
or _.identifier not in context.settings.skip_checks
1014+
]
1015+
for check in checks_to_perform:
10141016
try:
10151017
if check.overridden and not check.requirement.profile.identifier == context.profile_identifier:
10161018
logger.debug("Skipping check '%s' because overridden by '%r'",
10171019
check.identifier, [_.identifier for _ in check.overridden_by])
10181020
continue
1019-
context.validator.notify(RequirementCheckValidationEvent(
1020-
EventType.REQUIREMENT_CHECK_VALIDATION_START, check))
1021+
# Determine whether to skip event notification for inherited profiles
1022+
skip_event_notify = False
1023+
if check.requirement.profile.identifier != context.profile_identifier and \
1024+
context.settings.disable_inherited_profiles_issue_reporting:
1025+
logger.debug("Inherited profiles reporting disabled. "
1026+
"Skipping requirement %s as it belongs to an inherited profile %s",
1027+
check.requirement.identifier, check.requirement.profile.identifier)
1028+
skip_event_notify = True
1029+
# Notify the start of the check execution if not skip_event_notify is set to True
1030+
if not skip_event_notify:
1031+
context.validator.notify(RequirementCheckValidationEvent(
1032+
EventType.REQUIREMENT_CHECK_VALIDATION_START, check))
1033+
# Execute the check
10211034
check_result = check.execute_check(context)
10221035
logger.debug("Result of check %s: %s", check.identifier, check_result)
10231036
context.result._add_executed_check(check, check_result)
1024-
context.validator.notify(RequirementCheckValidationEvent(
1025-
EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result))
1037+
# Notify the end of the check execution if not skip_event_notify is set to True
1038+
if not skip_event_notify:
1039+
context.validator.notify(RequirementCheckValidationEvent(
1040+
EventType.REQUIREMENT_CHECK_VALIDATION_END, check, validation_result=check_result))
10261041
logger.debug("Ran check '%s'. Got result %s", check.identifier, check_result)
1042+
# Ensure the check result is a boolean
10271043
if not isinstance(check_result, bool):
10281044
logger.warning("Ignoring the check %s as it returned the value %r instead of a boolean", check.name)
10291045
raise RuntimeError(f"Ignoring invalid result from check {check.name}")
1046+
# Aggregate the check result
10301047
all_passed = all_passed and check_result
10311048
if not all_passed and context.fail_fast:
10321049
break
@@ -1040,7 +1057,8 @@ def _do_validate_(self, context: ValidationContext) -> bool:
10401057
logger.warning("Consider reporting this as a bug.")
10411058
if logger.isEnabledFor(logging.DEBUG):
10421059
logger.exception(e)
1043-
1060+
skipped_checks = set(self._checks) - set(checks_to_perform)
1061+
context.result.skipped_checks.update(skipped_checks)
10441062
logger.debug("Checks for Requirement '%s' completed. Checks passed? %s", self.name, all_passed)
10451063
return all_passed
10461064

@@ -1625,7 +1643,7 @@ def __initialise__(cls, validation_settings: ValidationSettings):
16251643
profiles = [profile]
16261644

16271645
# add inherited profiles if enabled
1628-
if validation_settings.enable_profile_inheritance:
1646+
if not validation_settings.disable_inherited_profiles_issue_reporting:
16291647
profiles.extend(profile.inherited_profiles)
16301648
logger.debug("Inherited profiles: %r", profile.inherited_profiles)
16311649

@@ -1656,9 +1674,20 @@ def __initialise__(cls, validation_settings: ValidationSettings):
16561674
if severity < severity_validation:
16571675
continue
16581676
# count the checks
1659-
requirement_checks = [_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name))
1660-
if not _.overridden or
1661-
_.requirement.profile.identifier == target_profile_identifier]
1677+
requirement_checks = [
1678+
_
1679+
for _ in requirement.get_checks_by_level(
1680+
LevelCollection.get(severity.name)
1681+
)
1682+
if (
1683+
not validation_settings.skip_checks
1684+
or _.identifier not in validation_settings.skip_checks
1685+
)
1686+
and (
1687+
not _.overridden
1688+
or _.requirement.profile.identifier == target_profile_identifier
1689+
)
1690+
]
16621691
num_checks = len(requirement_checks)
16631692
requirement_checks_count += num_checks
16641693
if num_checks > 0:
@@ -2325,12 +2354,20 @@ class ValidationSettings:
23252354
#: The profile identifier to validate against
23262355
profile_identifier: str = DEFAULT_PROFILE_IDENTIFIER
23272356
#: Flag to enable profile inheritance
2357+
# Use the `enable_profile_inheritance` flag with caution: disable inheritance only if the
2358+
# target validation profile is fully self-contained and does not rely on definitions
2359+
# from inherited profiles (e.g., entities defined upstream). For modularization
2360+
# purposes, some base entities and properties are defined in the base RO-Crate
2361+
# profile and are intentionally not redefined in specialized profiles; they are
2362+
# required for validations targeting those specializations and therefore cannot be skipped.
2363+
# Nevertheless, the validator can still suppress issue reporting for checks defined
2364+
# in inherited profiles by setting disable_inherited_profiles_issue_reporting to `True`.
23282365
enable_profile_inheritance: bool = True
23292366
# Validation settings
23302367
#: Flag to abort on first error
23312368
abort_on_first: Optional[bool] = False
2332-
#: Flag to disable inherited profiles reporting
2333-
disable_inherited_profiles_reporting: bool = False
2369+
#: Flag to disable reporting of issues related to inherited profiles
2370+
disable_inherited_profiles_issue_reporting: bool = False
23342371
#: Flag to disable remote crate download
23352372
disable_remote_crate_download: bool = True
23362373
# Requirement settings

rocrate_validator/requirements/python/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ def __init__(self,
5555
self._check_function = check_function
5656

5757
def execute_check(self, context: ValidationContext) -> bool:
58+
if self.requirement.profile.identifier != context.profile_identifier and \
59+
context.settings.disable_inherited_profiles_issue_reporting:
60+
logger.debug("Skipping requirement %s as it belongs to an inherited profile %s",
61+
self.requirement.identifier, self.requirement.profile.identifier)
62+
return True
5863
return self._check_function(self, context)
5964

6065

rocrate_validator/requirements/shacl/checks.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,19 +200,21 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext):
200200
assert shape is not None, "Unable to map the violation to a shape"
201201
requirementCheck = SHACLCheck.get_instance(shape)
202202
assert requirementCheck is not None, "The requirement check cannot be None"
203-
failed_requirements_checks.add(requirementCheck)
204-
violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None)
205-
if violations is None:
206-
failed_requirements_checks_violations[requirementCheck.identifier] = violations = []
207-
violations.append(violation)
203+
if (not shacl_context.settings.skip_checks or
204+
requirementCheck.identifier not in shacl_context.settings.skip_checks):
205+
failed_requirements_checks.add(requirementCheck)
206+
violations = failed_requirements_checks_violations.get(requirementCheck.identifier, None)
207+
if violations is None:
208+
failed_requirements_checks_violations[requirementCheck.identifier] = (violations := [])
209+
violations.append(violation)
208210
# sort the failed checks by identifier and severity
209211
# to ensure a consistent order of the issues
210212
# and to make the fail fast mode deterministic
211213
for requirementCheck in sorted(failed_requirements_checks, key=lambda x: (x.identifier, x.severity)):
212214
# if the check is not in the current profile
213215
# and the disable_inherited_profiles_reporting is enabled, skip it
214216
if requirementCheck.requirement.profile != shacl_context.current_validation_profile and \
215-
shacl_context.settings.disable_inherited_profiles_reporting:
217+
shacl_context.settings.disable_inherited_profiles_issue_reporting:
216218
continue
217219
for violation in failed_requirements_checks_violations[requirementCheck.identifier]:
218220
violating_entity = make_uris_relative(violation.focusNode.toPython(), shacl_context.publicID)
@@ -273,6 +275,9 @@ def __do_execute_check__(self, shacl_context: SHACLValidationContext):
273275
if requirementCheck.identifier not in failed_requirement_checks_notified:
274276
failed_requirement_checks_notified.append(requirementCheck.identifier)
275277
shacl_context.result._add_executed_check(requirementCheck, True)
278+
if requirementCheck.requirement.profile != shacl_context.target_profile and \
279+
shacl_context.settings.disable_inherited_profiles_issue_reporting:
280+
continue
276281
shacl_context.validator.notify(RequirementCheckValidationEvent(
277282
EventType.REQUIREMENT_CHECK_VALIDATION_END, requirementCheck, validation_result=True))
278283
logger.debug("Added skipped check to the context: %s", requirementCheck.identifier)

tests/unit/test_services.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from rocrate_validator.models import ValidationSettings
2222
from rocrate_validator.rocrate import ROCrateMetadata
2323
from rocrate_validator.services import detect_profiles, get_profiles, validate
24-
from tests.ro_crates import InvalidMultiProfileROC, ValidROC
24+
from tests.ro_crates import InvalidMultiProfileROC, ValidROC, InvalidFileDescriptorEntity
2525

2626
# set up logging
2727
logger = logging.getLogger(__name__)
@@ -106,6 +106,64 @@ def test_valid_local_workflow_testing_ro_crate():
106106
"Expected the 'workflow-testing-ro-crate-0.1' profile"
107107

108108

109+
def test_disable_inherited_profiles_issue_reporting():
110+
# Set the rocrate_uri to the workflow testing RO-Crate
111+
crate_path = ValidROC().workflow_testing_ro_crate
112+
logger.debug("Validating a local RO-Crate: %s", crate_path)
113+
114+
# First, validate with inherited profiles issue reporting enabled
115+
settings = ValidationSettings(
116+
rocrate_uri=crate_path,
117+
disable_inherited_profiles_issue_reporting=False
118+
)
119+
result = validate(settings)
120+
total_issues_with_inheritance = len(result.get_issues())
121+
logger.debug("Total issues with inherited profiles issue reporting enabled: %d", total_issues_with_inheritance)
122+
123+
# Now, validate with inherited profiles issue reporting disabled
124+
settings.disable_inherited_profiles_issue_reporting = True
125+
result = validate(settings)
126+
total_issues_without_inheritance = len(result.get_issues())
127+
logger.debug("Total issues with inherited profiles issue reporting disabled: %d", total_issues_without_inheritance)
128+
129+
# Check that disabling inherited profiles issue reporting reduces the number of reported issues
130+
assert total_issues_without_inheritance <= total_issues_with_inheritance, \
131+
"Disabling inherited profiles issue reporting should not increase the number of reported issues"
132+
133+
# Check that all reported issues are from the main profile
134+
main_profile_identifier = "workflow-testing-ro-crate-0.1"
135+
for issue in result.get_issues():
136+
assert issue.check.profile.identifier == main_profile_identifier, \
137+
"All reported issues should belong to the main profile when inherited profiles issue reporting is disabled"
138+
139+
140+
def test_skip_pycheck_on_workflow_ro_crate():
141+
# Set the rocrate_uri to the workflow testing RO-Crate
142+
crate_path = InvalidFileDescriptorEntity().invalid_conforms_to
143+
logger.debug("Validating a local RO-Crate: %s", crate_path)
144+
settings = ValidationSettings(rocrate_uri=crate_path)
145+
result = validate(settings)
146+
assert not result.passed(), \
147+
"The RO-Crate is expected to be invalid because of an incorrect conformsTo field and missing resources"
148+
assert len(result.failed_checks) == 2, "No failed checks expected when skipping the problematic check"
149+
assert any(check.identifier == "ro-crate-1.1_5.3" for check in result.failed_checks), \
150+
"Expected the check 'ro-crate-1.1_5.3' to fail"
151+
assert any(check.identifier == "ro-crate-1.1_12.1" for check in result.failed_checks), \
152+
"Expected the check 'ro-crate-1.1_12.1' to fail"
153+
154+
# Perform a new validation skipping specific checks
155+
settings.skip_checks = ["ro-crate-1.1_5.3", "ro-crate-1.1_12.1"]
156+
result = validate(settings)
157+
assert result.passed(), \
158+
"The RO-Crate should be valid when skipping the checks related to the invalid file descriptor entity"
159+
160+
# Ensure that the skipped checks are indeed skipped
161+
skipped_check_ids = {check.identifier for check in result.skipped_checks}
162+
# logger.error("Skipped checks: %s", result.skipped_checks)
163+
assert "ro-crate-1.1_5.3" in skipped_check_ids, "Expected check 'ro-crate-1.1_5.3' to be skipped"
164+
assert "ro-crate-1.1_12.1" in skipped_check_ids, "Expected check 'ro-crate-1.1_12.1' to be skipped"
165+
166+
109167
def test_valid_local_multi_profile_crate():
110168
# Set the rocrate_uri to the multi-profile RO-Crate
111169
crate_path = InvalidMultiProfileROC().invalid_multi_profile_crate

tests/unit/test_validation_settings.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,34 @@ def test_validation_settings_parse_dict():
2323
"profiles_path": "/path/to/profiles",
2424
"requirement_severity": "RECOMMENDED",
2525
"enable_profile_inheritance": False,
26+
"disable_inherited_profiles_issue_reporting": True,
27+
"skip_checks": ["check1", "check2"]
2628
}
2729
settings = ValidationSettings.parse(settings_dict)
2830
assert str(settings.rocrate_uri) == "/path/to/data"
2931
assert settings.profiles_path == "/path/to/profiles"
3032
assert settings.requirement_severity == Severity.RECOMMENDED
3133
assert settings.enable_profile_inheritance is False
34+
assert settings.disable_inherited_profiles_issue_reporting is True
35+
assert settings.skip_checks == ["check1", "check2"]
3236

3337

3438
def test_validation_settings_parse_object():
3539
existing_settings = ValidationSettings(
3640
rocrate_uri="/path/to/data",
3741
profiles_path="/path/to/profiles",
3842
requirement_severity=Severity.RECOMMENDED,
39-
enable_profile_inheritance=False
43+
enable_profile_inheritance=False,
44+
disable_inherited_profiles_issue_reporting=True,
45+
skip_checks=["check1", "check2"]
4046
)
4147
settings = ValidationSettings.parse(existing_settings)
4248
assert str(settings.rocrate_uri) == "/path/to/data"
4349
assert settings.profiles_path == "/path/to/profiles"
4450
assert settings.requirement_severity == Severity.RECOMMENDED
4551
assert settings.enable_profile_inheritance is False
52+
assert settings.disable_inherited_profiles_issue_reporting is True
53+
assert settings.skip_checks == ["check1", "check2"]
4654

4755

4856
def test_validation_settings_parse_invalid_type():
@@ -72,6 +80,17 @@ def test_validation_settings_enable_profile_inheritance():
7280
assert settings.enable_profile_inheritance is False
7381

7482

83+
def test_validation_settings_disable_inherited_profiles_issue_reporting():
84+
settings = ValidationSettings()
85+
assert settings.disable_inherited_profiles_issue_reporting is False
86+
87+
settings = ValidationSettings(disable_inherited_profiles_issue_reporting=True)
88+
assert settings.disable_inherited_profiles_issue_reporting is True
89+
90+
settings = ValidationSettings(disable_inherited_profiles_issue_reporting=False)
91+
assert settings.disable_inherited_profiles_issue_reporting is False
92+
93+
7594
def test_validation_settings_data_path():
7695
settings = ValidationSettings(rocrate_uri="/path/to/data")
7796
assert str(settings.rocrate_uri) == "/path/to/data"

0 commit comments

Comments
 (0)