Coverage for tests/test_serialization_io.py: 95%

115 statements  

« 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. 

11from __future__ import annotations 

12 

13import os 

14import tempfile 

15import unittest 

16 

17import numpy as np 

18 

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 

22 

23try: 

24 import h5py # noqa: F401 -- detect availability for NDF round-trip skip 

25except ImportError: 

26 H5PY_AVAILABLE = False 

27else: 

28 H5PY_AVAILABLE = True 

29 

30try: 

31 import piff # noqa: F401 -- detect availability for piff_psf fixture skip 

32except ImportError: 

33 PIFF_AVAILABLE = False 

34else: 

35 PIFF_AVAILABLE = True 

36 

37DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1") 

38 

39# Full Python type produced when each fixture is read through the generic 

40# read_archive() API, keyed by the fixture's file name. These are pinned 

41# here rather than derived from the schema registry so the test asserts 

42# the externally-observable type instead of re-running read_archive()'s 

43# own lookup against itself. 

44EXPECTED_TYPES = { 

45 "aperture_correction_map.json": "dict", 

46 "background_map.json": "lsst.images.BackgroundMap", 

47 "cell_psf.json": "lsst.images.cells.CellPointSpreadFunction", 

48 "cell_aperture_correction_map.json": "dict", 

49 "chebyshev_field.json": "lsst.images.fields.ChebyshevField", 

50 "coadd_provenance.json": "lsst.images.cells.CoaddProvenance", 

51 "color_image.json": "lsst.images.ColorImage", 

52 "detector.json": "lsst.images.cameras.Detector", 

53 "gaussian_psf.json": "lsst.images.psfs.GaussianPointSpreadFunction", 

54 "image.json": "lsst.images.Image", 

55 "mask.json": "lsst.images.Mask", 

56 "masked_image.json": "lsst.images.MaskedImage", 

57 "piff_psf.json": "lsst.images.psfs.PiffWrapper", 

58 "product_field.json": "lsst.images.fields.ProductField", 

59 "sky_projection.json": "lsst.images.SkyProjection", 

60 "sum_field.json": "lsst.images.fields.SumField", 

61 "transform.json": "lsst.images.Transform", 

62 "visit_image.json": "lsst.images.VisitImage", 

63 "cell_coadd.json": "lsst.images.cells.CellCoadd", 

64 "visit_image_dp1.json": "lsst.images.VisitImage", 

65 "visit_image_dp2.json": "lsst.images.VisitImage", 

66 "difference_image_dp2.json": "lsst.images.DifferenceImage", 

67} 

68 

69 

70class GenericReadTestCase(unittest.TestCase): 

71 """read_archive(path) dispatches by extension and produces the 

72 registered type. 

73 """ 

74 

75 def test_visit_image_json(self) -> None: 

76 path = os.path.join(DATA_DIR, "visit_image.json") 

77 result = read_archive(path) 

78 self.assertIsInstance(result, VisitImage) 

79 

80 def test_image_json(self) -> None: 

81 path = os.path.join(DATA_DIR, "image.json") 

82 result = read_archive(path) 

83 self.assertIsInstance(result, Image) 

84 

85 

86class GenericReadErrorsTestCase(unittest.TestCase): 

87 """Unknown schemas and bad extensions raise clean errors.""" 

88 

89 def setUp(self) -> None: 

90 tmp = tempfile.TemporaryDirectory() 

91 self.addCleanup(tmp.cleanup) 

92 self.tmp = tmp.name 

93 

94 def test_unsupported_extension(self) -> None: 

95 path = os.path.join(self.tmp, "bogus.txt") 

96 with open(path, "w") as f: 

97 f.write("nope") 

98 # backend_for_path raises ValueError; read_archive() must let it 

99 # through. 

100 with self.assertRaises(ValueError): 

101 read_archive(path) 

102 

103 def test_unregistered_schema(self) -> None: 

104 # Write a JSON file with a fabricated schema name not in the 

105 # registry. 

106 path = os.path.join(self.tmp, "fake.json") 

107 with open(path, "w") as f: 

108 f.write( 

109 '{"schema_url": "https://images.lsst.io/schemas/no-such-schema-99.0.0",' 

110 ' "schema_version": "99.0.0", "min_read_version": 1, "indirect": []}' 

111 ) 

112 with self.assertRaises(ArchiveReadError) as ctx: 

113 read_archive(path) 

114 self.assertIn("no-such-schema", str(ctx.exception)) 

115 

116 

117class FixtureSweepTestCase(unittest.TestCase): 

118 """Every schema_v1 JSON fixture reads through the generic API and 

119 produces the Python type pinned in ``EXPECTED_TYPES``. 

120 """ 

