Coverage for tests/test_tests_checks.py: 100%

187 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-09 10:54 +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. 

11 

12from __future__ import annotations 

13 

14import astropy.time 

15import astropy.units as u 

16import numpy as np 

17import pytest 

18from astropy.coordinates import SkyCoord 

19 

20from lsst.images import Box, Mask, MaskPlane, MaskSchema 

21from lsst.images.tests import annotate_errors, assert_masks_equal, assert_values_equal 

22 

23 

24def test_assert_values_equal_passes(): 

25 """Assert equal arrays pass.""" 

26 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2, 3])) 

27 

28 

29def test_assert_values_equal_nan_equal_by_default(): 

30 """Assert NaN compares equal by default.""" 

31 a = np.array([1.0, np.nan]) 

32 assert_values_equal(a, a.copy()) 

33 

34 

35def test_assert_values_equal_exact_by_default(): 

36 """Assert a small difference fails by default (exact equality).""" 

37 with pytest.raises(AssertionError, match="max abs diff"): 

38 assert_values_equal(np.array([1.0]), np.array([1.0 + 1e-6])) 

39 

40 

41def test_assert_values_equal_rtol_passes(): 

42 """Assert an explicit relative tolerance passes.""" 

43 assert_values_equal(np.array([1.0]), np.array([1.0 + 1e-6]), rtol=1e-5) 

44 

45 

46def test_assert_values_equal_fails_with_report(): 

47 """Assert a mismatch raises a report message.""" 

48 a = np.array([1.0, 2.0, 3.0]) 

49 b = np.array([1.0, 2.0, 4.0]) 

50 with pytest.raises(AssertionError, match="1/3 values differ"): 

51 assert_values_equal(a, b) 

52 

53 

54def test_assert_values_equal_label_prefix(): 

55 """Assert the label appears in the failure message.""" 

56 with pytest.raises(AssertionError, match=r"mask\[BAD\]: arrays are not equal"): 

57 assert_values_equal(np.array([True]), np.array([False]), label="mask[BAD]") 

58 

59 

60def test_assert_values_equal_shape_mismatch(): 

61 """Assert a shape mismatch raises.""" 

62 with pytest.raises(AssertionError, match="shape"): 

63 assert_values_equal(np.zeros((2, 2)), np.zeros((3, 3))) 

64 

65 

66def test_assert_values_equal_non_numeric_reports_indices(): 

67 """Assert non-numeric arrays report differing indices (via numpy).""" 

68 a = np.array(["a", "b", "c"]) 

69 b = np.array(["a", "b", "z"]) 

70 with pytest.raises(AssertionError, match=r"Mismatched elements: 1 / 3"): 

71 assert_values_equal(a, b) 

72 

73 

74def test_assert_values_equal_nan_not_equal_to_number(): 

75 """Assert NaN still does not equal a finite number.""" 

76 with pytest.raises(AssertionError): 

77 assert_values_equal(np.array([np.nan]), np.array([1.0])) 

78 

79 

80def test_assert_values_equal_atol_quantity_unit_aware(): 

81 """Assert a Quantity atol is converted to the unit of a.""" 

82 a = np.array([1.0, 2.0]) * u.deg 

83 b = a.to(u.arcsec) 

84 assert_values_equal(a, b, rtol=0.0, atol=1e-7 * u.arcsec) 

85 

86 

87def test_assert_values_equal_unit_aware(): 

88 """Assert unit-aware comparison converts b to the unit of a.""" 

89 a = np.array([1.0, 2.0]) * u.deg 

90 b = a.to(u.arcsec) 

91 assert_values_equal(a, b, rtol=1e-5, atol=0.0) 

92 

93 

94def test_assert_values_equal_incompatible_units_raise(): 

95 """Assert incompatible units raise.""" 

96 with pytest.raises(u.UnitConversionError): 

97 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.s) 

98 

99 

100def test_assert_values_equal_time_within_atol(): 

