Coverage for tests/test_schema_fixture_machinery.py: 99%

463 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-09-29 10:33 +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. 

11 

12from __future__ import annotations 

13 

14import json 

15from pathlib import Path 

16from typing import Any, ClassVar 

17 

18import pydantic 

19import pytest 

20from packaging.version import Version 

21 

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 

27 

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) 

48 

49 

50class _FrozenDouble(ArchiveTree): 

51 """Finalized-version double for the fixture machinery tests.""" 

52 

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 

57 

58 label: str = pydantic.Field(default="", description="A value to round-trip.") 

59 

60 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any: 

61 return {"label": self.label} 

62 

63 

64class _DevDouble(ArchiveTree): 

65 """Development-version double for the fixture machinery tests.""" 

66 

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 

71 

72 label: str = pydantic.Field(default="", description="A value to round-trip.") 

73 

74 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any: 

75 return {"label": self.label} 

76 

77 

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 """ 

82 

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 

87 

88 required: str = pydantic.Field(description="A value with no default.") 

89 

90 def deserialize(self, archive: InputArchive[Any], **kwargs: Any) -> Any: 

91 return {"required": self.required} 

92 

93 

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") 

100 

101 

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" 

107 

108 

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" 

116 

117 

118def test_current_fixture_path_resolves_by_schema_name() -> None: 

119 """Verify a consumer can find a fixture without naming a version. 

120 

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") 

133 

134 

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" 

141 

142 

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 

148 

149 

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 

177 

178 

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" 

187 

188 

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] 

197 

198 

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] 

209 

210 

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 

216 

217 

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 == [] 

227 

228 

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) 

241 

242 

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 

262 

263 

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) 

274 

275 

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. 

278 

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) 

294 

295 

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) 

301 

302 

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) 

308 

309 

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) 

324 

325 

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) 

345 

346 

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) 

358 

359 

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 

380 

381 def fail_retired(fixture: schema_fixture_module.SchemaFixture) -> ArchiveTree: 

382 if fixture.retired: 

383 raise RuntimeError("reader bug") 

384 return original(fixture) 

385 

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) 

389 

390 

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) 

410 

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) 

418 

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) == [] 

425 

426 

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) 

443 

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 ) 

451 

452 

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 == [] 

461 

462 

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. 

465 

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 == [] 

491 

492 

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. 

496 

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 == [] 

527 

528 

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. 

534 

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) 

556 

557 

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) 

572 

573 

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) 

587 

588 

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) 

607 

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 == [] 

622 

623 

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) 

637 

638 

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__) == [] 

651 

652 

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 

666 

667 

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" 

688 

689 

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. 

692 

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" 

714 

715 

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 

738 

739 

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" 

751 

752 

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" 

764 

765 

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. 

769 

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() 

793 

794 

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. 

797 

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__) == [] 

813 

814 

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" 

825 

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__) 

833 

834 

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() 

856 

857 

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() 

881 

882 

883DOUBLE_DIR = Path(__file__).parent / "data" / "fixture_doubles" 

884 

885 

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] 

891 

892 

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) 

897 

898 

899def test_compare_fixture_versions_is_strict_about_numeric_type() -> None: 

900 """Verify an integer where the current dump has a float is reported. 

901 

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}) 

905 

906 

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}) == [] 

911 

912 

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) 

917 

918 

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] 

925 

926 

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. 

930 

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) == [] 

944 

945 

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. 

949 

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) 

961 

962 

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. 

966 

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) == [] 

975 

976 

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. 

980 

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) == [] 

989 

990 

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'"] 

999 

1000 

1001def test_fixture_double_projection_contract() -> None: 

1002 """Verify the additive fixture double projects with no escape hatch. 

1003 

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) == [] 

1020 

1021 

1022def test_projection_double_negative_control() -> None: 

1023 """Verify the oracle fails when the old fixture disagrees. 

1024 

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) 

1034 

1035 

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" 

1041 

1042 

1043def test_retire_double_fixture_is_rejected() -> None: 

1044 """Verify a fixture under retired/ raises rather than reading. 

1045 

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) 

1058 

1059 

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] 

1067 

1068 

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 ) 

1101 

1102 

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. 

1105 

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