diff --git a/src/oold/validation/compliance.py b/src/oold/validation/compliance.py index 4a0e3c6..942a202 100644 --- a/src/oold/validation/compliance.py +++ b/src/oold/validation/compliance.py @@ -285,6 +285,11 @@ def record(description: str, kind: str, passed: bool, detail: str = "") -> None: return try: feature_schema = dereference(schema_ref) + # This is the compliance suite itself, walking fixture files rather than one committed + # document: a bad `schemaRef` in one group must record a "setup" failure for that group + # and let `run_suite` move on to the next, not abort the whole suite. `dereference` can + # also raise something other than SchemaResolutionError - a fixture resolving to a + # non-dict schema fails differently downstream - so the catch stays broad to match. except Exception as exc: record(str(schema_ref), "setup", False, f"could not dereference: {exc}") return @@ -333,6 +338,9 @@ def record(description: str, kind: str, passed: bool, detail: str = "") -> None: got_rdf == want_rdf, "" if got_rdf == want_rdf else f"not isomorphic\n got: {got_rdf}\n want: {want_rdf}", ) + # One bad fixture case must record an "rdf" failure and let the loop continue to the + # next test in the group, not abort the whole suite; fixture `data` is arbitrary, so + # this is not limited to JsonLdError. except Exception as exc: record(description, "rdf", False, describe_jsonld_error(exc)) @@ -359,6 +367,8 @@ def record(description: str, kind: str, passed: bool, detail: str = "") -> None: else f"instance != reconstruction\n in: {json.dumps(canonical(document))}" f"\n out: {json.dumps(canonical(restored))}", ) + # Same reasoning as the "rdf" case above: one bad fixture case records a "roundtrip" + # failure and the loop continues, rather than the whole suite aborting. except Exception as exc: record(description, "roundtrip", False, describe_jsonld_error(exc)) @@ -367,6 +377,9 @@ def record(description: str, kind: str, passed: bool, detail: str = "") -> None: raised: BaseException | None = None try: jsonld.to_rdf(data, loader.options(base=rdf_base, format="application/n-quads")) + # An `expectErrorCode` fixture asserts that processing this data *does* raise, so + # catching broadly is the point here, not a fallback: whatever comes out is exactly + # what `_error_code` and the assertion below need to inspect. except Exception as exc: raised = exc if raised is None: diff --git a/src/oold/validation/generate.py b/src/oold/validation/generate.py index dacab96..5dc01ac 100644 --- a/src/oold/validation/generate.py +++ b/src/oold/validation/generate.py @@ -326,6 +326,13 @@ def generate(schema: dict[str, Any], unique_ids: bool = True) -> GenerationResul except RecursionError: result.error = "generation recursed too deeply; the schema may not be fully bounded" return result + # `schema` reaches here even when `schema.meta` already failed: only unresolved `$ref`s stop + # the pipeline before this call, not meta-schema invalidity. `_generate` walks keyword values + # such as `properties` assuming they are shaped as the meta-schema requires, so a malformed + # one (say a string instead of an object) raises a plain AttributeError/TypeError from this + # module's own traversal, not from anything with a narrower, catchable type. Letting it + # propagate would abort the whole directory run over one bad schema instead of reporting a + # `generate.satisfiable` FAIL and moving on to the next file. except Exception as exc: result.error = f"generation failed: {type(exc).__name__}: {exc}" return result diff --git a/src/oold/validation/instance_checks.py b/src/oold/validation/instance_checks.py index efa23ed..c9f54dc 100644 --- a/src/oold/validation/instance_checks.py +++ b/src/oold/validation/instance_checks.py @@ -75,6 +75,12 @@ def validate_instance(instance: Any, schema: dict[str, Any]) -> InstanceCheckRes try: validator = Draft202012Validator(schema, format_checker=OOLD_FORMAT_CHECKER) errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path)) + # `schema` is whatever the instance's `$schema` dereferences to; nothing here has checked it + # is a well-formed OO-LD schema first, so it can carry a malformed keyword value (say + # `properties` typed as something other than an object) that makes jsonschema's internals + # raise a plain AttributeError/TypeError while walking it as a validator, not a + # jsonschema-specific type. Reporting that as an `instance.schema` FAIL, rather than raising, + # is what lets the run continue to the next instance file. except Exception as exc: result.valid = False result.errors = [f"could not validate against the schema: {type(exc).__name__}: {exc}"] @@ -118,6 +124,12 @@ def roundtrip_instance( else: result.method = "compacted" result.restored = jsonld.compact(back, schema_url, loader.options(base=instance_url)) + # Unlike roundtrip.py's `roundtrip()`, `schema` here has not already survived a successful + # `generate()` call - no such gate exists on the instance-check path - so a malformed keyword + # value can still reach `embedded_properties`/`schema_to_frame` and raise a plain + # AttributeError/TypeError from their traversal, on top of the JsonLdError the jsonld.* calls + # themselves can raise. Reporting that as a `roundtrip.instance` FAIL, rather than raising, + # is what lets the run continue to the next instance file. except Exception as exc: result.error = describe_jsonld_error(exc) return result diff --git a/src/oold/validation/pattern_lint.py b/src/oold/validation/pattern_lint.py index fc8512d..990907a 100644 --- a/src/oold/validation/pattern_lint.py +++ b/src/oold/validation/pattern_lint.py @@ -172,6 +172,10 @@ def lint(schema: dict[str, Any], bundle: MetaBundle) -> PatternLintResult: key=lambda e: list(e.absolute_path), ) result.schema_errors = [format_error(error) for error in errors] + # `schema` here is the same untrusted, not-yet-vetted document `validate_against_meta` sees + # (see the comment there), walked the same way by iter_errors, so the exception surface is + # the same: not just jsonschema-specific types, but plain AttributeError/TypeError from + # malformed keyword values and referencing.exceptions.Unresolvable from a bad `$ref`. except Exception as exc: result.schema_errors = [f"pattern lint could not run: {type(exc).__name__}: {exc}"] diff --git a/src/oold/validation/pipeline.py b/src/oold/validation/pipeline.py index 023414a..68d632f 100644 --- a/src/oold/validation/pipeline.py +++ b/src/oold/validation/pipeline.py @@ -349,6 +349,11 @@ def _check_schema_jsonld(run: _Run, name: str, raw: dict[str, Any]) -> None: run.loader.options(base=run.loader.base_url), ) run.add("context.remote", name, OK) + # Kept broad deliberately. This calls jsonld.expand on the same shared pyld.jsonld + # module predicates.py calls it through, so an unexpected exception here has the same + # nowhere-to-go problem as the one documented there: no attribution path of its own, and + # no per-file guard in validate_directory to land in if it propagates. Narrowing this + # waits on #145 for the same reason predicates.py's catch does. except Exception as exc: from .loader import describe_jsonld_error diff --git a/src/oold/validation/predicates.py b/src/oold/validation/predicates.py index 0697b7f..27ff11f 100644 --- a/src/oold/validation/predicates.py +++ b/src/oold/validation/predicates.py @@ -114,6 +114,13 @@ def classify_property(name: str, value: Any, context: Any, options: dict[str, An """ try: expanded = jsonld.expand({"@context": context, name: value, ANCHOR: "anchor"}, options) + # Kept broad deliberately. Routing to ERRORED already distinguishes a genuine processor + # failure from a permitted missing term (2a03e2f) - a narrower catch would only change which + # exceptions get that attribution, not add any. And `validate_directory` has no per-file + # guard (pipeline.py:649-652), so anything this does not catch would discard the verdicts for + # every other file in the run: there is currently nowhere for "this is my bug, not your + # document's" to go without either misattributing it or aborting the run. Narrowing this + # waits on that gap being closed; see #145. except Exception as exc: return PropertyOutcome(name=name, status=ERRORED, detail=f"expansion failed: {describe_jsonld_error(exc)}") diff --git a/src/oold/validation/roundtrip.py b/src/oold/validation/roundtrip.py index ffae8a5..037cb5e 100644 --- a/src/oold/validation/roundtrip.py +++ b/src/oold/validation/roundtrip.py @@ -21,6 +21,7 @@ from .frame import embedded_properties, instance_rdf_types, schema_to_frame from .loader import DocumentLoader, describe_jsonld_error +from .resolve import SchemaResolutionError #: Keys that are metadata rather than data, and are excluded from both comparisons. _METADATA_KEYS = frozenset({"@context", "$schema"}) @@ -210,7 +211,12 @@ def roundtrip( else: result.method = "compacted" result.restored = jsonld.compact(back, context_ref, loader.options(base=rdf_base)) - except Exception as exc: + # jsonld.to_rdf/from_rdf/frame/compact raise JsonLdError, and the document loader they call + # through converts every resolver failure to one (see loader._loader_error), so those two + # types cover this block. Anything else is a bug in this package's own code - `frame.py`'s + # `embedded_properties`/`schema_to_frame`, most likely - and must not be misreported as a + # `roundtrip.generated` finding against the schema under test. + except (jsonld.JsonLdError, SchemaResolutionError) as exc: result.ok = False result.error = describe_jsonld_error(exc) return result diff --git a/src/oold/validation/schema_checks.py b/src/oold/validation/schema_checks.py index 3d58bef..776fd63 100644 --- a/src/oold/validation/schema_checks.py +++ b/src/oold/validation/schema_checks.py @@ -88,6 +88,12 @@ def validate_against_meta(schema: Any, bundle: MetaBundle) -> MetaValidationResu bundle.meta_validator().iter_errors(schema), key=lambda e: list(e.absolute_path), ) + # `schema` here is a candidate document straight off disk, not yet known to be well-formed + # in any way, and iter_errors walks it as jsonschema instance data: a malformed keyword + # value (say `"properties": "x"`) surfaces as a plain AttributeError/TypeError from + # jsonschema's internals, and an unresolvable `$ref` as a referencing.exceptions.Unresolvable + # subclass, not a ValidationError - so no jsonschema-specific type covers this. The docstring + # above commits this function to never raising, which is what a broken-schema caller needs. except Exception as exc: return MetaValidationResult( valid=False,