101 """Assert times close enough (with atol in seconds) pass.""" 

102 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc") 

103 t1 = t0 + astropy.time.TimeDelta(1e-6, format="sec") 

104 assert_values_equal(t1, t0, atol=1e-5 * u.s) 

105 assert_values_equal(t1, t0, atol=astropy.time.TimeDelta(1e-5, format="sec")) 

106 with pytest.raises(u.UnitConversionError): 

107 assert_values_equal(t1, t0, atol=1e-5) 

108 

109 

110def test_assert_values_equal_time_exact_by_default(): 

111 """Assert times differing by any amount fail with zero tolerance.""" 

112 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc") 

113 t1 = t0 + astropy.time.TimeDelta(1e-6, format="sec") 

114 with pytest.raises(AssertionError, match="differ by"): 

115 assert_values_equal(t1, t0) 

116 

117 

118def test_assert_values_equal_time_quantity_atol(): 

119 """Assert a time-unit Quantity atol bounds the time difference.""" 

120 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc") 

121 t1 = t0 + astropy.time.TimeDelta(0.002, format="sec") 

122 assert_values_equal(t1, t0, atol=u.Quantity(3, "ms")) 

123 with pytest.raises(AssertionError, match="differ by"): 

124 assert_values_equal(t1, t0, atol=u.Quantity(1, "ms")) 

125 

126 

127def test_assert_values_equal_time_incompatible_atol_raises(): 

128 """Assert a non-time Quantity atol for times raises.""" 

129 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc") 

130 with pytest.raises(u.UnitConversionError): 

131 assert_values_equal(t0, t0, atol=u.Quantity(1.0, "m")) 

132 

133 

134def test_assert_values_equal_timedelta(): 

135 """Assert timedeltas compare by their difference in seconds.""" 

136 d0 = astropy.time.TimeDelta(30.0, format="sec") 

137 assert_values_equal(d0, astropy.time.TimeDelta(30.0, format="sec")) 

138 assert_values_equal(d0, astropy.time.TimeDelta(30.000001, format="sec"), atol=1e-5 * u.s) 

139 assert_values_equal( 

140 d0, astropy.time.TimeDelta(30.000001, format="sec"), atol=astropy.time.TimeDelta(1e-5, format="sec") 

141 ) 

142 with pytest.raises(AssertionError, match="differ by"): 

143 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec")) 

144 

145 

146def test_assert_values_equal_timedelta_rtol(): 

147 """Assert timedeltas accept a relative tolerance on their magnitude.""" 

148 d0 = astropy.time.TimeDelta(30.0, format="sec") 

149 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec"), rtol=0.1) 

150 with pytest.raises(AssertionError, match="differ by"): 

151 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec"), rtol=1e-6) 

152 

153 

154def test_assert_values_equal_nan_semantics(): 

155 """Assert NaN matches NaN only when equal_nan, for timedeltas too.""" 

156 d0 = astropy.time.TimeDelta([1.0, np.nan], format="sec") 

157 assert_values_equal(d0, d0.copy()) 

158 with pytest.raises(AssertionError, match="values differ"): 

159 assert_values_equal(d0, d0.copy(), equal_nan=False) 

160 with pytest.raises(AssertionError, match="values differ"): 

161 assert_values_equal(d0, astropy.time.TimeDelta([1.0, 2.0], format="sec")) 

162 

163 

164def test_assert_values_equal_mixed_operand_kinds_raise(): 

165 """Assert mixing operand kinds (bare numeric with unitful, or different 

166 astropy types) raises instead of comparing as objects. 

167 """ 

168 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc") 

169 d0 = astropy.time.TimeDelta(30.0, format="sec") 

170 with pytest.raises(TypeError, match="cannot compare Time with TimeDelta"): 

171 assert_values_equal(t0, d0) 

172 with pytest.raises(TypeError, match="cannot compare TimeDelta with Time"): 

