From 290272ca72d78df8329133484e3273340bdee850 Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 11 Sep 2026 12:46:48 +0200 Subject: [PATCH] test(validation): check context equality over every schema, not thirteen The equivalence was pinned on the examples only, which is half the corpus and the half least likely to break it. The compliance fixtures carry as many schemas again and exist precisely to hold the unusual constructs. Adds the same sweep to the parity suite, so it runs against whatever oold-schema main has now rather than against the snapshot taken when the fixture slice was cut - that is where a construct which breaks the claim would show up first. --- tests/test_validation/conftest.py | 12 ++++++ tests/test_validation/test_jsonld.py | 37 ++++++++++------ tests/test_validation/test_parity_live.py | 52 +++++++++++++++++++++++ 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/tests/test_validation/conftest.py b/tests/test_validation/conftest.py index c8fe257..4573703 100644 --- a/tests/test_validation/conftest.py +++ b/tests/test_validation/conftest.py @@ -16,6 +16,18 @@ RESOLVER_CACHE = DATA.parent / "resolver_cache" +def embedded_schemas(node): + """Every schema inside a compliance fixture, whatever nesting that file happens to use.""" + if isinstance(node, dict): + if isinstance(node.get("schema"), dict): + yield node["schema"] + for value in node.values(): + yield from embedded_schemas(value) + elif isinstance(node, list): + for value in node: + yield from embedded_schemas(value) + + @pytest.fixture def data_dir() -> Path: """The committed slice of oold-schema's examples.""" diff --git a/tests/test_validation/test_jsonld.py b/tests/test_validation/test_jsonld.py index 6cf5a61..44c7374 100644 --- a/tests/test_validation/test_jsonld.py +++ b/tests/test_validation/test_jsonld.py @@ -26,7 +26,7 @@ roundtrip, ) -from .conftest import read +from .conftest import embedded_schemas, read # ------------------------------------------------------------------ canonical / lost_keys @@ -245,25 +245,36 @@ def test_the_walker_and_pyld_agree_on_the_effective_term_set(resolver, data_dir) # @context" - a failure of the harness that reads exactly like a failure of the subject. options = {"processingMode": "json-ld-1.1"} - compared = 0 - for path in sorted(data_dir.glob("*.schema.json")): - loaded = resolver.load(path) - if "@context" not in loaded.schema: - continue - context = resolve_context(loaded.schema, loaded.base_uri, resolver) + def agree(schema, base_uri, base_url, label): + """True when both produce the same term set; None when the schema has nothing to compare.""" + if "@context" not in schema: + return None + context = resolve_context(schema, base_uri, resolver) if context.errors or context.is_empty: - continue - + return None active = processor.process_context( processor._get_initial_context(options), context.as_jsonld(), - {**options, "base": loader.url_for(path.name), "documentLoader": loader}, + {**options, "base": base_url, "documentLoader": loader}, ) theirs = {term for term in active["mappings"] if not term.startswith("@")} - assert set(context.terms()) == theirs, path.name - compared += 1 + assert set(context.terms()) == theirs, label + return True + + compared = 0 + for path in sorted(data_dir.glob("*.schema.json")): + loaded = resolver.load(path) + compared += bool(agree(loaded.schema, loaded.base_uri, loader.url_for(path.name), path.name)) + + # The compliance fixtures carry as many schemas again as the examples do, and they are where + # the unusual constructs live - that is what they are for - so leaving them out would test the + # equivalence on exactly the documents least likely to break it. + synthetic = loader.url_for("compliance-case.schema.json") + for path in sorted((data_dir / "compliance").glob("*.json")): + for index, schema in enumerate(embedded_schemas(json.loads(path.read_text(encoding="utf-8")))): + compared += bool(agree(schema, synthetic, synthetic, f"{path.name}[{index}]")) - assert compared >= 13, f"only {compared} schemas carried a resolvable @context" + assert compared >= 26, f"only {compared} schemas carried a resolvable @context" def test_pyld_keeps_a_scoped_context_unresolved(resolver, data_dir): diff --git a/tests/test_validation/test_parity_live.py b/tests/test_validation/test_parity_live.py index 086c67f..87e195c 100644 --- a/tests/test_validation/test_parity_live.py +++ b/tests/test_validation/test_parity_live.py @@ -13,6 +13,7 @@ from __future__ import annotations +import json import shutil import subprocess from pathlib import Path @@ -20,6 +21,11 @@ import pytest from oold.validation import Options, run_compliance, validate_directory +from oold.validation.context_resolution import resolve_context +from oold.validation.loader import DocumentLoader +from oold.validation.resolve import Resolver + +from .conftest import embedded_schemas pytestmark = pytest.mark.parity @@ -149,3 +155,49 @@ def test_every_mapped_rule_resolves_against_the_upstream_catalog(upstream): known = {r["id"] for r in json.loads(catalog.read_text(encoding="utf-8"))["rules"]} unknown = {check: rule for check, rule in rule_map().items() if rule not in known} assert not unknown, f"the registry cites ids absent from the upstream catalog: {unknown}" + + +def test_the_walker_and_pyld_agree_across_the_upstream_corpus(upstream): + """The same equivalence the committed slice pins, over a corpus that is still moving. + + `context_resolution`'s docstring justifies a hand-written walk by what pyld does not return. + The fixture slice is a snapshot and can only pin that against constructs upstream had when it + was taken; this runs it against whatever `main` has now, which is where a construct that + breaks the claim would appear first. + """ + from pyld.jsonld import JsonLdProcessor + + resolver = Resolver(offline=False) + processor = JsonLdProcessor() + options = {"processingMode": "json-ld-1.1"} + compared = 0 + + def agree(schema, base_uri, base_url, loader, label): + if "@context" not in schema: + return False + context = resolve_context(schema, base_uri, resolver) + if context.errors or context.is_empty: + return False + active = processor.process_context( + processor._get_initial_context(options), + context.as_jsonld(), + {**options, "base": base_url, "documentLoader": loader}, + ) + theirs = {term for term in active["mappings"] if not term.startswith("@")} + assert set(context.terms()) == theirs, label + return True + + examples = upstream / "examples" + loader = DocumentLoader(resolver, directory=examples) + for path in sorted(examples.glob("*.schema.json")): + loaded = resolver.load(path) + compared += agree(loaded.schema, loaded.base_uri, loader.url_for(path.name), loader, path.name) + + compliance = examples / "compliance" + comp_loader = DocumentLoader(resolver, directory=compliance) + synthetic = comp_loader.url_for("compliance-case.schema.json") + for path in sorted(compliance.glob("*.json")): + for index, schema in enumerate(embedded_schemas(json.loads(path.read_text(encoding="utf-8")))): + compared += agree(schema, synthetic, synthetic, comp_loader, f"{path.name}[{index}]") + + assert compared >= 20, f"only {compared} upstream schemas carried a resolvable @context"