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
5 changes: 3 additions & 2 deletions src/oold/validation/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ def _classify(target: Path) -> str:
"--meta",
"meta",
multiple=True,
metavar="VERSION",
metavar="VERSION|PATH",
help="Meta-schema version: latest (default), a version such as 0.7.0, remote, or all. "
"Repeat to validate against several at once.",
"A path to an oold-schema checkout validates against its working tree, including rules it "
"has not released yet. Repeat to validate against several at once.",
)
_offline_option = click.option(
"--offline", is_flag=True, help="Never fetch over the network; use local files and the cache."
Expand Down
58 changes: 56 additions & 2 deletions src/oold/validation/meta_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
from .resolve import SchemaResolutionError, default_cache_dir, http_get_json

META_SCHEMA_FILE = "oold-meta-schema.json"
#: The dialect body the wrapper `$ref`s, present from 1.0.0-rc.2 onward. Named here because a
#: local checkout has no index entry to declare its file set.
META_SCHEMA_BASE_FILE = "oold-meta-schema-base.json"
UI_META_SCHEMA_FILE = "oold-ui-meta-schema.json"
PATTERN_LINT_FILE = "oold-pattern-lint.schema.json"

Expand Down Expand Up @@ -428,6 +431,47 @@ def load_tracked(version: str) -> MetaBundle:
)


LOCAL = "local"


def load_local(directory: Path | str) -> MetaBundle:
"""Load a meta-schema set from a checkout rather than from a tracked release.

Exists for the one case the tracked versions cannot serve: validating a change to the
specification *before* it is released. A tracked version is a tag and `--meta remote` is
`refs/heads/main`, so a rule added on a branch is invisible to both - the checks bound to it
skip, reporting that the version never stated it, and the pull request that introduces a rule
is the one run that cannot enforce it.

`directory` is a checkout of oold-schema, or its `meta/` directly. Unlike a tracked version
nothing here is checksummed: the files are working state and expected to change, which is the
point. So this must not be the default, and a released version must never be read this way -
`load_tracked` stays the only path to those, and its checksums stay meaningful.
"""
root = Path(directory)
candidate = root / "meta" if (root / "meta" / META_SCHEMA_FILE).is_file() else root
if not (candidate / META_SCHEMA_FILE).is_file():
raise MetaSchemaError(
f"{root} holds no meta-schemas: expected {META_SCHEMA_FILE} in it or in its meta/ subdirectory"
)
present = [name for name in [*meta_files(), META_SCHEMA_BASE_FILE] if (candidate / name).is_file()]
documents = _read_documents(candidate, f"meta-schemas in {candidate}", present)
catalog, catalog_error = _read_rules(candidate)
rules, rules_error = _parse_rules(catalog, catalog_error)
rules_schema, rules_schema_error = _read_rules_schema(candidate)
return MetaBundle(
version=LOCAL,
origin=str(candidate),
documents=documents,
registry=_build_registry(documents),
rules=rules,
rules_document=catalog,
rules_schema=rules_schema,
rules_schema_error=rules_schema_error,
rules_error=rules_error,
)


# ---------------------------------------------------------------------------- remote


Expand Down Expand Up @@ -518,9 +562,17 @@ def resolve_selection(
requested = list(selectors) or [LATEST]

wanted: list[str] = []
local_paths: dict[str, str] = {}
for selector in requested:
name = selector.strip()
if name == ALL:
# A selector naming a directory is a checkout, not a version. Recognised by being one,
# rather than by a prefix, so `--meta ../oold-schema` reads the way a path should; a
# tracked version name can never collide, since none of them is a directory here.
if name not in (ALL, LATEST, REMOTE) and Path(name).is_dir():
key = f"{LOCAL}:{Path(name).resolve()}"
local_paths[key] = name
resolved = [key]
elif name == ALL:
resolved = tracked_versions()
if not resolved:
raise MetaSchemaError(f"no meta-schema versions are tracked in {meta_dir()}")
Expand All @@ -534,7 +586,9 @@ def resolve_selection(

bundles: list[MetaBundle] = []
for version in wanted:
if version == REMOTE:
if version in local_paths:
bundles.append(load_local(local_paths[version]))
elif version == REMOTE:
bundles.append(load_remote(offline=offline, timeout=timeout))
else:
bundles.append(load_tracked(version))
Expand Down
37 changes: 37 additions & 0 deletions tests/test_validation/test_meta_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,40 @@ def fake_get(uri, timeout=10.0):
lambda *a, **k: pytest.fail("offline mode fetched over the network"),
)
assert meta_store.load_remote(offline=True).version == "remote"


def test_a_directory_selector_loads_the_meta_schemas_in_it(tmp_path):
"""`--meta <path>` reads a checkout, which is the only way to see an unreleased rule.

A tracked version is a tag and `remote` is `refs/heads/main`, so a rule added on a branch is
invisible to both: the checks bound to it skip, saying the version never stated it, and the
pull request introducing a rule becomes the one run that cannot enforce it.
"""
latest = meta_store.meta_dir() / meta_store.latest_version()
checkout = tmp_path / "oold-schema" / "meta"
checkout.mkdir(parents=True)
for src in latest.glob("*.json"):
(checkout / src.name).write_bytes(src.read_bytes())

# A rule that exists only here, the way a branch would carry one.
catalog = json.loads((checkout / meta_store.RULES_FILE).read_text(encoding="utf-8"))
invented = dict(catalog["rules"][0], id="OOLD-XXX-beef", summary="only in the checkout")
catalog["rules"].append(invented)
(checkout / meta_store.RULES_FILE).write_text(json.dumps(catalog), encoding="utf-8")

tracked = meta_store.load_tracked(meta_store.latest_version())
# Both the repository root and its meta/ directory resolve to the same thing.
for selector in (tmp_path / "oold-schema", checkout):
bundle = meta_store.resolve_selection([str(selector)])[0]
assert bundle.version == meta_store.LOCAL
ids = {r.id for r in bundle.rules}
assert "OOLD-XXX-beef" in ids, "a local checkout must surface a rule no release carries"
assert ids - {"OOLD-XXX-beef"} == {r.id for r in tracked.rules}

assert "OOLD-XXX-beef" not in {r.id for r in tracked.rules}, "the tracked copy must be untouched"


def test_a_directory_without_meta_schemas_is_rejected_by_name(tmp_path):
"""The likely mistake is pointing at the wrong directory, so say what was expected."""
with pytest.raises(MetaSchemaError, match=meta_store.META_SCHEMA_FILE):
meta_store.load_local(tmp_path)
Loading