Skip to content

Commit 40f9a56

Browse files
committed
feat(validation): ✨ support unconditional validation abort
Add an abort mechanism to the validation framework so a check can stop the whole run when the metadata is unreadable (e.g. the file descriptor is not valid JSON), instead of letting later checks emit false positives.
1 parent 6d570f5 commit 40f9a56

3 files changed

Lines changed: 62 additions & 0 deletions

File tree

rocrate_validator/models/requirement.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import annotations
1616

1717
import importlib
18+
import json
1819
from abc import ABC, abstractmethod
1920
from dataclasses import dataclass
2021
from functools import total_ordering
@@ -225,6 +226,11 @@ def _do_validate_(self, context: ValidationContext) -> bool:
225226
logger.debug("Skipping check '%s' because: %s", check.name, e)
226227
context.result._add_skipped_check(check)
227228
continue
229+
except json.JSONDecodeError as e:
230+
# The file descriptor is not valid JSON, so this check could not run.
231+
# This is a malformed-input problem (reported as an ad-hoc issue by the
232+
# dedicated "File Descriptor JSON format" check), not a validator bug.
233+
logger.debug("Skipping check %s: file descriptor is not valid JSON: %s", check, e)
228234
except Exception as e:
229235
if context.maybe_warn_offline_cache_miss(e):
230236
logger.debug("Offline cache miss during check %s: %s", check, e)
@@ -233,6 +239,9 @@ def _do_validate_(self, context: ValidationContext) -> bool:
233239
logger.warning("Consider reporting this as a bug.")
234240
if logger.isEnabledFor(logging.DEBUG):
235241
logger.exception("Unhandled exception during check execution", exc_info=e)
242+
# Stop running further checks once the metadata is known to be unusable.
243+
if context.aborted:
244+
break
236245
skipped_checks = set(self._checks) - set(checks_to_perform)
237246
context.result.skipped_checks.update(skipped_checks)
238247
logger.debug(

rocrate_validator/models/validation.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ def __do_validate__(self, requirements: list[Requirement] | None = None) -> Vali
227227
logger.debug("Aborting on first requirement failure")
228228
terminate = True
229229
break
230+
# Unconditional abort: the metadata is unusable (e.g. not valid JSON),
231+
# so stop now instead of reporting false positives from later checks.
232+
if context.aborted:
233+
logger.debug("Aborting validation: %s", context.abort_reason)
234+
terminate = True
235+
break
230236
self.notify(ProfileValidationEvent(EventType.PROFILE_VALIDATION_END, profile=profile))
231237
if terminate:
232238
break
@@ -287,6 +293,10 @@ def __init__(self, validator: Validator, settings: ValidationSettings):
287293
self._properties: dict = {}
288294
# URLs already reported as missing from the HTTP cache during this run
289295
self._offline_cache_misses_warned: set[str] = set()
296+
# flag set when the validation must be aborted because the metadata
297+
# cannot be read (e.g. the file descriptor is not valid JSON)
298+
self._aborted: bool = False
299+
self._abort_reason: str | None = None
290300

291301
# initialize the ROCrate object
292302
if settings.metadata_dict:
@@ -426,6 +436,43 @@ def fail_fast(self) -> bool:
426436
"""
427437
return bool(self.settings.abort_on_first)
428438

439+
@property
440+
def aborted(self) -> bool:
441+
"""
442+
Whether the validation has been aborted because the metadata cannot be read.
443+
444+
Unlike :attr:`fail_fast`, this abort is unconditional: when the file descriptor
445+
cannot be parsed (e.g. it is not valid JSON) no metadata can be read, so any
446+
further check would only produce false positives.
447+
448+
:return: ``True`` if the validation has been aborted, ``False`` otherwise
449+
:rtype: bool
450+
"""
451+
return self._aborted
452+
453+
@property
454+
def abort_reason(self) -> str | None:
455+
"""
456+
The reason why the validation has been aborted, if any.
457+
"""
458+
return self._abort_reason
459+
460+
def abort_validation(self, reason: str | None = None) -> None:
461+
"""
462+
Signal that the validation cannot meaningfully continue because the metadata
463+
is unusable (e.g. the file descriptor is not valid JSON).
464+
465+
The check that detects the problem is expected to record its issue first and
466+
then call this method; the validation loop stops as soon as the current
467+
requirement completes, avoiding false positives from checks that depend on
468+
readable metadata.
469+
470+
:param reason: An optional human-readable description of the abort cause
471+
"""
472+
self._aborted = True
473+
self._abort_reason = reason
474+
logger.debug("Validation aborted: %s", reason)
475+
429476
@property
430477
def rel_fd_path(self) -> Path:
431478
"""

rocrate_validator/requirements/shacl/requirements.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def finalize(cls, context: ValidationContext) -> None:
101101

102102
logger.debug("Starting %s requirement finalization for context %s", cls.__name__, context)
103103

104+
# If the validation was aborted (e.g. the metadata is not valid JSON), the data
105+
# graph cannot be parsed: skip the forced SHACL run to avoid false positives.
106+
if context.aborted:
107+
logger.debug("Skipping forced SHACL run: validation aborted (%s)", context.abort_reason)
108+
return
109+
104110
# extract profiles and target profile from context
105111
profiles = context.profiles
106112

0 commit comments

Comments
 (0)