Coverage for tests/test_schema_fixture_machinery.py: 99%
463 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 09:13 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-10 09:13 +0000
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14import json
15from pathlib import Path
16from typing import Any, ClassVar
18import pydantic
19import pytest
20from packaging.version import Version
22# Importing this registers the purpose-built projection_test and retire_test
23# schemas whose fixtures live under fixture_doubles/. It is a plain module
24# rather than a test module, so registration does not depend on pytest's
25# collection order.
26from schema_doubles import ProjectionTestModel, RetireTestModel # noqa: F401
28import lsst.images.tests._schema_fixtures as schema_fixture_module
29from lsst.images.serialization import ArchiveReadError, ArchiveTree, InputArchive
30from lsst.images.tests import (
31 SchemaFixtureError,
32 canonical_fixture_text,
33 check_schema_fixtures,
34 compare_fixture_versions,
35 current_fixture_path,
36 fixture_version,
37 freeze_schema_fixtures,
38 iter_schema_fixtures,
39 read_fixture_tree,
40 refresh_schema_fixtures,
41)
42from lsst.images.tests._schema_fixtures import (
43 _AS_SHIPPED_VARIANT,
44 _CANONICAL_VARIANT,
45 _fixture_dir_path,
46 _fixture_filename,
47)
50class _FrozenDouble(ArchiveTree):
51 """Finalized-version double for the fixture machinery tests."""
53 SCHEMA_NAME: ClassVar[str] = "fixture_machinery_frozen"
54 SCHEMA_VERSION: ClassVar[str] = "1.2.3"
55 MIN_READ_VERSION: ClassVar[int] = 1
56 PUBLIC_TYPE: ClassVar[type] = dict
58 label: str = pydantic.Field(default="", description="A value to round-trip.")
60 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any:
61 return {"label": self.label}
64class _DevDouble(ArchiveTree):
65 """Development-version double for the fixture machinery tests."""
67 SCHEMA_NAME: ClassVar[str] = "fixture_machinery_dev"
68 SCHEMA_VERSION: ClassVar[str] = "2.0.0.dev3"
69 MIN_READ_VERSION: ClassVar[int] = 1
70 PUBLIC_TYPE: ClassVar[type] = dict
72 label: str = pydantic.Field(default="", description="A value to round-trip.")
74 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any:
75 return {"label": self.label}
78class _StrictDouble(ArchiveTree):
79 """Double with a required field, used to force a genuine pydantic
80 validation failure distinct from a schema-compatibility rejection.
81 """
83 SCHEMA_NAME: ClassVar[str] = "fixture_machinery_strict"
84 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
85 MIN_READ_VERSION: ClassVar[int] = 1
86 PUBLIC_TYPE: ClassVar[type] = dict
88 required: str = pydantic.Field(description="A value with no default.")
90 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any:
91 return {"required": self.required}
94# All three test doubles above share this module, so `available_schema_classes`
95# filtering by `package=<double>.__module__` always sees all three regardless
96# of which double a test is nominally about; tests that assert an exact
97# problem list rather than membership must exempt the siblings they are not
98# exercising.
99_SIBLING_SCHEMAS = ("fixture_machinery_dev", "fixture_machinery_strict")
102def test_fixture_version_strips_the_dev_counter() -> None:
103 """Verify a filename version keeps the release part and a bare .dev."""
104 assert fixture_version("1.2.3") == "1.2.3"
105 assert fixture_version("2.0.0.dev0") == "2.0.0.dev"
106 assert fixture_version("2.0.0.dev3") == "2.0.0.dev"
109def test_fixture_filename_and_path() -> None:
110 """Verify names and paths follow {name}/{name}-{version}[-{variant}]."""
111 assert _fixture_filename("image", "1.0.0") == "image-1.0.0.json"
112 assert _fixture_filename("image", "1.0.0", "dp1") == "image-1.0.0-dp1.json"
113 root = Path("/fixtures")
114 assert _fixture_dir_path(root, "image", "1.0.0") == root / "image" / "image-1.0.0.json"
115 assert _fixture_dir_path(root, "image", "1.0.0", "dp1") == root / "image" / "image-1.0.0-dp1.json"
118def test_current_fixture_path_resolves_by_schema_name() -> None:
119 """Verify a consumer can find a fixture without naming a version.
121 Covers both a finalized schema and a development one, since the live
122 version is what the filename is built from.
123 """
124 root = Path("/fixtures")
125 assert current_fixture_path(root, _FrozenDouble.SCHEMA_NAME) == (
126 root / "fixture_machinery_frozen" / "fixture_machinery_frozen-1.2.3.json"
127 )
128 assert current_fixture_path(root, _DevDouble.SCHEMA_NAME, variant="dp1") == (
129 root / "fixture_machinery_dev" / "fixture_machinery_dev-2.0.0.dev-dp1.json"
130 )
131 with pytest.raises(LookupError, match="no_such_schema"):
132 current_fixture_path(root, "no_such_schema")
135def test_canonical_fixture_text_is_indented_and_newline_terminated() -> None:
136 """Verify the canonical form is indent=2 with a trailing newline."""
137 text = canonical_fixture_text(_FrozenDouble(label="x"))
138 assert text.startswith('{\n "schema_version"')
139 assert text.endswith("}\n")
140 assert json.loads(text)["label"] == "x"
143def test_canonical_fixture_text_is_idempotent() -> None:
144 """Verify re-reading canonical text reproduces it byte for byte."""
145 first = canonical_fixture_text(_FrozenDouble(label="x"))
146 tree = _FrozenDouble.model_validate_json(first)
147 assert canonical_fixture_text(tree) == first
150def test_iter_schema_fixtures_parses_the_layout(tmp_path: Path) -> None:
151 """Verify the scan reports version, variant, retired flag and class."""
152 directory = tmp_path / "fixture_machinery_frozen"
153 (directory / "retired").mkdir(parents=True)
154 (directory / "fixture_machinery_frozen-1.2.3.json").write_text(
155 canonical_fixture_text(_FrozenDouble(label="current"))
156 )
157 (directory / "fixture_machinery_frozen-1.2.3-as_shipped.json").write_text(
158 canonical_fixture_text(_FrozenDouble(label="shipped"))
159 )
160 (directory / "fixture_machinery_frozen-1.2.3-canonical.json").write_text(
161 canonical_fixture_text(_FrozenDouble(label="shipped"))
162 )
163 (directory / "retired" / "fixture_machinery_frozen-1.0.0.json").write_text("{}\n")
164 found = {(f.version, f.variant, f.retired) for f in iter_schema_fixtures(tmp_path)}
165 assert found == {
166 ("1.2.3", None, False),
167 ("1.2.3", _AS_SHIPPED_VARIANT, False),
168 ("1.2.3", _CANONICAL_VARIANT, False),
169 ("1.0.0", None, True),
170 }
171 by_variant = {f.variant: f for f in iter_schema_fixtures(tmp_path)}
172 assert by_variant[_AS_SHIPPED_VARIANT].is_as_shipped
173 assert not by_variant[_AS_SHIPPED_VARIANT].is_canonical_twin
174 assert by_variant[_CANONICAL_VARIANT].is_canonical_twin is True
175 assert by_variant[_CANONICAL_VARIANT].is_as_shipped is False
176 assert by_variant[None].tree_cls is _FrozenDouble
179def test_iter_schema_fixtures_reports_an_unknown_schema(tmp_path: Path) -> None:
180 """Verify a directory with no matching schema yields tree_cls None."""
181 directory = tmp_path / "not_a_schema"
182 directory.mkdir()
183 (directory / "not_a_schema-1.0.0.json").write_text("{}\n")
184 (fixture,) = list(iter_schema_fixtures(tmp_path))
185 assert fixture.tree_cls is None
186 assert fixture.name == "not_a_schema"
189def test_iter_schema_fixtures_skips_non_fixture_files(tmp_path: Path) -> None:
190 """Verify a README and a mis-named file are not reported as fixtures."""
191 directory = tmp_path / "fixture_machinery_frozen"
192 directory.mkdir()
193 (tmp_path / "README.md").write_text("notes\n")
194 (directory / "fixture_machinery_frozen-1.2.3.json").write_text(canonical_fixture_text(_FrozenDouble()))
195 (directory / "wrong-name.json").write_text("{}\n")
196 assert [f.variant for f in iter_schema_fixtures(tmp_path)] == [None]
199def test_read_fixture_tree(tmp_path: Path) -> None:
200 """Verify a fixture is validated through its live model."""
201 directory = tmp_path / "fixture_machinery_frozen"
202 directory.mkdir()
203 path = directory / "fixture_machinery_frozen-1.2.3.json"
204 path.write_text(canonical_fixture_text(_FrozenDouble(label="read me")))
205 (fixture,) = list(iter_schema_fixtures(tmp_path))
206 tree = read_fixture_tree(fixture)
207 assert isinstance(tree, ArchiveTree)
208 assert tree.label == "read me" # type: ignore[attr-defined]
211def _write(directory: Path, name: str, text: str) -> Path:
212 directory.mkdir(parents=True, exist_ok=True)
213 path = directory / name
214 path.write_text(text)
215 return path
218def test_check_accepts_a_sound_tree(tmp_path: Path) -> None:
219 """Verify a canonical fixture at the live version reports no problems."""
220 _write(
221 tmp_path / "fixture_machinery_frozen",
222 "fixture_machinery_frozen-1.2.3.json",
223 canonical_fixture_text(_FrozenDouble(label="ok")),
224 )
225 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS)
226 assert problems == []
229def test_check_reports_drift(tmp_path: Path) -> None:
230 """Verify a fixture that does not round-trip is reported."""
231 text = canonical_fixture_text(_FrozenDouble(label="ok"))
232 stale = json.loads(text)
233 del stale["label"]
234 _write(
235 tmp_path / "fixture_machinery_frozen",
236 "fixture_machinery_frozen-1.2.3.json",
237 json.dumps(stale, indent=2) + "\n",
238 )
239 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
240 assert any("not canonical" in p for p in problems)
243def test_check_wording_distinguishes_frozen_from_development(tmp_path: Path) -> None:
244 """Verify a frozen fixture says to bump, a dev fixture says to refresh."""
245 frozen = json.loads(canonical_fixture_text(_FrozenDouble(label="x")))
246 del frozen["label"]
247 _write(
248 tmp_path / "fixture_machinery_frozen",
249 "fixture_machinery_frozen-1.2.3.json",
250 json.dumps(frozen, indent=2) + "\n",
251 )
252 dev = json.loads(canonical_fixture_text(_DevDouble(label="x")))
253 del dev["label"]
254 _write(
255 tmp_path / "fixture_machinery_dev",
256 "fixture_machinery_dev-2.0.0.dev.json",
257 json.dumps(dev, indent=2) + "\n",
258 )
259 problems = "\n".join(check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__))
260 assert "bump SCHEMA_VERSION" in problems
261 assert "fixtures refresh" in problems
264def test_check_reports_a_stamp_that_disagrees_with_the_filename(tmp_path: Path) -> None:
265 """Verify a filename version that contradicts the stamp is reported."""
266 tree = json.loads(canonical_fixture_text(_FrozenDouble()))
267 _write(
268 tmp_path / "fixture_machinery_frozen",
269 "fixture_machinery_frozen-9.9.9.json",
270 json.dumps(tree, indent=2) + "\n",
271 )
272 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
273 assert any("filename version" in p for p in problems)
276def test_check_reports_a_malformed_schema_version_without_raising(tmp_path: Path) -> None:
277 """Verify a malformed-but-integer-major stamp is reported, not raised.
279 ``fixture_version`` parses the stamp with ``packaging.version.Version``,
280 which raises ``InvalidVersion`` for a plausible hand-edit like "1.2.O"
281 (letter O for zero); ``check_schema_fixtures`` promises never to raise,
282 so that must become a reported problem like any other, not an escaped
283 exception.
284 """
285 tree = json.loads(canonical_fixture_text(_FrozenDouble()))
286 tree["schema_version"] = "1.2.O"
287 _write(
288 tmp_path / "fixture_machinery_frozen",
289 "fixture_machinery_frozen-1.2.3.json",
290 json.dumps(tree, indent=2) + "\n",
291 )
292 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
293 assert any("1.2.O" in p and "not a valid version" in p for p in problems)
296def test_check_reports_a_missing_development_fixture(tmp_path: Path) -> None:
297 """Verify a development schema with no fixture at all is reported."""
298 (tmp_path / "fixture_machinery_dev").mkdir()
299 problems = check_schema_fixtures(tmp_path, package=_DevDouble.__module__)
300 assert any("fixture_machinery_dev-2.0.0.dev.json" in p and "missing" in p for p in problems)
303def test_check_reports_an_unknown_schema_directory(tmp_path: Path) -> None:
304 """Verify a directory that maps to no registered schema is reported."""
305 _write(tmp_path / "not_a_schema", "not_a_schema-1.0.0.json", "{}\n")
306 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
307 assert any("not_a_schema" in p and "not registered" in p for p in problems)
310def test_check_requires_a_retired_fixture_to_be_rejected(tmp_path: Path) -> None:
311 """Verify a retired fixture that still validates is reported."""
312 _write(
313 tmp_path / "fixture_machinery_frozen",
314 "fixture_machinery_frozen-1.2.3.json",
315 canonical_fixture_text(_FrozenDouble()),
316 )
317 _write(
318 tmp_path / "fixture_machinery_frozen" / "retired",
319 "fixture_machinery_frozen-1.0.0.json",
320 canonical_fixture_text(_FrozenDouble()),
321 )
322 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
323 assert any("retired" in p and "still validates" in p for p in problems)
326def test_check_applies_identity_checks_to_a_retired_fixture(tmp_path: Path) -> None:
327 """Verify expected rejection does not hide a retired fixture's identity."""
328 directory = tmp_path / "fixture_machinery_frozen"
329 _write(
330 directory,
331 "fixture_machinery_frozen-1.2.3.json",
332 canonical_fixture_text(_FrozenDouble()),
333 )
334 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
335 retired["schema_version"] = "1.0.0"
336 retired["schema_url"] = "https://example.org/wrong"
337 retired["min_read_version"] = 999
338 _write(
339 directory / "retired",
340 "fixture_machinery_frozen-1.0.0.json",
341 json.dumps(retired, indent=2) + "\n",
342 )
343 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS)
344 assert any("schema_url" in problem and "expected" in problem for problem in problems)
347def test_check_reports_malformed_retired_fixture_json(tmp_path: Path) -> None:
348 """Verify invalid JSON is a defect, not successful retirement."""
349 directory = tmp_path / "fixture_machinery_frozen"
350 _write(
351 directory,
352 "fixture_machinery_frozen-1.2.3.json",
353 canonical_fixture_text(_FrozenDouble()),
354 )
355 _write(directory / "retired", "fixture_machinery_frozen-1.0.0.json", "{\n")
356 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS)
357 assert any("is not valid JSON" in problem for problem in problems)
360def test_check_reports_an_unexpected_retired_read_exception(
361 tmp_path: Path, monkeypatch: pytest.MonkeyPatch
362) -> None:
363 """Verify a programming failure cannot masquerade as retirement."""
364 directory = tmp_path / "fixture_machinery_frozen"
365 _write(
366 directory,
367 "fixture_machinery_frozen-1.2.3.json",
368 canonical_fixture_text(_FrozenDouble()),
369 )
370 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
371 retired["schema_version"] = "1.0.0"
372 retired["schema_url"] = "https://images.lsst.io/schemas/fixture_machinery_frozen-1.0.0"
373 retired["min_read_version"] = 999
374 _write(
375 directory / "retired",
376 "fixture_machinery_frozen-1.0.0.json",
377 json.dumps(retired, indent=2) + "\n",
378 )
379 original = schema_fixture_module.read_fixture_tree
381 def fail_retired(fixture: schema_fixture_module.SchemaFixture) -> ArchiveTree:
382 if fixture.retired:
383 raise RuntimeError("reader bug")
384 return original(fixture)
386 monkeypatch.setattr(schema_fixture_module, "read_fixture_tree", fail_retired)
387 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS)
388 assert any("unexpected RuntimeError: reader bug" in problem for problem in problems)
391def test_check_binds_the_as_shipped_pair(tmp_path: Path) -> None:
392 """Verify an as_shipped fixture needs a twin matching its canonical
393 read.
394 """
395 directory = tmp_path / "fixture_machinery_frozen"
396 _write(
397 directory,
398 "fixture_machinery_frozen-1.2.3.json",
399 canonical_fixture_text(_FrozenDouble()),
400 )
401 shipped = json.loads(canonical_fixture_text(_FrozenDouble(label="shipped")))
402 _write(
403 directory,
404 "fixture_machinery_frozen-1.2.3-as_shipped.json",
405 json.dumps(shipped, indent=2) + "\n",
406 )
407 exempt = _SIBLING_SCHEMAS
408 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=exempt)
409 assert any("canonical" in p and "missing" in p for p in problems)
411 _write(
412 directory,
413 "fixture_machinery_frozen-1.2.3-canonical.json",
414 canonical_fixture_text(_FrozenDouble(label="something else")),
415 )
416 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=exempt)
417 assert any("does not match" in p for p in problems)
419 _write(
420 directory,
421 "fixture_machinery_frozen-1.2.3-canonical.json",
422 canonical_fixture_text(_FrozenDouble(label="shipped")),
423 )
424 assert check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=exempt) == []
427def test_check_pairs_fixtures_with_frozen_documents(tmp_path: Path) -> None:
428 """Verify both pairing directions against a frozen schema directory."""
429 fixtures = tmp_path / "fixtures"
430 schemas = tmp_path / "schemas"
431 schemas.mkdir()
432 _write(
433 fixtures / "fixture_machinery_frozen",
434 "fixture_machinery_frozen-1.2.3.json",
435 canonical_fixture_text(_FrozenDouble()),
436 )
437 # No frozen document at all: the finalized schema is unpaired.
438 exempt = _SIBLING_SCHEMAS
439 problems = check_schema_fixtures(
440 fixtures, schema_directory=schemas, package=_FrozenDouble.__module__, exempt=exempt
441 )
442 assert any("has no frozen document" in p for p in problems)
444 _write(schemas / "fixture_machinery_frozen", "fixture_machinery_frozen-1.2.3.json", "{}\n")
445 assert (
446 check_schema_fixtures(
447 fixtures, schema_directory=schemas, package=_FrozenDouble.__module__, exempt=exempt
448 )
449 == []
450 )
453def test_check_honors_exemptions(tmp_path: Path) -> None:
454 """Verify an exempt schema is not reported as missing a fixture."""
455 problems = check_schema_fixtures(
456 tmp_path,
457 package=_DevDouble.__module__,
458 exempt=["fixture_machinery_dev", "fixture_machinery_frozen", "fixture_machinery_strict"],
459 )
460 assert problems == []
463def test_check_does_not_flag_a_retired_fixture_as_unfrozen(tmp_path: Path) -> None:
464 """Verify a retired fixture with no frozen document is not reported.
466 A retired fixture is checked only for being rejected; it must not also
467 be run through the fixture/frozen-document pairing check, which would
468 otherwise flag it as an interrupted freeze.
469 """
470 fixtures = tmp_path / "fixtures"
471 schemas = tmp_path / "schemas"
472 _write(
473 fixtures / "fixture_machinery_frozen",
474 "fixture_machinery_frozen-1.2.3.json",
475 canonical_fixture_text(_FrozenDouble()),
476 )
477 _write(schemas / "fixture_machinery_frozen", "fixture_machinery_frozen-1.2.3.json", "{}\n")
478 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
479 retired["schema_version"] = "1.0.0"
480 retired["schema_url"] = "https://images.lsst.io/schemas/fixture_machinery_frozen-1.0.0"
481 retired["min_read_version"] = 999
482 _write(
483 fixtures / "fixture_machinery_frozen" / "retired",
484 "fixture_machinery_frozen-1.0.0.json",
485 json.dumps(retired, indent=2) + "\n",
486 )
487 problems = check_schema_fixtures(
488 fixtures, schema_directory=schemas, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS
489 )
490 assert problems == []
493def test_check_does_not_canonicalize_a_retired_as_shipped_fixture(tmp_path: Path) -> None:
494 """Verify a retired as_shipped fixture is not run through
495 canonicalization.
497 A retired fixture no longer validates by design, so passing it to
498 `_check_as_shipped_pairs` would always fail to canonicalize; that
499 pairing check must skip retired fixtures just as the single-fixture
500 check does.
501 """
502 directory = tmp_path / "fixture_machinery_frozen"
503 _write(
504 directory,
505 "fixture_machinery_frozen-1.2.3.json",
506 canonical_fixture_text(_FrozenDouble()),
507 )
508 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
509 retired["schema_version"] = "1.0.0"
510 retired["schema_url"] = "https://images.lsst.io/schemas/fixture_machinery_frozen-1.0.0"
511 retired["min_read_version"] = 999
512 # Both halves of the retired pair, so the bug being guarded against
513 # (reaching read_fixture_tree(shipped) instead of short-circuiting on a
514 # missing twin) is exercised precisely.
515 _write(
516 directory / "retired",
517 "fixture_machinery_frozen-1.0.0-as_shipped.json",
518 json.dumps(retired, indent=2) + "\n",
519 )
520 _write(
521 directory / "retired",
522 "fixture_machinery_frozen-1.0.0-canonical.json",
523 json.dumps(retired, indent=2) + "\n",
524 )
525 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS)
526 assert problems == []
529def test_check_reports_a_non_retired_as_shipped_fixture_that_fails_to_validate(
530 tmp_path: Path,
531) -> None:
532 """Verify a non-retired as_shipped fixture that fails to validate is
533 reported rather than raising.
535 `_check_as_shipped_pairs` reads each as_shipped fixture through its live
536 model to canonicalize it for comparison against its twin; unlike a
537 retired fixture, this one is expected to validate, so a failure here is
538 a genuine defect that must be reported, not an exception that escapes
539 `check_schema_fixtures`.
540 """
541 directory = tmp_path / "fixture_machinery_strict"
542 tree = json.loads(canonical_fixture_text(_StrictDouble(required="x")))
543 del tree["required"]
544 _write(
545 directory,
546 "fixture_machinery_strict-1.0.0-as_shipped.json",
547 json.dumps(tree, indent=2) + "\n",
548 )
549 _write(
550 directory,
551 "fixture_machinery_strict-1.0.0-canonical.json",
552 canonical_fixture_text(_StrictDouble(required="x")),
553 )
554 problems = check_schema_fixtures(tmp_path, package=_StrictDouble.__module__)
555 assert any("cannot be canonicalized" in p for p in problems)
558def test_check_reports_a_validation_failure_without_raising(tmp_path: Path) -> None:
559 """Verify a fixture that fails pydantic validation is reported as a
560 problem instead of propagating the exception; check_schema_fixtures
561 must never raise.
562 """
563 tree = json.loads(canonical_fixture_text(_StrictDouble(required="x")))
564 del tree["required"]
565 _write(
566 tmp_path / "fixture_machinery_strict",
567 "fixture_machinery_strict-1.0.0.json",
568 json.dumps(tree, indent=2) + "\n",
569 )
570 problems = check_schema_fixtures(tmp_path, package=_StrictDouble.__module__)
571 assert any("does not validate" in p for p in problems)
574def test_check_reports_a_read_incompatibility_without_raising(tmp_path: Path) -> None:
575 """Verify a fixture whose min_read_version exceeds the reader major is
576 reported as a problem instead of propagating the exception.
577 """
578 tree = json.loads(canonical_fixture_text(_FrozenDouble()))
579 tree["min_read_version"] = 999
580 _write(
581 tmp_path / "fixture_machinery_frozen",
582 "fixture_machinery_frozen-1.2.3.json",
583 json.dumps(tree, indent=2) + "\n",
584 )
585 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
586 assert any("does not validate" in p for p in problems)
589def test_check_reports_a_superseded_frozen_document_with_no_fixture(tmp_path: Path) -> None:
590 """Verify every frozen document, not just the live one, needs a
591 same-version fixture, and that a retired fixture satisfies it.
592 """
593 fixtures = tmp_path / "fixtures"
594 schemas = tmp_path / "schemas"
595 _write(
596 fixtures / "fixture_machinery_frozen",
597 "fixture_machinery_frozen-1.2.3.json",
598 canonical_fixture_text(_FrozenDouble()),
599 )
600 _write(schemas / "fixture_machinery_frozen", "fixture_machinery_frozen-1.2.3.json", "{}\n")
601 # A superseded version, frozen but with no fixture anywhere.
602 _write(schemas / "fixture_machinery_frozen", "fixture_machinery_frozen-1.0.0.json", "{}\n")
603 problems = check_schema_fixtures(
604 fixtures, schema_directory=schemas, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS
605 )
606 assert any("fixture_machinery_frozen-1.0.0.json" in p and "has no fixture" in p for p in problems)
608 # A retired fixture at that version counts as present.
609 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
610 retired["schema_version"] = "1.0.0"
611 retired["schema_url"] = "https://images.lsst.io/schemas/fixture_machinery_frozen-1.0.0"
612 retired["min_read_version"] = 999
613 _write(
614 fixtures / "fixture_machinery_frozen" / "retired",
615 "fixture_machinery_frozen-1.0.0.json",
616 json.dumps(retired, indent=2) + "\n",
617 )
618 problems = check_schema_fixtures(
619 fixtures, schema_directory=schemas, package=_FrozenDouble.__module__, exempt=_SIBLING_SCHEMAS
620 )
621 assert problems == []
624def test_check_reports_a_schema_url_that_disagrees_with_the_stamp(tmp_path: Path) -> None:
625 """Verify an on-disk schema_url that does not match its own
626 schema_version stamp is reported.
627 """
628 tree = json.loads(canonical_fixture_text(_FrozenDouble()))
629 tree["schema_url"] = "https://example.org/wrong"
630 _write(
631 tmp_path / "fixture_machinery_frozen",
632 "fixture_machinery_frozen-1.2.3.json",
633 json.dumps(tree, indent=2) + "\n",
634 )
635 problems = check_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
636 assert any("schema_url" in p and "expected" in p for p in problems)
639def test_refresh_rewrites_a_stale_development_fixture(tmp_path: Path) -> None:
640 """Verify refresh canonicalizes a drifted .dev fixture in place."""
641 stale = json.loads(canonical_fixture_text(_DevDouble(label="x")))
642 del stale["label"]
643 path = _write(
644 tmp_path / "fixture_machinery_dev",
645 "fixture_machinery_dev-2.0.0.dev.json",
646 json.dumps(stale, indent=2) + "\n",
647 )
648 assert refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__) == [path]
649 assert json.loads(path.read_text())["label"] == ""
650 assert refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__) == []
653def test_refresh_refuses_to_rewrite_a_frozen_fixture(tmp_path: Path) -> None:
654 """Verify a drifted frozen fixture raises rather than being rewritten."""
655 stale = json.loads(canonical_fixture_text(_FrozenDouble(label="x")))
656 del stale["label"]
657 path = _write(
658 tmp_path / "fixture_machinery_frozen",
659 "fixture_machinery_frozen-1.2.3.json",
660 json.dumps(stale, indent=2) + "\n",
661 )
662 before = path.read_text()
663 with pytest.raises(SchemaFixtureError, match="bump SCHEMA_VERSION"):
664 refresh_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
665 assert path.read_text() == before
668def test_refresh_seeds_a_missing_development_fixture(tmp_path: Path) -> None:
669 """Verify a new .dev fixture is seeded from the newest existing version."""
670 directory = tmp_path / "fixture_machinery_dev"
671 _write(
672 directory,
673 "fixture_machinery_dev-1.0.0.json",
674 canonical_fixture_text(_DevDouble(label="carried forward")),
675 )
676 _write(
677 directory,
678 "fixture_machinery_dev-1.0.0-dp1.json",
679 canonical_fixture_text(_DevDouble(label="variant carried forward")),
680 )
681 written = refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__)
682 seeded = directory / "fixture_machinery_dev-2.0.0.dev.json"
683 seeded_variant = directory / "fixture_machinery_dev-2.0.0.dev-dp1.json"
684 assert set(written) == {seeded, seeded_variant}
685 assert json.loads(seeded.read_text())["label"] == "carried forward"
686 assert json.loads(seeded_variant.read_text())["label"] == "variant carried forward"
687 assert json.loads(seeded.read_text())["schema_version"] == "2.0.0.dev3"
690def test_refresh_seeds_from_a_frozen_release_over_a_stale_dev_residual(tmp_path: Path) -> None:
691 """Verify a frozen release outranks a same-release .dev residual.
693 A stale ``.dev`` fixture left behind by an interrupted freeze and its
694 properly frozen same-release counterpart must not tie when picking a
695 newest source to seed from: the frozen one is strictly newer and must
696 win regardless of which one a directory listing happens to yield
697 first.
698 """
699 directory = tmp_path / "fixture_machinery_dev"
700 _write(
701 directory,
702 "fixture_machinery_dev-1.0.0.dev.json",
703 canonical_fixture_text(_DevDouble(label="stale dev residual")),
704 )
705 _write(
706 directory,
707 "fixture_machinery_dev-1.0.0.json",
708 canonical_fixture_text(_DevDouble(label="frozen release")),
709 )
710 written = refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__)
711 seeded = directory / "fixture_machinery_dev-2.0.0.dev.json"
712 assert written == [seeded]
713 assert json.loads(seeded.read_text())["label"] == "frozen release"
716def test_refresh_refuses_to_change_a_canonical_twin_at_a_frozen_version(tmp_path: Path) -> None:
717 """Verify refresh cannot erase a shipped-file compatibility failure."""
718 directory = tmp_path / "fixture_machinery_frozen"
719 _write(
720 directory,
721 "fixture_machinery_frozen-1.2.3.json",
722 canonical_fixture_text(_FrozenDouble()),
723 )
724 twin = _write(
725 directory,
726 "fixture_machinery_frozen-1.2.3-canonical.json",
727 canonical_fixture_text(_FrozenDouble(label="old normalization")),
728 )
729 _write(
730 directory,
731 "fixture_machinery_frozen-1.2.3-as_shipped.json",
732 canonical_fixture_text(_FrozenDouble(label="new normalization")),
733 )
734 before = twin.read_text()
735 with pytest.raises(SchemaFixtureError, match="compatibility contract"):
736 refresh_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
737 assert twin.read_text() == before
740def test_refresh_regenerates_a_development_canonical_twin(tmp_path: Path) -> None:
741 """Verify a development twin remains derived refreshable data."""
742 directory = tmp_path / "fixture_machinery_dev"
743 _write(
744 directory,
745 "fixture_machinery_dev-2.0.0.dev-as_shipped.json",
746 canonical_fixture_text(_DevDouble(label="shipped")),
747 )
748 twin = directory / "fixture_machinery_dev-2.0.0.dev-canonical.json"
749 assert refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__) == [twin]
750 assert json.loads(twin.read_text())["label"] == "shipped"
753def test_refresh_never_touches_retired_fixtures(tmp_path: Path) -> None:
754 """Verify a retired fixture is left exactly as it is."""
755 directory = tmp_path / "fixture_machinery_dev"
756 _write(
757 directory,
758 "fixture_machinery_dev-2.0.0.dev.json",
759 canonical_fixture_text(_DevDouble()),
760 )
761 retired = _write(directory / "retired", "fixture_machinery_dev-1.0.0.json", "{}\n")
762 refresh_schema_fixtures(tmp_path, package=_DevDouble.__module__)
763 assert retired.read_text() == "{}\n"
766def test_refresh_never_touches_a_retired_as_shipped_fixture(tmp_path: Path) -> None:
767 """Verify a retired as_shipped fixture is not read while regenerating
768 canonical twins.
770 A retired fixture no longer validates by design (that is what makes it
771 retired), so a twin-regeneration loop that does not exclude retired
772 fixtures would pass it to `read_fixture_tree` and raise instead of
773 leaving it alone, exactly the defect `check_schema_fixtures` was fixed
774 for with its own retired fixtures.
775 """
776 directory = tmp_path / "fixture_machinery_frozen"
777 _write(
778 directory,
779 "fixture_machinery_frozen-1.2.3.json",
780 canonical_fixture_text(_FrozenDouble()),
781 )
782 retired = json.loads(canonical_fixture_text(_FrozenDouble()))
783 retired["min_read_version"] = 999
784 retired_path = _write(
785 directory / "retired",
786 "fixture_machinery_frozen-1.0.0-as_shipped.json",
787 json.dumps(retired, indent=2) + "\n",
788 )
789 before = retired_path.read_text()
790 assert refresh_schema_fixtures(tmp_path, package=_FrozenDouble.__module__) == []
791 assert retired_path.read_text() == before
792 assert not (directory / "retired" / "fixture_machinery_frozen-1.0.0-canonical.json").exists()
795def test_freeze_writes_the_final_version_and_deletes_the_dev_file(tmp_path: Path) -> None:
796 """Verify freeze writes {name}-{X.Y.Z}.json and removes the .dev file.
798 The double's live version is finalized, so its .dev fixture is the
799 residue of a freeze that has not been completed yet.
800 """
801 directory = tmp_path / "fixture_machinery_frozen"
802 dev_tree = json.loads(canonical_fixture_text(_FrozenDouble(label="frozen soon")))
803 dev_tree["schema_version"] = "1.2.3.dev0"
804 dev_path = _write(
805 directory, "fixture_machinery_frozen-1.2.3.dev.json", json.dumps(dev_tree, indent=2) + "\n"
806 )
807 frozen_path = directory / "fixture_machinery_frozen-1.2.3.json"
808 assert freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__) == [(frozen_path, dev_path)]
809 assert not dev_path.exists()
810 assert json.loads(frozen_path.read_text())["schema_version"] == "1.2.3"
811 assert json.loads(frozen_path.read_text())["label"] == "frozen soon"
812 assert freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__) == []
815def test_freeze_carries_variants_and_refuses_to_clobber(tmp_path: Path) -> None:
816 """Verify each variant freezes too, and an existing target is refused."""
817 directory = tmp_path / "fixture_machinery_frozen"
818 _write(
819 directory,
820 "fixture_machinery_frozen-1.2.3.dev-dp1.json",
821 canonical_fixture_text(_FrozenDouble(label="variant")),
822 )
823 ((written, _),) = freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
824 assert written.name == "fixture_machinery_frozen-1.2.3-dp1.json"
826 _write(
827 directory,
828 "fixture_machinery_frozen-1.2.3.dev-dp1.json",
829 canonical_fixture_text(_FrozenDouble(label="again")),
830 )
831 with pytest.raises(SchemaFixtureError, match="already exists"):
832 freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
835def test_freeze_refuses_an_as_shipped_development_fixture(tmp_path: Path) -> None:
836 """Verify freeze neither rewrites shipped bytes nor mislabels their
837 version.
838 """
839 directory = tmp_path / "fixture_machinery_frozen"
840 shipped_text = (
841 '{"label": "shipped", "min_read_version": 1, '
842 '"schema_version": "1.2.3.dev0", '
843 '"schema_url": "https://images.lsst.io/schemas/fixture_machinery_frozen-1.2.3.dev0"}'
844 )
845 shipped_path = _write(directory, "fixture_machinery_frozen-1.2.3.dev-as_shipped.json", shipped_text)
846 twin_path = _write(
847 directory,
848 "fixture_machinery_frozen-1.2.3.dev-canonical.json",
849 canonical_fixture_text(_FrozenDouble(label="shipped")),
850 )
851 with pytest.raises(SchemaFixtureError, match="cannot be frozen"):
852 freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
853 assert shipped_path.read_text() == shipped_text
854 assert twin_path.exists()
855 assert not (directory / "fixture_machinery_frozen-1.2.3-as_shipped.json").exists()
858def test_freeze_preflights_all_target_conflicts_before_writing(tmp_path: Path) -> None:
859 """Verify a later variant conflict leaves earlier sources untouched."""
860 directory = tmp_path / "fixture_machinery_frozen"
861 base_source = _write(
862 directory,
863 "fixture_machinery_frozen-1.2.3.dev.json",
864 canonical_fixture_text(_FrozenDouble(label="base")),
865 )
866 variant_source = _write(
867 directory,
868 "fixture_machinery_frozen-1.2.3.dev-dp1.json",
869 canonical_fixture_text(_FrozenDouble(label="variant")),
870 )
871 _write(
872 directory,
873 "fixture_machinery_frozen-1.2.3-dp1.json",
874 canonical_fixture_text(_FrozenDouble(label="existing")),
875 )
876 with pytest.raises(SchemaFixtureError, match="already exists"):
877 freeze_schema_fixtures(tmp_path, package=_FrozenDouble.__module__)
878 assert base_source.exists()
879 assert variant_source.exists()
880 assert not (directory / "fixture_machinery_frozen-1.2.3.json").exists()
883DOUBLE_DIR = Path(__file__).parent / "data" / "fixture_doubles"
886def test_compare_fixture_versions_reports_a_shared_path_that_differs() -> None:
887 """Verify a value the old file expressed differently is reported."""
888 problems = compare_fixture_versions({"kept": "a"}, {"kept": "b", "added": 1})
889 assert len(problems) == 1
890 assert ".kept" in problems[0]
893def test_compare_fixture_versions_reports_a_path_lost_from_the_current_dump() -> None:
894 """Verify a path in the old dump but not the current one is reported."""
895 problems = compare_fixture_versions({"kept": "a", "dropped": 1}, {"kept": "a"})
896 assert any("dropped" in p for p in problems)
899def test_compare_fixture_versions_is_strict_about_numeric_type() -> None:
900 """Verify an integer where the current dump has a float is reported.
902 This is the drift that put integers into piff_psf's float fields.
903 """
904 assert compare_fixture_versions({"x": 0}, {"x": 0.0})
907def test_compare_fixture_versions_treats_nan_as_equal() -> None:
908 """Verify NaN compares equal to NaN, since many fields default to it."""
909 nan = float("nan")
910 assert compare_fixture_versions({"x": nan}, {"x": nan}) == []
913def test_compare_fixture_versions_requires_equal_list_lengths() -> None:
914 """Verify a list length change is reported rather than partly compared."""
915 problems = compare_fixture_versions({"xs": [1, 2]}, {"xs": [1, 2, 3]})
916 assert any("length" in p for p in problems)
919def test_compare_fixture_versions_recurses() -> None:
920 """Verify nested dicts and lists are compared path by path."""
921 old = {"a": {"b": [{"c": 1}]}}
922 current = {"a": {"b": [{"c": 2}]}}
923 problems = compare_fixture_versions(old, current)
924 assert ".a.b[0].c" in problems[0]
927def test_compare_fixture_versions_ignores_a_later_born_field_with_a_real_value() -> None:
928 """Verify a later-born field is ignored using the on-disk expressed set,
929 not ``old``'s own materialized-default value.
931 ``old`` already carries ``added`` at its post-read default (``0``),
932 exactly as reading a fixture that never mentioned it through the live
933 model would produce; only ``on_disk`` (the raw file) lacks it. Guards
934 against the regression this design corrects: deciding "later-born" from
935 ``old`` itself would treat this exact input as already agreeing only by
936 the coincidence that the real value chosen for ``added`` in ``current``
937 happens to equal its default -- it would report ``.added: 0 != 7`` for
938 any other value, which is exactly what the pre-fix algorithm does.
939 """
940 old = {"kept": "exemplar", "added": 0}
941 on_disk = {"kept": "exemplar"}
942 current = {"kept": "exemplar", "added": 7}
943 assert compare_fixture_versions(old, current, on_disk=on_disk) == []
946def test_compare_fixture_versions_expressed_paths_include_ancestors() -> None:
947 """Verify a path counts as expressed when a container ancestor is on
948 disk with a different container kind, even with no exact path match.
950 A field whose shape changed between versions -- here, a pair collapsed
951 from a two-element list into named components -- has no path on disk
952 that matches its new leaves exactly, but its container does, as a list
953 where the dump now has a dict; that reshape is what must be enough to
954 bring the leaves into comparison.
955 """
956 on_disk = {"image_pos": [1.0, 2.0]}
957 old = {"image_pos": {"x": 1.0, "y": 2.0}}
958 current = {"image_pos": {"x": 1.0, "y": 9.0}}
959 problems = compare_fixture_versions(old, current, on_disk=on_disk)
960 assert any(".image_pos.y" in p for p in problems)
963def test_compare_fixture_versions_ignores_a_later_born_field_under_an_empty_container() -> None:
964 """Verify a later-born field nested under an on-disk *empty* container
965 is ignored, not compared via the ancestor rule.
967 The container itself (``.container``) is on disk as an empty dict, and
968 stays a dict in the old dump too -- no reshape -- so its new leaf must
969 not inherit "expressed" status from it.
970 """
971 on_disk = {"kept": "exemplar", "container": {}}
972 old = {"kept": "exemplar", "container": {"newfield": 0}}
973 current = {"kept": "exemplar", "container": {"newfield": 42}}
974 assert compare_fixture_versions(old, current, on_disk=on_disk) == []
977def test_compare_fixture_versions_ignores_a_later_born_field_under_a_populated_container() -> None:
978 """Verify a later-born field nested under an on-disk *populated*
979 container is ignored, not compared via the ancestor rule.
981 Same as the empty-container case, except the container already has a
982 sibling field on disk, which is the more realistic shape these schemas
983 will actually take when a field is added to an existing group.
984 """
985 on_disk = {"kept": "exemplar", "container": {"already": 1}}
986 old = {"kept": "exemplar", "container": {"already": 1, "newfield": 0}}
987 current = {"kept": "exemplar", "container": {"already": 1, "newfield": 42}}
988 assert compare_fixture_versions(old, current, on_disk=on_disk) == []
991def test_compare_fixture_versions_exact_path_match_still_compares() -> None:
992 """Verify a path present on disk exactly still gets compared, with
993 ``on_disk`` passed explicitly rather than left to default to ``old``.
994 """
995 on_disk = {"kept": "a"}
996 old = {"kept": "a"}
997 current = {"kept": "b"}
998 assert compare_fixture_versions(old, current, on_disk=on_disk) == [".kept: 'a' != 'b'"]
1001def test_fixture_double_projection_contract() -> None:
1002 """Verify the additive fixture double projects with no escape hatch.
1004 ``projection_test`` is the oracle's positive control on real committed
1005 files. Its 1.1.0 fixture's ``added`` field carries a real, non-default
1006 value (``7``): a later-born field is only meaningfully tested by a value
1007 that is not its own type's default. The other cross-version double,
1008 ``migration_test``, renames a path and so cannot serve as a positive
1009 control; it is declared in ``test_schema_fixtures.EXPECTED_DIVERGENCE``
1010 and ``test_migration_test_fixture_reads_from_disk`` asserts its
1011 transformed value.
1012 """
1013 fixtures = {f.version: f for f in iter_schema_fixtures(DOUBLE_DIR) if f.name == "projection_test"}
1014 old_version = min(fixtures, key=Version)
1015 current_version = max(fixtures, key=Version)
1016 on_disk = json.loads(fixtures[old_version].path.read_text())
1017 old = json.loads(canonical_fixture_text(read_fixture_tree(fixtures[old_version])))
1018 current = json.loads(canonical_fixture_text(read_fixture_tree(fixtures[current_version])))
1019 assert compare_fixture_versions(old, current, on_disk=on_disk) == []
1022def test_projection_double_negative_control() -> None:
1023 """Verify the oracle fails when the old fixture disagrees.
1025 Without this, a projection check that could never fail would still pass
1026 every run.
1027 """
1028 fixtures = {f.version: f for f in iter_schema_fixtures(DOUBLE_DIR) if f.name == "projection_test"}
1029 on_disk = json.loads(fixtures["1.0.0"].path.read_text())
1030 old = json.loads(canonical_fixture_text(read_fixture_tree(fixtures["1.0.0"])))
1031 current = json.loads(canonical_fixture_text(read_fixture_tree(fixtures["1.1.0"])))
1032 old["kept"] = "tampered"
1033 assert compare_fixture_versions(old, current, on_disk=on_disk)
1036def test_projection_double_stamps_normalize_to_the_live_version() -> None:
1037 """Verify the older fixture's stamps compare equal after reading."""
1038 fixtures = {f.version: f for f in iter_schema_fixtures(DOUBLE_DIR) if f.name == "projection_test"}
1039 old = json.loads(canonical_fixture_text(read_fixture_tree(fixtures["1.0.0"])))
1040 assert old["schema_version"] == "1.1.0"
1043def test_retire_double_fixture_is_rejected() -> None:
1044 """Verify a fixture under retired/ raises rather than reading.
1046 A 1.0.0 tree cannot satisfy the 2.0.0 shape and no migration covers the
1047 gap, so Pydantic rejects it. ``read_fixture_tree`` translates that
1048 validation failure to the archive-domain error required by the retirement
1049 contract.
1050 """
1051 fixtures = {
1052 (f.version, f.retired): f for f in iter_schema_fixtures(DOUBLE_DIR) if f.name == "retire_test"
1053 }
1054 retired = fixtures[("1.0.0", True)]
1055 assert retired.retired
1056 with pytest.raises(ArchiveReadError):
1057 read_fixture_tree(retired)
1060def test_current_fixture_of_a_retiring_schema_still_reads() -> None:
1061 """Verify retirement scopes to the old version, not the whole schema."""
1062 fixtures = {
1063 (f.version, f.retired): f for f in iter_schema_fixtures(DOUBLE_DIR) if f.name == "retire_test"
1064 }
1065 tree = read_fixture_tree(fixtures[("2.0.0", False)])
1066 assert tree.required_at_v2 == "present" # type: ignore[attr-defined]
1069def test_check_accepts_a_retired_fixture_that_is_rejected(tmp_path: Path) -> None:
1070 """Verify a properly retired fixture is not reported as a problem."""
1071 directory = tmp_path / "retire_test"
1072 _write(
1073 directory,
1074 "retire_test-2.0.0.json",
1075 canonical_fixture_text(RetireTestModel(required_at_v2="present")),
1076 )
1077 _write(
1078 directory / "retired",
1079 "retire_test-1.0.0.json",
1080 '{\n "schema_version": "1.0.0",\n "min_read_version": 1,\n'
1081 ' "schema_url": "https://images.lsst.io/schemas/retire_test-1.0.0"\n}\n',
1082 )
1083 # Every schema in schema_doubles shares this __module__, so the package
1084 # filter pulls all of them in; exempt the ones this test is not about,
1085 # or their absent fixtures are reported as missing.
1086 assert (
1087 check_schema_fixtures(
1088 tmp_path,
1089 package=RetireTestModel.__module__,
1090 exempt=[
1091 "chain_test",
1092 "gap_test",
1093 "migration_isolation_test",
1094 "migration_test",
1095 "projection_test",
1096 "unmigratable_test",
1097 ],
1098 )
1099 == []
1100 )
1103def test_check_reports_a_retired_fixture_at_the_live_version(tmp_path: Path) -> None:
1104 """Verify a stray file under retired/ is reported, not quietly accepted.
1106 Retirement is how a *superseded* version's fixture is kept, so a retired
1107 fixture at the live version is a file in the wrong place. Every other
1108 check passes it: its stamps are self-consistent, and the model rejects it,
1109 which is exactly what a retired fixture is required to do.
1110 """
1111 directory = tmp_path / "retire_test"
1112 _write(
1113 directory,
1114 "retire_test-2.0.0.json",
1115 canonical_fixture_text(RetireTestModel(required_at_v2="present")),
1116 )
1117 _write(
1118 directory / "retired",
1119 "retire_test-2.0.0.json",
1120 '{\n "schema_version": "2.0.0",\n "min_read_version": 2,\n'
1121 ' "schema_url": "https://images.lsst.io/schemas/retire_test-2.0.0"\n}\n',
1122 )
1123 problems = check_schema_fixtures(
1124 tmp_path,
1125 package=RetireTestModel.__module__,
1126 exempt=[
1127 "chain_test",
1128 "gap_test",
1129 "migration_isolation_test",
1130 "migration_test",
1131 "projection_test",
1132 "unmigratable_test",
1133 ],
1134 )
1135 assert [p for p in problems if "retired" in p], problems