From 4b5a1104a1207e6ad2b6d845ac9124f9673ca675 Mon Sep 17 00:00:00 2001 From: Yufei Date: Fri, 18 Sep 2026 15:03:43 -0700 Subject: [PATCH 1/4] Fix SDK document ordering and schema-independent root validation --- .../__snapshots__/test_msi_to_ossie.ambr | 8 +-- .../__snapshots__/test_ossie_to_msi.ambr | 2 +- converters/dbt/tests/test_msi_to_ossie.py | 1 + .../src/ossie_orionbelt/validation.py | 14 ++-- .../orionbelt/tests/test_ossie_validation.py | 67 +++++++++++++++++++ converters/sigma/tests/test_roundtrip.py | 1 + .../wisdom/tests/test_wisdom_to_ossie.py | 1 + python/README.md | 3 +- python/src/ossie/models.py | 10 ++- python/tests/test_models.py | 49 ++++++++++++++ 10 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 converters/orionbelt/tests/test_ossie_validation.py diff --git a/converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr b/converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr index 88d1f9b8..a795f9ae 100644 --- a/converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr +++ b/converters/dbt/tests/__snapshots__/test_msi_to_ossie.ambr @@ -17,6 +17,7 @@ # under the License. # name: TestMetricConversion.test_derived_metric_nested ''' + version: 0.2.0.dev0 name: semantic_model datasets: - name: orders @@ -63,12 +64,12 @@ dialects: - dialect: ANSI_SQL expression: (SUM(orders.amount) - SUM(orders.cost_amount)) - SUM(orders.expense_amount) - version: 0.2.0.dev0 ''' # --- # name: TestMetricConversion.test_ratio_metric_inlines_sub_expressions ''' + version: 0.2.0.dev0 name: semantic_model datasets: - name: orders @@ -101,12 +102,12 @@ - dialect: ANSI_SQL expression: (SUM(orders.amount)) / (SUM(CASE WHEN orders.order_id IS NOT NULL THEN 1 ELSE 0 END)) - version: 0.2.0.dev0 ''' # --- # name: TestMetricFilterFlattening.test_metric_and_measure_filters_combined_with_and ''' + version: 0.2.0.dev0 name: semantic_model datasets: - name: orders @@ -124,12 +125,12 @@ - dialect: ANSI_SQL expression: SUM(CASE WHEN (status = 'paid') AND (region = 'intl') THEN orders.amount END) - version: 0.2.0.dev0 ''' # --- # name: TestRelationshipConversion.test_three_datasets_produce_all_pairs ''' + version: 0.2.0.dev0 name: semantic_model datasets: - name: users_a @@ -182,7 +183,6 @@ - user_id to_columns: - user_id - version: 0.2.0.dev0 ''' # --- diff --git a/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr b/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr index 7b89427a..103c3889 100644 --- a/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr +++ b/converters/dbt/tests/__snapshots__/test_ossie_to_msi.ambr @@ -17,6 +17,7 @@ # under the License. # name: TestOssieToMSIRoundTrip.test_ossie_to_msi_to_ossie_preserves_structure ''' + version: 0.2.0.dev0 name: semantic_model datasets: - name: orders @@ -56,7 +57,6 @@ dialects: - dialect: ANSI_SQL expression: SUM(orders.amount) - version: 0.2.0.dev0 ''' # --- diff --git a/converters/dbt/tests/test_msi_to_ossie.py b/converters/dbt/tests/test_msi_to_ossie.py index fe9742ad..d5f9f38b 100644 --- a/converters/dbt/tests/test_msi_to_ossie.py +++ b/converters/dbt/tests/test_msi_to_ossie.py @@ -1412,6 +1412,7 @@ def test_to_ossie_json_produces_valid_json(self) -> None: parsed = json.loads(result.to_ossie_json()) assert parsed["version"] == "0.2.0.dev0" + assert next(iter(parsed)) == "version" assert "semantic_model" not in parsed assert parsed["name"] == "my_project" diff --git a/converters/orionbelt/src/ossie_orionbelt/validation.py b/converters/orionbelt/src/ossie_orionbelt/validation.py index b4dbe6f1..65d5af61 100644 --- a/converters/orionbelt/src/ossie_orionbelt/validation.py +++ b/converters/orionbelt/src/ossie_orionbelt/validation.py @@ -207,13 +207,19 @@ def validate_ossie(ossie_dict: dict[str, Any], schema_path: Path | None = None) _validate_json_schema(ossie_dict, schema_path or _OSSIE_SCHEMA_PATH, result, draft="draft2020") # The semantic checks below assume a well-formed structure (lists of dicts). - # JSON Schema validation above already reports structural errors, so guard - # every level here rather than raising on malformed input. + # Guard every level rather than raising on malformed input, even when + # JSON Schema validation is unavailable. def _as_dict_list(value: Any) -> list[dict[str, Any]]: return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else [] - # Legacy wrappers are schema errors, not model contents to traverse. - if not isinstance(ossie_dict, dict) or "semantic_model" in ossie_dict: + if not isinstance(ossie_dict, dict): + result.semantic_errors.append("[INVALID_DOCUMENT] Ossie document must be an object") + return result + if "semantic_model" in ossie_dict: + result.semantic_errors.append( + "[LEGACY_WRAPPER] Legacy 'semantic_model' wrappers are not supported; " + "place model properties at the document root" + ) return result model = ossie_dict diff --git a/converters/orionbelt/tests/test_ossie_validation.py b/converters/orionbelt/tests/test_ossie_validation.py new file mode 100644 index 00000000..60c2d5d6 --- /dev/null +++ b/converters/orionbelt/tests/test_ossie_validation.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import sys + +import pytest + +from ossie_orionbelt.validation import _OSSIE_SCHEMA_PATH, validate_ossie + + +@pytest.fixture(params=["available", "missing_file", "missing_package"]) +def schema_path(request, tmp_path, monkeypatch): + if request.param == "missing_file": + return tmp_path / "missing-schema.json" + if request.param == "missing_package": + monkeypatch.setitem(sys.modules, "jsonschema", None) + return _OSSIE_SCHEMA_PATH + + +@pytest.mark.parametrize( + "wrapper", + [None, [], {}, [{"name": "legacy", "datasets": []}]], +) +@pytest.mark.parametrize("include_root_model", [False, True]) +def test_legacy_wrapper_is_always_invalid(schema_path, wrapper, include_root_model): + document = {"version": "0.2.0.dev0", "semantic_model": wrapper} + if include_root_model: + document.update(name="m", datasets=[{"name": "t", "source": "a.b.c"}]) + + result = validate_ossie(document, schema_path=schema_path) + + assert not result.valid + assert any("[LEGACY_WRAPPER]" in error for error in result.semantic_errors) + + +@pytest.mark.parametrize("document", [None, [], "not a mapping", 42]) +def test_non_mapping_document_is_always_invalid(schema_path, document): + result = validate_ossie(document, schema_path=schema_path) + + assert not result.valid + assert any("[INVALID_DOCUMENT]" in error for error in result.semantic_errors) + + +def test_valid_flat_document_can_be_checked_without_schema(schema_path): + document = { + "version": "0.2.0.dev0", + "name": "m", + "datasets": [{"name": "t", "source": "a.b.c"}], + } + + result = validate_ossie(document, schema_path=schema_path) + + assert result.valid diff --git a/converters/sigma/tests/test_roundtrip.py b/converters/sigma/tests/test_roundtrip.py index 4e5a0905..8f2361cc 100644 --- a/converters/sigma/tests/test_roundtrip.py +++ b/converters/sigma/tests/test_roundtrip.py @@ -39,6 +39,7 @@ def test_sigma_osi_sigma_roundtrip_through_yaml_serialization(fixture_name): yaml_text = document.to_ossie_yaml() serialized = yaml.safe_load(yaml_text) + assert next(iter(serialized)) == "version" assert serialized["name"] == spec["name"] assert "semantic_model" not in serialized assert "dialects" not in serialized diff --git a/converters/wisdom/tests/test_wisdom_to_ossie.py b/converters/wisdom/tests/test_wisdom_to_ossie.py index e08c4d9f..9526689a 100644 --- a/converters/wisdom/tests/test_wisdom_to_ossie.py +++ b/converters/wisdom/tests/test_wisdom_to_ossie.py @@ -158,6 +158,7 @@ def test_stale_measure_is_kept_with_warning(result, model): def test_output_round_trips_through_ossie_yaml(result): serialized = yaml.safe_load(result.output.to_ossie_yaml()) + assert next(iter(serialized)) == "version" assert serialized["name"] == "Sample Sales" assert "semantic_model" not in serialized document = OssieDocument.model_validate(serialized) diff --git a/python/README.md b/python/README.md index 88a3b7f4..bb0ea7ba 100644 --- a/python/README.md +++ b/python/README.md @@ -25,7 +25,8 @@ Each `OssieDocument` is one semantic model: `name`, `datasets`, `relationships`, and `metrics` sit at the root alongside `version`. Construct documents with `OssieDocument(name="sales", datasets=[...])` and access their datasets as `document.datasets`. JSON and YAML serialization use the same -flat shape. The former `semantic_model` wrapper is rejected. Unwrap old +flat shape, with `version` serialized first when included. The former +`semantic_model` wrapper is rejected. Unwrap old single-model documents and split multi-model documents into separate files, preserving model contents and setting `version` in each file before loading them. diff --git a/python/src/ossie/models.py b/python/src/ossie/models.py index a31b8b1a..43dbab86 100644 --- a/python/src/ossie/models.py +++ b/python/src/ossie/models.py @@ -19,7 +19,7 @@ from typing import Any, Optional, Union import yaml -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, SerializerFunctionWrapHandler, model_serializer class OssieDialect(str, Enum): @@ -215,6 +215,14 @@ class OssieDocument(OssieSemanticModel): version: str = "0.2.0.dev0" + @model_serializer(mode="wrap") + def _serialize_document(self, handler: SerializerFunctionWrapHandler): + # Omit a custom return type to retain Pydantic's serialization schema. + data = handler(self) + if "version" in data: + return {"version": data.pop("version"), **data} + return data + def to_ossie_yaml(self, **kwargs: Any) -> str: """Serialize to Ossie-compliant YAML (uses field aliases and excludes None values).""" data = self.model_dump(by_alias=True, exclude_none=True, mode="json", **kwargs) diff --git a/python/tests/test_models.py b/python/tests/test_models.py index f809a00e..583bcd77 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -146,6 +146,55 @@ def test_document_serialization_preserves_flat_model_and_metadata( assert OssieDocument.model_validate(serialized) == document +@pytest.mark.parametrize( + "serialize", ["model_dump", "model_dump_json", "to_ossie_yaml", "to_ossie_json"] +) +def test_document_serialization_puts_version_first(serialize: str) -> None: + data = _document() + document = OssieDocument.model_validate(data) + + serialized = getattr(document, serialize)() + if isinstance(serialized, str): + serialized = yaml.safe_load(serialized) + + assert next(iter(serialized)) == "version" + assert serialized["version"] == data["version"] + + +@pytest.mark.parametrize( + "serialize", ["model_dump", "model_dump_json", "to_ossie_yaml", "to_ossie_json"] +) +@pytest.mark.parametrize( + "options", + [ + {"exclude": {"version"}}, + {"include": {"name", "datasets"}}, + {"exclude_defaults": True}, + {"exclude_unset": True}, + ], +) +def test_document_serialization_can_omit_version(serialize: str, options: dict) -> None: + data = _document() + del data["version"] + document = OssieDocument.model_validate(data) + + serialized = getattr(document, serialize)(**options) + if isinstance(serialized, str): + serialized = yaml.safe_load(serialized) + + assert "version" not in serialized + assert serialized["name"] == data["name"] + + +def test_document_serialization_schema_preserves_model_fields() -> None: + schema = OssieDocument.model_json_schema(mode="serialization") + + assert schema["properties"]["version"]["type"] == "string" + assert schema["properties"]["datasets"]["type"] == "array" + assert set(schema["required"]) == {"name", "datasets"} + assert schema["additionalProperties"] is False + + @pytest.mark.parametrize( "legacy_value", [ From 2f7e99c4157286f973b3cff53e700fbe13819fdb Mon Sep 17 00:00:00 2001 From: Yufei Date: Fri, 18 Sep 2026 15:12:52 -0700 Subject: [PATCH 2/4] Make serialization schema test compatible with Pydantic 2.0 --- python/tests/test_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 583bcd77..e37d88b7 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -191,7 +191,8 @@ def test_document_serialization_schema_preserves_model_fields() -> None: assert schema["properties"]["version"]["type"] == "string" assert schema["properties"]["datasets"]["type"] == "array" - assert set(schema["required"]) == {"name", "datasets"} + # Early Pydantic 2.x versions also require defaulted fields in serialization schemas. + assert {"name", "datasets"} <= set(schema["required"]) assert schema["additionalProperties"] is False From 74096cb9e7cfa35370a1eab99601cd472b7f1507 Mon Sep 17 00:00:00 2001 From: Yufei Date: Mon, 21 Sep 2026 10:21:21 -0700 Subject: [PATCH 3/4] Clarify reduced validation coverage when JSON Schema is skipped --- converters/orionbelt/README.md | 16 ++- .../orionbelt/src/ossie_orionbelt/cli.py | 12 ++- .../src/ossie_orionbelt/validation.py | 17 +++- .../tests/test_ossie_converter_ontology.py | 2 + .../orionbelt/tests/test_ossie_validation.py | 97 ++++++++++++++++++- 5 files changed, 136 insertions(+), 8 deletions(-) diff --git a/converters/orionbelt/README.md b/converters/orionbelt/README.md index daefed37..a038ce85 100644 --- a/converters/orionbelt/README.md +++ b/converters/orionbelt/README.md @@ -87,7 +87,7 @@ from ossie_orionbelt import OBMLtoOssie, OssietoOBML, validate_ossie obml = yaml.safe_load(open("model.obml.yaml")) ossie = OBMLtoOssie(obml, "sales", "Sales model").convert() result = validate_ossie(ossie) -assert result.valid +assert result.schema_validation_performed and result.valid obml_again = OssietoOBML(ossie).convert() ``` @@ -111,6 +111,20 @@ of the Ossie metrics with a warning and kept whole in the model-level extension definition, and every field its OBML column name, so Ossie to OBML restores the original model instead of re-parsing the SQL. +`validate_ossie` checks JSON Schema conformance, unique names, and relationship +references. If the schema file or `jsonschema` package is unavailable, it emits +a warning and reports `JSON Schema: skipped`. The result's +`schema_validation_performed` flag is `False` in that case. + +This fallback provides partial validation: it rejects non-object roots and +legacy `semantic_model` wrappers, and checks unique names and references in +entries it can traverse. Required fields and field types are not checked, so +documents missing `name` or `datasets`, with malformed `datasets`, or with a +dataset missing `source` can still return `valid=True`. That value means only +that the checks performed found no errors. Require both +`schema_validation_performed` and `valid`, as above, when structural validity +is required. + ## Vendor extensions Ossie `custom_extensions` carry vendor-tagged payloads. This converter: diff --git a/converters/orionbelt/src/ossie_orionbelt/cli.py b/converters/orionbelt/src/ossie_orionbelt/cli.py index 73283218..7646e721 100644 --- a/converters/orionbelt/src/ossie_orionbelt/cli.py +++ b/converters/orionbelt/src/ossie_orionbelt/cli.py @@ -78,7 +78,13 @@ def _report_validation(label: str, result: dict[str, Any], validate_fn: Any) -> for line in vr.summary_lines(): print(line, file=sys.stderr) if vr.valid: - print(f"{label} is valid", file=sys.stderr) + if vr.schema_validation_performed: + print(f"{label} is valid", file=sys.stderr) + else: + print( + f"{label} passed available checks (JSON Schema validation skipped)", + file=sys.stderr, + ) return False print(f"{label} has validation errors", file=sys.stderr) return True @@ -136,7 +142,9 @@ def main(argv: list[str] | None = None) -> int: "-o", "--output", required=True, metavar="FILE", help="Path for output Ossie YAML" ) o2s.add_argument( - "--ontology", action="store_true", help="Emit an Ossie ontology document instead of core-spec" + "--ontology", + action="store_true", + help="Emit an Ossie ontology document instead of core-spec", ) o2s.add_argument( "--model-name", default="semantic_model", metavar="NAME", help="Ossie semantic model name" diff --git a/converters/orionbelt/src/ossie_orionbelt/validation.py b/converters/orionbelt/src/ossie_orionbelt/validation.py index 65d5af61..cfce67c0 100644 --- a/converters/orionbelt/src/ossie_orionbelt/validation.py +++ b/converters/orionbelt/src/ossie_orionbelt/validation.py @@ -70,12 +70,14 @@ class ValidationResult: def __init__(self, format_name: str = "OBML") -> None: self.format_name = format_name + self.schema_validation_performed = False self.schema_errors: list[str] = [] self.semantic_errors: list[str] = [] self.semantic_warnings: list[str] = [] @property def valid(self) -> bool: + """Whether the checks that ran found no errors; skipped checks may leave gaps.""" return not self.schema_errors and not self.semantic_errors def summary_lines(self) -> list[str]: @@ -84,8 +86,10 @@ def summary_lines(self) -> list[str]: lines.append(f" JSON Schema: {len(self.schema_errors)} error(s)") for e in self.schema_errors: lines.append(f" - {e}") - else: + elif self.schema_validation_performed: lines.append(" JSON Schema: ✓ valid") + else: + lines.append(" JSON Schema: skipped") if self.semantic_errors: lines.append(f" Semantic: {len(self.semantic_errors)} error(s)") for e in self.semantic_errors: @@ -135,6 +139,7 @@ def _validate_json_schema( for error in sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path)): path = ".".join(str(p) for p in error.absolute_path) or "(root)" result.schema_errors.append(f"[{path}] {error.message}") + result.schema_validation_performed = True # ── OBML Validation ────────────────────────────────────────────────────── @@ -200,6 +205,16 @@ def validate_ossie(ossie_dict: dict[str, Any], schema_path: Path | None = None) (Draft 2020-12) 2. **Unique names** — datasets, fields, metrics, relationships 3. **References** — relationship from/to reference existing datasets + + If the schema file or ``jsonschema`` package is unavailable, schema validation + is skipped with a warning and ``schema_validation_performed`` is False. + This fallback has reduced coverage: it rejects non-object roots and legacy + ``semantic_model`` wrappers, and checks names and references in the entries + it can traverse. It does not check required fields or field types; missing + ``name`` or ``datasets``, malformed ``datasets``, and missing dataset ``source`` + can pass. ``valid=True`` only means the checks that ran found no errors. + Require both ``schema_validation_performed`` and ``valid`` to establish + structural validity as well as passing the semantic checks. """ result = ValidationResult("Ossie") diff --git a/converters/orionbelt/tests/test_ossie_converter_ontology.py b/converters/orionbelt/tests/test_ossie_converter_ontology.py index 93068895..2ed29523 100644 --- a/converters/orionbelt/tests/test_ossie_converter_ontology.py +++ b/converters/orionbelt/tests/test_ossie_converter_ontology.py @@ -132,6 +132,8 @@ def test_passes_ontology_semantic_validation(self) -> None: result = conv.validate_ossie_ontology(doc) assert result.valid, result.semantic_errors assert not result.semantic_errors + assert not result.schema_validation_performed + assert result.summary_lines()[0] == " JSON Schema: skipped" def test_one_to_one_multiplicity(self) -> None: obml = { diff --git a/converters/orionbelt/tests/test_ossie_validation.py b/converters/orionbelt/tests/test_ossie_validation.py index 60c2d5d6..802a2e6a 100644 --- a/converters/orionbelt/tests/test_ossie_validation.py +++ b/converters/orionbelt/tests/test_ossie_validation.py @@ -19,14 +19,20 @@ import pytest +from ossie_orionbelt.cli import _report_validation from ossie_orionbelt.validation import _OSSIE_SCHEMA_PATH, validate_ossie @pytest.fixture(params=["available", "missing_file", "missing_package"]) -def schema_path(request, tmp_path, monkeypatch): - if request.param == "missing_file": +def schema_mode(request): + return request.param + + +@pytest.fixture +def schema_path(schema_mode, tmp_path, monkeypatch): + if schema_mode == "missing_file": return tmp_path / "missing-schema.json" - if request.param == "missing_package": + if schema_mode == "missing_package": monkeypatch.setitem(sys.modules, "jsonschema", None) return _OSSIE_SCHEMA_PATH @@ -55,7 +61,7 @@ def test_non_mapping_document_is_always_invalid(schema_path, document): assert any("[INVALID_DOCUMENT]" in error for error in result.semantic_errors) -def test_valid_flat_document_can_be_checked_without_schema(schema_path): +def test_valid_flat_document_can_be_checked_without_schema(schema_path, schema_mode): document = { "version": "0.2.0.dev0", "name": "m", @@ -65,3 +71,86 @@ def test_valid_flat_document_can_be_checked_without_schema(schema_path): result = validate_ossie(document, schema_path=schema_path) assert result.valid + assert result.schema_validation_performed == (schema_mode == "available") + expected_status = "✓ valid" if schema_mode == "available" else "skipped" + assert result.summary_lines()[0] == f" JSON Schema: {expected_status}" + if schema_mode != "available": + assert result.semantic_warnings + + +@pytest.mark.parametrize( + "model", + [ + {"name": "m", "datasets": "not a list"}, + {"name": "m"}, + {"datasets": [{"name": "t", "source": "a.b.c"}]}, + {"name": "m", "datasets": [{"name": "t"}]}, + {"name": "m", "datasets": ["not an object"]}, + ], + ids=["datasets_type", "datasets_missing", "name_missing", "source_missing", "dataset_type"], +) +def test_structural_validation_coverage_is_reported(schema_path, schema_mode, model): + result = validate_ossie({"version": "0.2.0.dev0", **model}, schema_path=schema_path) + + assert not result.semantic_errors + if schema_mode == "available": + assert result.schema_validation_performed + assert not result.valid + assert result.schema_errors + assert result.summary_lines()[0] == f" JSON Schema: {len(result.schema_errors)} error(s)" + else: + assert not result.schema_validation_performed + assert result.valid + assert not result.schema_errors + assert result.semantic_warnings + assert result.summary_lines()[0] == " JSON Schema: skipped" + + +@pytest.mark.parametrize("error_code", ["DUPLICATE_DATASET", "UNKNOWN_DATASET_REF"]) +def test_semantic_checks_still_run_without_schema(schema_path, error_code): + document = { + "version": "0.2.0.dev0", + "name": "m", + "datasets": [{"name": "t", "source": "a.b.c"}], + } + if error_code == "DUPLICATE_DATASET": + document["datasets"].append({"name": "t", "source": "a.b.d"}) + else: + document["relationships"] = [ + { + "name": "r", + "from": "t", + "to": "missing", + "from_columns": ["id"], + "to_columns": ["id"], + } + ] + + result = validate_ossie(document, schema_path=schema_path) + + assert not result.valid + assert any(f"[{error_code}]" in error for error in result.semantic_errors) + + +@pytest.mark.parametrize("has_name", [True, False]) +def test_cli_reports_validation_coverage(schema_path, schema_mode, has_name, capsys): + document = {"version": "0.2.0.dev0", "datasets": [{"name": "t", "source": "a.b.c"}]} + if has_name: + document["name"] = "m" + + has_errors = _report_validation( + "Ossie output", document, lambda doc: validate_ossie(doc, schema_path=schema_path) + ) + stderr = capsys.readouterr().err + + if schema_mode != "available": + assert not has_errors + assert "JSON Schema: skipped" in stderr + assert "passed available checks (JSON Schema validation skipped)" in stderr + assert "Ossie output is valid" not in stderr + elif has_name: + assert not has_errors + assert "Ossie output is valid" in stderr + else: + assert has_errors + assert "Ossie output has validation errors" in stderr From 242f435a6252d87e8b8252fa3563024bb80a9c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Thu, 1 Oct 2026 16:39:45 +0200 Subject: [PATCH 4/4] Use document_data fixture in version serialization tests --- python/tests/test_models.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/python/tests/test_models.py b/python/tests/test_models.py index e37d88b7..80b55098 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -149,8 +149,10 @@ def test_document_serialization_preserves_flat_model_and_metadata( @pytest.mark.parametrize( "serialize", ["model_dump", "model_dump_json", "to_ossie_yaml", "to_ossie_json"] ) -def test_document_serialization_puts_version_first(serialize: str) -> None: - data = _document() +def test_document_serialization_puts_version_first( + serialize: str, document_data: dict +) -> None: + data = document_data document = OssieDocument.model_validate(data) serialized = getattr(document, serialize)() @@ -173,8 +175,10 @@ def test_document_serialization_puts_version_first(serialize: str) -> None: {"exclude_unset": True}, ], ) -def test_document_serialization_can_omit_version(serialize: str, options: dict) -> None: - data = _document() +def test_document_serialization_can_omit_version( + serialize: str, options: dict, document_data: dict +) -> None: + data = document_data del data["version"] document = OssieDocument.model_validate(data)