Coverage for tests/test_serialization_io.py: 30%

115 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-12 07:51 +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. 

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, write 

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() API, keyed by the fixture's file name. These are pinned here rather 

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

42# externally-observable type instead of re-running read()'s own lookup against 

43# 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(path) dispatches by extension and produces the registered type.""" 

72 

73 def test_visit_image_json(self) -> None: 

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

75 result = read(path) 

76 self.assertIsInstance(result, VisitImage) 

77 

78 def test_image_json(self) -> None: 

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

80 result = read(path) 

81 self.assertIsInstance(result, Image) 

82 

83 

84class GenericReadErrorsTestCase(unittest.TestCase): 

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

86 

87 def setUp(self) -> None: 

88 tmp = tempfile.TemporaryDirectory() 

89 self.addCleanup(tmp.cleanup) 

90 self.tmp = tmp.name 

91 

92 def test_unsupported_extension(self) -> None: 

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

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

95 f.write("nope") 

96 # backend_for_path raises ValueError; read() must let it through. 

97 with self.assertRaises(ValueError): 

98 read(path) 

99 

100 def test_unregistered_schema(self) -> None: 

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

102 # registry. 

103 path = os.path.join(self.tmp, "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 self.assertRaises(ArchiveReadError) as ctx: 

110 read(path) 

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

112 

113 

114class FixtureSweepTestCase(unittest.TestCase): 

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

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

117 """ 

118 

119 def test_sweep(self) -> None: 

120 # piff is an optional dependency that PiffSerializationModel 

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

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

123 seen = set() 

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

125 for root in roots: 

126 if not os.path.isdir(root): 

127 continue 

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

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

130 continue 

131 with self.subTest(entry=entry): 

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

133 result = read(os.path.join(root, entry)) 

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

135 seen.add(entry) 

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

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

138 

139 

140class GenericWriteRoundTripTestCase(unittest.TestCase): 

141 """write(obj, path) dispatches by extension and round-trips.""" 

142 

143 def setUp(self) -> None: 

144 tmp = tempfile.TemporaryDirectory() 

145 self.addCleanup(tmp.cleanup) 

146 self.tmp = tmp.name 

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

148 

149 def test_round_trip_fits(self) -> None: 

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

151 write(self.image, path) 

152 result = read(path) 

153 self.assertIsInstance(result, Image) 

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

155 

156 def test_round_trip_json(self) -> None: 

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

158 write(self.image, path) 

159 result = read(path) 

160 self.assertIsInstance(result, Image) 

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

162 

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

164 def test_round_trip_ndf(self) -> None: 

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

166 write(self.image, path) 

167 result = read(path) 

168 self.assertIsInstance(result, Image) 

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

170 

171 

172class GenericReadKwargsTestCase(unittest.TestCase): 

173 """**kwargs forwarded by read() reach the backend deserialize.""" 

174 

175 def setUp(self) -> None: 

176 tmp = tempfile.TemporaryDirectory() 

177 self.addCleanup(tmp.cleanup) 

178 self.tmp = tmp.name 

179 

180 def test_bbox_subset_fits(self) -> None: 

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

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

183 write(img, path) 

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

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

186 sub = read(path, bbox=Box.factory[2:6, 2:6]) 

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

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

189 

190 

191class ReadClsTestCase(unittest.TestCase): 

192 """read(path, cls=...) validates the deserialized type.""" 

193 

194 def test_read_cls_match(self) -> None: 

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

196 result = read(path, cls=Image) 

197 self.assertIsInstance(result, Image) 

198 

199 def test_read_cls_mismatch_raises(self) -> None: 

200 from lsst.images import Mask 

201 

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

203 with self.assertRaises(TypeError) as ctx: 

204 read(path, cls=Mask) 

205 msg = str(ctx.exception) 

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

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

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

209 

210 

211if __name__ == "__main__": 

212 unittest.main()