121 

122 def test_sweep(self) -> None: 

123 # piff is an optional dependency that PiffSerializationModel 

124 # imports lazily on deserialise; skip its fixture when missing. 

125 skip = set() if PIFF_AVAILABLE else {"piff_psf.json"} 

126 seen = set() 

127 roots = [DATA_DIR, os.path.join(DATA_DIR, "legacy")] 

128 for root in roots: 

129 if not os.path.isdir(root): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 continue 

131 for entry in sorted(os.listdir(root)): 

132 if not entry.endswith(".json") or entry in skip: 

133 continue 

134 with self.subTest(entry=entry): 

135 self.assertIn(entry, EXPECTED_TYPES, f"no expected type recorded for {entry!r}") 

136 result = read_archive(os.path.join(root, entry)) 

137 self.assertEqual(get_full_type_name(type(result)), EXPECTED_TYPES[entry]) 

138 seen.add(entry) 

139 # Fail if EXPECTED_TYPES drifts from the fixtures actually on disk. 

140 self.assertEqual(seen, set(EXPECTED_TYPES) - skip) 

141 

142 

143class GenericWriteRoundTripTestCase(unittest.TestCase): 

144 """write_archive(obj, path) dispatches by extension and round-trips.""" 

145 

146 def setUp(self) -> None: 

147 tmp = tempfile.TemporaryDirectory() 

148 self.addCleanup(tmp.cleanup) 

149 self.tmp = tmp.name 

150 self.image = Image(np.arange(16, dtype=np.float32).reshape(4, 4), bbox=Box.factory[0:4, 0:4]) 

151 

152 def test_round_trip_fits(self) -> None: 

153 path = os.path.join(self.tmp, "x.fits") 

154 write_archive(self.image, path) 

155 result = read_archive(path) 

156 self.assertIsInstance(result, Image) 

157 np.testing.assert_array_equal(result.array, self.image.array) 

158 

159 def test_round_trip_json(self) -> None: 

160 path = os.path.join(self.tmp, "x.json") 

161 write_archive(self.image, path) 

162 result = read_archive(path) 

163 self.assertIsInstance(result, Image) 

164 np.testing.assert_array_equal(result.array, self.image.array) 

165 

166 @unittest.skipUnless(H5PY_AVAILABLE, "h5py not available.") 

167 def test_round_trip_ndf(self) -> None: 

168 path = os.path.join(self.tmp, "x.sdf") 

169 write_archive(self.image, path) 

170 result = read_archive(path) 

171 self.assertIsInstance(result, Image) 

172 np.testing.assert_array_equal(result.array, self.image.array) 

173 

174 

175class GenericReadKwargsTestCase(unittest.TestCase): 

176 """**kwargs forwarded by read_archive() reach the backend deserialize.""" 

177 

178 def setUp(self) -> None: 

179 tmp = tempfile.TemporaryDirectory() 

180 self.addCleanup(tmp.cleanup) 

181 self.tmp = tmp.name 

182 

183 def test_bbox_subset_fits(self) -> None: 

184 img = Image(np.arange(64, dtype=np.float32).reshape(8, 8), bbox=Box.factory[0:8, 0:8]) 

185 path = os.path.join(self.tmp, "x.fits") 

186 write_archive(img, path) 

187 # Read a 4x4 subset. bbox is the FITS-specific kwarg understood 

188 # by Image.deserialize; the generic read must forward it. 

189 sub = read_archive(path, bbox=Box.factory[2:6, 2:6]) 

190 self.assertEqual(sub.array.shape, (4, 4)) 

191 np.testing.assert_array_equal(sub.array, img.array[2:6, 2:6]) 

192 

193 

194class ReadClsTestCase(unittest.TestCase): 

195 """read_archive(path, cls=...) validates the deserialized type.""" 

196 

197 def test_read_cls_match(self) -> None: 

198 path = os.path.join(DATA_DIR, "image.json") 

199 result = read_archive(path, cls=Image) 

200 self.assertIsInstance(result, Image) 

201 

202 def test_read_cls_mismatch_raises(self) -> None: 

203 from lsst.images import Mask 

204 

205 path = os.path.join(DATA_DIR, "image.json") 

206 with self.assertRaises(TypeError) as ctx: 

207 read_archive(path, cls=Mask) 

208 msg = str(ctx.exception) 

209 self.assertIn("image", msg) # path / schema name 

210 self.assertIn("Image", msg) # actual deserialized type 

211 self.assertIn("Mask", msg) # requested cls 

212 

213 

214if __name__ == "__main__": 

215 unittest.main()