Coverage for tests/test_fits_output_archive.py: 100%
45 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 15:24 -0700
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
14from collections.abc import Callable
15from pathlib import Path
16from typing import Any, ClassVar
18import astropy.io.fits
19import numpy as np
20import pydantic
22from lsst.images.fits import FitsOutputArchive
23from lsst.images.serialization import ArchiveTree, InputArchive
26class _TinyTree(ArchiveTree):
27 """Minimal concrete ArchiveTree for low-level archive writes."""
29 SCHEMA_NAME: ClassVar[str] = "test_fits_output_archive"
30 SCHEMA_VERSION: ClassVar[str] = "1.0.0"
31 MIN_READ_VERSION: ClassVar[int] = 1
32 PUBLIC_TYPE: ClassVar[type] = object
34 def deserialize(
35 self, archive: InputArchive[Any], **kwargs: Any
36 ) -> _TinyTree: # pragma: no cover - never invoked
37 raise NotImplementedError()
40class _PointerTarget(pydantic.BaseModel):
41 """A trivial pointer-target model holding an array reference."""
43 data: dict[str, Any] | None = None
46def _write_archive(body: Callable[[FitsOutputArchive], None], tmp_path: Path) -> list[tuple[str, int | None]]:
47 """Write an archive, applying ``body`` to it, and return the
48 ``(EXTNAME, EXTVER)`` pairs of the resulting extension HDUs.
49 """
50 filename = tmp_path / "test.fits"
51 with FitsOutputArchive.open(filename) as archive:
52 body(archive)
53 archive.add_tree(_TinyTree())
54 with astropy.io.fits.open(filename) as hdu_list:
55 return [
56 (hdu.header["EXTNAME"], hdu.header.get("EXTVER"))
57 for hdu in hdu_list[1:]
58 if hdu.header.get("EXTNAME") not in ("JSON", "INDEX")
59 ]
62def test_repeated_direct_names_get_increasing_extver(tmp_path: Path) -> None:
63 """Verify repeated direct names get increasing EXTVER disambiguation."""
64 array = np.zeros((2, 2), dtype=np.float32)
65 sources = []
67 def body(archive: FitsOutputArchive) -> None:
68 sources.append(archive.add_array(array, name="data").source)
69 sources.append(archive.add_array(array, name="data").source)
71 keys = _write_archive(body, tmp_path)
72 assert sources == ["fits:DATA", "fits:DATA,2"]
73 assert keys == [("DATA", None), ("DATA", 2)]
76def test_direct_and_pointer_target_names_do_not_collide(tmp_path: Path) -> None:
77 """Verify a direct name and a pointer target's nested name do not
78 collide.
79 """
80 # A direct name and a pointer target's nested name (registered with
81 # a leading slash because the pointer's nested archive is rooted at
82 # "") already produce distinct EXTNAMEs, so neither needs EXTVER
83 # disambiguation.
84 array = np.zeros((2, 2), dtype=np.float32)
85 sources = []
87 def serializer(archive: FitsOutputArchive):
88 ref = archive.add_array(array, name="data")
89 sources.append(ref.source)
90 return _PointerTarget(data=ref.model_dump())
92 def body(archive: FitsOutputArchive):
93 sources.append(archive.add_array(array, name="data").source)
94 archive.serialize_pointer("psf", serializer, key="psf-key") # type: ignore[arg-type]
96 keys = _write_archive(body, tmp_path)
97 assert sources == ["fits:DATA", "fits:/DATA"]
98 assert keys == [("DATA", None), ("/DATA", None)]