diff --git a/docs/index.md b/docs/index.md index 9bc4e26c..0d72d526 100644 --- a/docs/index.md +++ b/docs/index.md @@ -236,7 +236,7 @@ You can build a converter in a separate repository or contribute one to the [oss No. Import converters translate existing vendor-specific models into the Ossie format automatically. Your existing models remain intact — Ossie provides an additional interchange layer on top of them. **How do I validate an Ossie model?** -Use the [validation script](../validation/validate.py) included in the repository. It checks your model against the [JSON Schema](../core-spec/ossie-schema.json), validates SQL expressions across dialects, and ensures referential integrity between datasets and relationships. +Use the [validation script](../validation/validate.py) included in the repository. It checks your model against the [JSON Schema](../core-spec/ossie-schema.json), validates SQL expressions across dialects, and ensures referential integrity between datasets and relationships. Schema errors are reported first; semantic checks run after those errors are fixed so malformed document shapes cannot interrupt validation. ### Technical diff --git a/validation/tests/test_validate.py b/validation/tests/test_validate.py index 1a9d5ec6..ab2551d7 100644 --- a/validation/tests/test_validate.py +++ b/validation/tests/test_validate.py @@ -18,6 +18,7 @@ import copy import json import re +import sys import urllib.request from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path @@ -45,6 +46,71 @@ def core_schema() -> dict: return json.loads(schema_path.read_text()) +@pytest.mark.parametrize( + ("content", "error_path"), + [ + ("", "(root)"), + ("[]\n", "(root)"), + ("scalar\n", "(root)"), + ("semantic_model: invalid\n", "(root)"), + ], +) +def test_schema_invalid_documents_report_errors_without_traceback( + content: str, + error_path: str, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + model_path = tmp_path / "model.yaml" + model_path.write_text(content, encoding="utf-8") + monkeypatch.setattr(sys, "argv", ["validate.py", str(model_path)]) + + with pytest.raises(SystemExit) as raised: + _VALIDATE.main() + + output = capsys.readouterr().out + assert raised.value.code == 1 + assert f"[Schema] {error_path}:" in output + assert "Semantic checks skipped until schema errors are fixed." in output + + +def test_schema_errors_defer_semantic_errors( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + model_path = tmp_path / "model.yaml" + model_path.write_text( + "version: 0.2.0.dev0\n" + "name: sales\n" + "unknown: true\n" + "datasets:\n" + " - name: orders\n" + " source: db.orders\n" + " - name: orders\n" + " source: db.orders_archive\n" + "relationships:\n" + " - name: orders_to_customers\n" + " from: orders\n" + " to: customers\n" + " from_columns: [customer_id]\n" + " to_columns: [id]\n", + encoding="utf-8", + ) + monkeypatch.setattr(sys, "argv", ["validate.py", str(model_path)]) + + with pytest.raises(SystemExit) as raised: + _VALIDATE.main() + + output = capsys.readouterr().out + assert raised.value.code == 1 + assert "[Schema] (root):" in output + assert "Semantic checks skipped until schema errors are fixed." in output + assert "[Unique]" not in output + assert "[Reference]" not in output + + def _document(datasets: list[dict], relationships: list[dict]) -> dict: # Deep-copied so a test can edit the document it gets back without # changing the module-level fixtures below for every test after it. diff --git a/validation/validate.py b/validation/validate.py index e4e04eed..4fdc39da 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -197,7 +197,7 @@ def _retrieve_local_schema(uri: str) -> str: _SCHEMA_REGISTRY = Registry(retrieve=_retrieve_local_schema) -def validate_schema(data: dict, schema: dict) -> list[str]: +def validate_schema(data: object, schema: dict) -> list[str]: """Validate against JSON Schema, resolving Ossie schema references locally.""" validator = Draft202012Validator(schema, registry=_SCHEMA_REGISTRY) errors = [] @@ -1801,6 +1801,10 @@ def validate_file(yaml_path: Path, schema: dict) -> bool: print(f"\nValidation FAILED with {len(actual_errors)} error(s):\n") for error in actual_errors: print(f" {error}") + # Only validate_schema() emits [Schema] errors, and any of them stops + # validate_document() before the semantic checks. + if any(error.startswith("[Schema]") for error in actual_errors): + print("\nSemantic checks skipped until schema errors are fixed.") return False print(f"Validation PASSED: {yaml_path.name}")