Coverage for tests/test_serialization_io.py: 92%
98 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.
11from __future__ import annotations
13import os
14from pathlib import Path
16import numpy as np
17import pytest
19from lsst.images import Box, Image, VisitImage
20from lsst.images.serialization import ArchiveReadError, read_archive, write_archive
21from lsst.utils.introspection import get_full_type_name
23try:
24 import h5py # noqa: F401 -- detect availability for NDF round-trip skip
26 H5PY_AVAILABLE = True
27except ImportError:
28 H5PY_AVAILABLE = False
30try:
31 import piff # noqa: F401 -- detect availability for piff_psf fixture skip
33 PIFF_AVAILABLE = True
34except ImportError:
35 PIFF_AVAILABLE = False
37skip_no_h5py = pytest.mark.skipif(not H5PY_AVAILABLE, reason="h5py is not installed")
39LOCAL_DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1")
41# Full Python type produced when each fixture is read through the generic
42# read_archive() API, keyed by the fixture's file name. These are pinned
43# here rather than derived from the schema registry so the test asserts
44# the externally-observable type instead of re-running read_archive()'s
45# own lookup against itself.
46EXPECTED_TYPES = {
47 "aperture_correction_map.json": "dict",
48 "background_map.json": "lsst.images.BackgroundMap",
49 "cell_psf.json": "lsst.images.cells.CellPointSpreadFunction",
50 "cell_aperture_correction_map.json": "dict",
51 "chebyshev_field.json": "lsst.images.fields.ChebyshevField",
52 "coadd_provenance.json": "lsst.images.cells.CoaddProvenance",
53 "color_image.json": "lsst.images.ColorImage",
54 "detector.json": "lsst.images.cameras.Detector",
55 "gaussian_psf.json": "lsst.images.psfs.GaussianPointSpreadFunction",
56 "image.json": "lsst.images.Image",
57 "mask.json": "lsst.images.Mask",
58 "masked_image.json": "lsst.images.MaskedImage",
59 "piff_psf.json": "lsst.images.psfs.PiffWrapper",
60 "product_field.json": "lsst.images.fields.ProductField",
61 "sky_projection.json": "lsst.images.SkyProjection",
62 "sum_field.json": "lsst.images.fields.SumField",
63 "transform.json": "lsst.images.Transform",
64 "visit_image.json": "lsst.images.VisitImage",
65 "cell_coadd.json": "lsst.images.cells.CellCoadd",
66 "visit_image_dp1.json": "lsst.images.VisitImage",
67 "visit_image_dp2.json": "lsst.images.VisitImage",
68 "difference_image_dp2.json": "lsst.images.DifferenceImage",
69}
72def test_generic_read_visit_image_json() -> None:
73 """Verify read_archive() on a visit_image JSON fixture returns a
74 VisitImage.
75 """
76 path = os.path.join(LOCAL_DATA_DIR, "visit_image.json")
77 result = read_archive(path)
78 assert isinstance(result, VisitImage)
81def test_generic_read_image_json() -> None:
82 """Verify read_archive() on an image JSON fixture returns an Image."""
83 path = os.path.join(LOCAL_DATA_DIR, "image.json")
84 result = read_archive(path)
85 assert isinstance(result, Image)
88def test_read_unsupported_extension(tmp_path: Path) -> None:
89 """Verify read_archive() raises ValueError for an unrecognized file
90 extension.
91 """
92 path = tmp_path / "bogus.txt"
93 with open(path, "w") as f:
94 f.write("nope")
95 with pytest.raises(ValueError):
96 read_archive(path)
99def test_read_unregistered_schema(tmp_path: Path) -> None:
100 """Verify read_archive() raises ArchiveReadError for a JSON with an unknown
101 schema.
102 """
103 path = tmp_path / "fake.json"
104 with open(path, "w") as f:
105 f.write(
106 '{"schema_url": "https://images.lsst.io/schemas/no-such-schema-99.0.0",'
107 ' "schema_version": "99.0.0", "min_read_version": 1, "indirect": []}'
108 )
109 with pytest.raises(ArchiveReadError) as exc_info:
110 read_archive(path)
111 assert "no-such-schema" in str(exc_info.value)
114@pytest.mark.parametrize("entry", sorted(EXPECTED_TYPES))
115def test_fixture_sweep(entry: str) -> None:
116 """Verify every schema_v1 JSON fixture reads to the pinned Python type."""
117 if entry == "piff_psf.json" and not PIFF_AVAILABLE: 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 pytest.skip("piff not available")
119 roots = [LOCAL_DATA_DIR, os.path.join(LOCAL_DATA_DIR, "legacy")]
120 for root in roots: 120 ↛ 126line 120 didn't jump to line 126 because the loop on line 120 didn't complete
121 path = os.path.join(root, entry)
122 if os.path.exists(path):
123 result = read_archive(path)
124 assert get_full_type_name(type(result)) == EXPECTED_TYPES[entry], entry
125 return
126 pytest.skip(f"fixture {entry!r} not found on disk")
129def _make_image() -> Image:
130 """Return a small float32 Image for round-trip tests."""
131 return Image(np.arange(16, dtype=np.float32).reshape(4, 4), bbox=Box.factory[0:4, 0:4])
134def test_generic_write_round_trip_fits(tmp_path: Path) -> None:
135 """Verify write_archive() + read_archive() round-trips an Image
136 through FITS.
137 """
138 image = _make_image()
139 path = tmp_path / "x.fits"
140 write_archive(image, path)
141 result = read_archive(path)
142 assert isinstance(result, Image)
143 np.testing.assert_array_equal(result.array, image.array)
146def test_generic_write_round_trip_json(tmp_path: Path) -> None:
147 """Verify write_archive() + read_archive() round-trips an Image
148 through JSON.
149 """
150 image = _make_image()
151 path = tmp_path / "x.json"
152 write_archive(image, path)
153 result = read_archive(path)
154 assert isinstance(result, Image)
155 np.testing.assert_array_equal(result.array, image.array)
158@skip_no_h5py
159def test_generic_write_round_trip_ndf(tmp_path: Path) -> None:
160 """Verify write_archive() + read_archive() round-trips an Image
161 through NDF.
162 """
163 image = _make_image()
164 path = tmp_path / "x.sdf"
165 write_archive(image, path)
166 result = read_archive(path)
167 assert isinstance(result, Image)
168 np.testing.assert_array_equal(result.array, image.array)
171def test_read_bbox_subset_fits(tmp_path: Path) -> None:
172 """Verify read_archive() forwards bbox kwarg to the FITS backend for subset
173 reads.
174 """
175 img = Image(np.arange(64, dtype=np.float32).reshape(8, 8), bbox=Box.factory[0:8, 0:8])
176 path = tmp_path / "x.fits"
177 write_archive(img, path)
178 sub = read_archive(path, bbox=Box.factory[2:6, 2:6])
179 assert sub.array.shape == (4, 4)
180 np.testing.assert_array_equal(sub.array, img.array[2:6, 2:6])
183def test_read_cls_match() -> None:
184 """Verify read_archive() with cls= returns the expected type when it
185 matches.
186 """
187 path = os.path.join(LOCAL_DATA_DIR, "image.json")
188 result = read_archive(path, cls=Image)
189 assert isinstance(result, Image)
192def test_read_cls_mismatch_raises() -> None:
193 """Verify read_archive() raises TypeError when the deserialized type
194 does not match cls.
195 """
196 from lsst.images import Mask
198 path = os.path.join(LOCAL_DATA_DIR, "image.json")
199 with pytest.raises(TypeError) as exc_info:
200 read_archive(path, cls=Mask)
201 msg = str(exc_info.value)
202 assert "image" in msg # path / schema name
203 assert "Image" in msg # actual deserialized type
204 assert "Mask" in msg # requested cls