Coverage for tests/test_serialization_streams.py: 96%
152 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:27 -0700
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 02:27 -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.
11"""Tests for reading archives from in-memory bytes and binary streams."""
13from __future__ import annotations
15import gzip
16import io
17import os
18import tempfile
19import unittest
21import numpy as np
23from lsst.images import Box, Image, Mask
24from lsst.images.fits import FitsInputArchive
25from lsst.images.json import JsonInputArchive
26from lsst.images.serialization import open_archive, read_archive, write_archive
28try:
29 import h5py # noqa: F401 -- detect availability for NDF stream skips
31 from lsst.images.ndf import NdfInputArchive
33 H5PY_AVAILABLE = True
34except ImportError:
35 H5PY_AVAILABLE = False
37try:
38 from compression import zstd as _stdlib_zstd # noqa: F401 -- detect zstd availability
40 ZSTD_AVAILABLE = True
41except ImportError:
42 try:
43 import zstandard # noqa: F401
45 ZSTD_AVAILABLE = True
46 except ImportError:
47 ZSTD_AVAILABLE = False
50def _zstd_compress(data: bytes) -> bytes:
51 """Compress with whichever zstd library is available."""
52 try:
53 from compression import zstd
54 except ImportError:
55 import zstandard
57 return zstandard.ZstdCompressor().compress(data)
58 return zstd.compress(data)
61def _test_image() -> Image:
62 return Image(np.arange(16, dtype=np.float32).reshape(4, 4), bbox=Box.factory[0:4, 0:4])
65def _serialized_bytes(obj: object, extension: str) -> bytes:
66 """Write ``obj`` to a temporary file and return the file's content."""
67 with tempfile.TemporaryDirectory() as tmp:
68 path = os.path.join(tmp, f"x{extension}")
69 write_archive(obj, path)
70 with open(path, "rb") as f:
71 return f.read()
74class FitsOpenTreeStreamTestCase(unittest.TestCase):
75 """FitsInputArchive.open_tree accepts a seekable binary stream."""
77 def test_open_tree_stream(self) -> None:
78 image = _test_image()
79 stream = io.BytesIO(_serialized_bytes(image, ".fits"))
80 with FitsInputArchive.open_tree(stream) as (archive, tree, info):
81 self.assertEqual(info.schema_name, "image")
82 result = tree.deserialize(archive)
83 self.assertIsInstance(result, Image)
84 np.testing.assert_array_equal(result.array, image.array)
87class JsonOpenTreeStreamTestCase(unittest.TestCase):
88 """JsonInputArchive.open_tree accepts a seekable binary stream."""
90 def test_open_tree_stream(self) -> None:
91 image = _test_image()
92 stream = io.BytesIO(_serialized_bytes(image, ".json"))
93 with JsonInputArchive.open_tree(stream) as (archive, tree, info):
94 result = tree.deserialize(archive)
95 self.assertIsInstance(result, Image)
96 np.testing.assert_array_equal(result.array, image.array)
99@unittest.skipUnless(H5PY_AVAILABLE, "h5py not available.")
100class NdfOpenTreeStreamTestCase(unittest.TestCase):
101 """NdfInputArchive.open_tree accepts a seekable binary stream."""
103 def test_open_tree_stream(self) -> None:
104 image = _test_image()
105 stream = io.BytesIO(_serialized_bytes(image, ".sdf"))
106 with NdfInputArchive.open_tree(stream) as (archive, tree, info):
107 result = tree.deserialize(archive)
108 self.assertIsInstance(result, Image)
109 np.testing.assert_array_equal(result.array, image.array)
112class ReadStreamTestCase(unittest.TestCase):
113 """read_archive(io.BytesIO(data)) turns in-memory data into objects."""
115 def setUp(self) -> None:
116 self.image = _test_image()
118 def test_fits(self) -> None:
119 result = read_archive(io.BytesIO(_serialized_bytes(self.image, ".fits")))
120 self.assertIsInstance(result, Image)
121 np.testing.assert_array_equal(result.array, self.image.array)
123 def test_json(self) -> None:
124 result = read_archive(io.BytesIO(_serialized_bytes(self.image, ".json")))
125 self.assertIsInstance(result, Image)
126 np.testing.assert_array_equal(result.array, self.image.array)
128 @unittest.skipUnless(H5PY_AVAILABLE, "h5py not available.")
129 def test_ndf(self) -> None:
130 result = read_archive(io.BytesIO(_serialized_bytes(self.image, ".sdf")))
131 self.assertIsInstance(result, Image)
132 np.testing.assert_array_equal(result.array, self.image.array)
134 def test_cls_match_and_mismatch(self) -> None:
135 data = _serialized_bytes(self.image, ".fits")
136 result = read_archive(io.BytesIO(data), cls=Image)
137 self.assertIsInstance(result, Image)
138 with self.assertRaises(TypeError):
139 read_archive(io.BytesIO(data), cls=Mask)
141 def test_kwargs_forwarded(self) -> None:
142 big = Image(np.arange(64, dtype=np.float32).reshape(8, 8), bbox=Box.factory[0:8, 0:8])
143 sub = read_archive(io.BytesIO(_serialized_bytes(big, ".fits")), bbox=Box.factory[2:6, 2:6])
144 self.assertEqual(sub.array.shape, (4, 4))
145 np.testing.assert_array_equal(sub.array, big.array[2:6, 2:6])
147 def test_format_override(self) -> None:
148 result = read_archive(io.BytesIO(_serialized_bytes(self.image, ".json")), format="json")
149 self.assertIsInstance(result, Image)
151 def test_wrong_format_override(self) -> None:
152 # FITS bytes forced through the JSON backend fail in that backend.
153 with self.assertRaises(ValueError):
154 read_archive(io.BytesIO(_serialized_bytes(self.image, ".fits")), format="json")
156 def test_unrecognized_bytes(self) -> None:
157 with self.assertRaises(ValueError) as cm:
158 read_archive(io.BytesIO(b"certainly not a supported format"))
159 self.assertIn("FITS", str(cm.exception))
161 def test_bad_format_name(self) -> None:
162 with self.assertRaises(ValueError):
163 read_archive(io.BytesIO(_serialized_bytes(self.image, ".json")), format="asdf")
165 def test_compressed_stream_raises(self) -> None:
166 # Compressed streams are the caller's responsibility to decompress;
167 # the sniff error says what to do.
168 data = gzip.compress(_serialized_bytes(self.image, ".fits"))
169 with self.assertRaises(ValueError) as cm:
170 read_archive(io.BytesIO(data))
171 self.assertIn("gzip", str(cm.exception))
174class OpenStreamTestCase(unittest.TestCase):
175 """open() accepts a seekable binary stream, with component reads."""
177 DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1")
179 def test_open_stream_components(self) -> None:
180 visit_image = read_archive(os.path.join(self.DATA_DIR, "visit_image.json"))
181 stream = io.BytesIO(_serialized_bytes(visit_image, ".fits"))
182 with open_archive(stream) as reader:
183 self.assertEqual(reader.info.schema_name, "visit_image")
184 self.assertIsInstance(reader.metadata, dict)
185 self.assertIsNotNone(reader.get_component("sky_projection"))
186 full = reader.read()
187 self.assertEqual(type(full).__name__, "VisitImage")
190class CompressedPathTestCase(unittest.TestCase):
191 """Compressed files read transparently via path-based read_archive()."""
193 def setUp(self) -> None:
194 tmp = tempfile.TemporaryDirectory()
195 self.addCleanup(tmp.cleanup)
196 self.tmp = tmp.name
197 self.image = _test_image()
199 def _write_compressed(self, extension: str, compress) -> str:
200 data = compress(_serialized_bytes(self.image, extension))
201 suffix = ".gz" if compress is gzip.compress else ".zst"
202 path = os.path.join(self.tmp, f"x{extension}{suffix}")
203 with open(path, "wb") as f:
204 f.write(data)
205 return path
207 def test_fits_gz(self) -> None:
208 # Regression: .fits.gz was dispatched to the FITS backend but
209 # handed to it still compressed.
210 path = self._write_compressed(".fits", gzip.compress)
211 result = read_archive(path)
212 self.assertIsInstance(result, Image)
213 np.testing.assert_array_equal(result.array, self.image.array)
215 def test_json_gz(self) -> None:
216 path = self._write_compressed(".json", gzip.compress)
217 self.assertIsInstance(read_archive(path), Image)
219 @unittest.skipUnless(ZSTD_AVAILABLE, "no zstd decompressor available.")
220 def test_fits_zst(self) -> None:
221 path = self._write_compressed(".fits", _zstd_compress)
222 self.assertIsInstance(read_archive(path), Image)
224 def test_open_fits_gz(self) -> None:
225 path = self._write_compressed(".fits", gzip.compress)
226 with open_archive(path) as reader:
227 self.assertIsInstance(reader.read(), Image)
230if __name__ == "__main__":
231 unittest.main()