173 assert_values_equal(d0, t0) 

174 with pytest.raises(TypeError, match="cannot compare Time with float"): 

175 assert_values_equal(t0, 1.0) 

176 with pytest.raises(u.UnitConversionError): 

177 assert_values_equal(1.0 * u.s, 1.0) 

178 with pytest.raises(u.UnitConversionError): 

179 assert_values_equal(1.0, 1.0 * u.s) 

180 with pytest.raises(TypeError, match="cannot compare SkyCoord with float"): 

181 assert_values_equal(SkyCoord(1.0, 2.0, unit="deg"), 1.0) 

182 

183 

184def test_assert_values_equal_exact_operands(): 

185 """Assert non-floating-point operands (ints, bools, strings) compare 

186 exactly and report mismatches through numpy.testing.assert_array_equal. 

187 """ 

188 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2, 3])) 

189 assert_values_equal(np.array([True, False]), np.array([True, False])) 

190 assert_values_equal(["a", "b"], ["a", "b"]) 

191 assert_values_equal("a", "a") 

192 with pytest.raises(AssertionError, match=r"Mismatched elements: 1 / 3"): 

193 assert_values_equal(np.array([1, 2, 3]), np.array([1, 5, 3])) 

194 with pytest.raises(AssertionError, match=r"ACTUAL: array\('a'"): 

195 assert_values_equal("a", "b") 

196 with pytest.raises(AssertionError, match=r"mask: arrays are not equal"): 

197 assert_values_equal(np.array([True, False]), np.array([True, True]), label="mask") 

198 with pytest.raises(AssertionError, match=r"shapes \(3,\), \(2,\) mismatch"): 

199 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2])) 

200 

201 

202def test_assert_values_equal_exact_operands_reject_tolerances(): 

203 """Assert rtol/atol on non-floating-point operands raise (they have no 

204 numeric tolerance semantics), while a unit-independent zero atol is 

205 still accepted. 

206 """ 

207 with pytest.raises(ValueError, match="floating-point"): 

208 assert_values_equal(np.array([1, 2]), np.array([1, 2]), rtol=1e-9) 

209 with pytest.raises(ValueError, match="floating-point"): 

210 assert_values_equal(["a"], ["a"], atol=1e-3) 

211 assert_values_equal(np.array([1, 2]), np.array([1, 2]), atol=0.0 * u.m) 

212 

213 

214def test_assert_values_equal_atol_units_must_match_operands(): 

215 """Assert atol units follow the operands: bare for unitless, Quantity 

216 (or exactly zero) for unitful. 

217 """ 

218 with pytest.raises(u.UnitConversionError): 

219 assert_values_equal(np.array([1.0]), np.array([1.0]), atol=1e-3 * u.m) 

220 with pytest.raises(u.UnitConversionError): 

221 assert_values_equal(1.0, 1.0, atol=astropy.time.TimeDelta(1e-3, format="sec")) 

222 with pytest.raises(u.UnitConversionError): 

223 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.deg, atol=1e-6) 

224 with pytest.raises(u.UnitConversionError): 

225 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.deg, atol=1e-6 * u.s) 

226 

227 

228def test_assert_values_equal_sky_coords_within_atol(): 

229 """Assert sky coordinates close enough (with an angular atol) pass.""" 

230 a = SkyCoord(10.0, 45.0, unit="deg") 

231 b = SkyCoord(10.0 + 1e-4, 45.0, unit="deg") 

232 assert_values_equal(a, b, atol=1 * u.arcsec) 

233 with pytest.raises(AssertionError, match=r"differ by .*arcsec"): 

234 assert_values_equal(a, b, atol=0.1 * u.arcsec) 

235 

236 

237def test_assert_values_equal_sky_coords_wrap(): 

238 """Assert separations across the RA = 0 meridian compare correctly.""" 

239 a = SkyCoord(359.9999, 30.0, unit="deg") 

