diff --git a/docs/index.md b/docs/index.md index 5860ac68..674f2d20 100644 --- a/docs/index.md +++ b/docs/index.md @@ -237,7 +237,7 @@ You can contribute a converter. The [Converters Guide](../converters/README.md) 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 8583c53c..70b5f2ef 100644 --- a/validation/tests/test_validate.py +++ b/validation/tests/test_validate.py @@ -16,6 +16,7 @@ # under the License. import json +import sys from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path @@ -42,6 +43,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: return { "version": "0.2.0.dev0", diff --git a/validation/validate.py b/validation/validate.py index 99b390c6..0911a554 100644 --- a/validation/validate.py +++ b/validation/validate.py @@ -145,7 +145,7 @@ def _check_unique_keys(self, node: yaml.Node, visited: set) -> None: self._check_unique_keys(child, visited) -def validate_schema(data: dict, schema: dict) -> list[str]: +def validate_schema(data: object, schema: dict) -> list[str]: """Validate against JSON Schema.""" validator = Draft202012Validator(schema) errors = [] @@ -386,8 +386,8 @@ def main(): sys.exit(1) # Run validations - errors = [] - errors.extend(validate_schema(data, schema)) + errors = validate_schema(data, schema) + semantic_checks_skipped = bool(errors) # Semantic checks rely on valid structure; let schema validation report # malformed inputs (including legacy arrays) without traversing them. @@ -410,6 +410,8 @@ def main(): print(f"\nValidation FAILED with {len(actual_errors)} error(s):\n") for error in actual_errors: print(f" {error}") + if semantic_checks_skipped: + print("\nSemantic checks skipped until schema errors are fixed.") sys.exit(1) else: print(f"Validation PASSED: {yaml_path.name}")