240 b = SkyCoord(0.0001, 30.0, unit="deg") 

241 assert_values_equal(a, b, atol=1 * u.arcsec) 

242 

243 

244def test_assert_values_equal_sky_coords_exact_atol_zero(): 

245 """Assert bare atol=0.0 demands exact coincidence.""" 

246 a = SkyCoord(10.0, 45.0, unit="deg") 

247 assert_values_equal(a, SkyCoord(10.0, 45.0, unit="deg"), atol=0.0) 

248 with pytest.raises(AssertionError, match=r"differ by .*arcsec"): 

249 assert_values_equal(a, SkyCoord(10.0 + 1e-9, 45.0, unit="deg"), atol=0.0) 

250 

251 

252def test_assert_values_equal_sky_coords_unitless_atol_raises(): 

253 """Assert a nonzero bare-float atol for sky coords raises.""" 

254 a = SkyCoord(10.0, 45.0, unit="deg") 

255 with pytest.raises(u.UnitConversionError): 

256 assert_values_equal(a, a, atol=1e-3) 

257 with pytest.raises(ValueError, match="rtol"): 

258 assert_values_equal(a, a, rtol=1e-6) 

259 

260 

261def test_assert_values_equal_wrap_angles(): 

262 """Assert angles differing by whole turns compare equal.""" 

263 a = 359.9999 * u.deg 

264 b = 0.0001 * u.deg 

265 assert_values_equal(a, b, atol=1 * u.arcsec, wrap_angles=True) 

266 assert_values_equal(1 * u.deg, -359.0 * u.deg, atol=0.0, wrap_angles=True) 

267 with pytest.raises(AssertionError, match="!="): 

268 assert_values_equal(a, b, atol=0.1 * u.arcsec, wrap_angles=True) 

269 

270 

271def test_assert_values_equal_wrap_angles_requires_units(): 

272 """Assert wrap_angles demands angular units on operands and atol.""" 

273 with pytest.raises(u.UnitConversionError): 

274 assert_values_equal(1.0, 1.0, wrap_angles=True) 

275 with pytest.raises(u.UnitConversionError): 

276 assert_values_equal(1 * u.deg, 1 * u.deg, atol=1e-3, wrap_angles=True) 

277 with pytest.raises(ValueError, match="rtol"): 

278 assert_values_equal(1 * u.deg, 1 * u.deg, rtol=1e-6, wrap_angles=True) 

279 with pytest.raises(u.UnitConversionError): 

280 assert_values_equal(1 * u.s, 1 * u.s, wrap_angles=True) 

281 

282 

283def test_label_assertions_notes(): 

284 """Assert a failing check is re-raised with the label as a note.""" 

285 with pytest.raises(AssertionError) as exc_info: 

286 with annotate_errors("psf"): 

287 raise AssertionError("inner") 

288 assert exc_info.value.args == ("inner",) 

289 assert exc_info.value.__notes__ == ["psf"] 

290 

291 

292def test_label_assertions_passthrough_on_success(): 

293 """Assert a passing block is not re-raised.""" 

294 with annotate_errors("psf"): 

295 pass # must not raise 

296 

297 

298def test_assert_masks_equal_notes_plane_diff() -> None: 

299 """Assert that a mask mismatch adds a per-plane +/- note.""" 

300 schema = MaskSchema([MaskPlane("A", "dA")], dtype=np.uint8) 

301 m1 = Mask(0, schema=schema, bbox=Box.factory[0:2, 0:1]) 

302 m1.set("A", np.array([[True], [False]])) 

303 m2 = Mask(0, schema=schema, bbox=Box.factory[0:2, 0:1]) 

304 m2.set("A", np.array([[True], [True]])) 

305 with pytest.raises(AssertionError) as excinfo: 

306 assert_masks_equal(m1, m2) 

307 notes = excinfo.value.__notes__ or [] 

308 assert any("mask[A]: +0 -1" in n for n in notes)