Coverage for tests/test_formatter_cache.py: 98%

102 statements  

« 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 

12 

13import os 

14 

15import numpy as np 

16import pytest 

17 

18from lsst.images import Box, VisitImage 

19from lsst.images.serialization import open_archive, read_archive 

20from lsst.images.tests import TemporaryButler 

21 

22try: 

23 # The formatter module requires lsst.daf.butler. 

24 from lsst.images.formatters import GenericFormatter, _TreeCache 

25 

26 HAVE_BUTLER = True 

27except ImportError: 

28 HAVE_BUTLER = False 

29 

30skip_no_butler = pytest.mark.skipif(not HAVE_BUTLER, reason="lsst.daf.butler could not be imported.") 

31 

32LOCAL_DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1") 

33 

34 

35def _count_ser_opens(): 

36 """Return a patch that counts calls to lsst.images.serialization.open.""" 

37 import unittest.mock 

38 

39 return unittest.mock.patch("lsst.images.serialization.open_archive", side_effect=open_archive) 

40 

41 

42def _reset_cache() -> None: 

43 """Reset the GenericFormatter tree cache to an empty state.""" 

44 GenericFormatter._tree_cache.value = _TreeCache() 

45 

46 

47@pytest.fixture(scope="session") 

48def visit_image() -> VisitImage: 

49 """Return a test VisitImage.""" 

50 return read_archive(os.path.join(LOCAL_DATA_DIR, "visit_image.json")) 

51 

52 

53@skip_no_butler 

54def test_free_component_reads_share_one_open(visit_image: VisitImage) -> None: 

55 """Verify multiple free component reads share a single 

56 serialization.open_archive call. 

57 """ 

58 with TemporaryButler(visit_image="VisitImage") as helper: 

59 helper.butler.put(visit_image, helper.visit_image) 

60 _reset_cache() 

61 with _count_ser_opens() as mocked: 

62 summary_stats = helper.butler.get(helper.visit_image.makeComponentRef("summary_stats")) 

63 assert mocked.call_count == 1 

64 obs_info = helper.butler.get(helper.visit_image.makeComponentRef("obs_info")) 

65 sky_projection = helper.butler.get(helper.visit_image.makeComponentRef("sky_projection")) 

66 assert mocked.call_count == 1 

67 assert summary_stats == visit_image.summary_stats 

68 assert obs_info == visit_image.obs_info 

69 assert sky_projection is not None 

70 _reset_cache() 

71 

72 

73@skip_no_butler 

74def test_cached_components_are_independent(visit_image: VisitImage) -> None: 

75 """Verify repeated component reads return equal but independent objects.""" 

76 with TemporaryButler(visit_image="VisitImage") as helper: 

77 helper.butler.put(visit_image, helper.visit_image) 

78 _reset_cache() 

79 ref = helper.visit_image.makeComponentRef("summary_stats") 

80 first = helper.butler.get(ref) 

81 second = helper.butler.get(ref) 

82 assert first == second 

83 assert first is not second 

84 # Mutating one result must not leak into later reads. 

85 first.zeroPoint = -100.0 

86 third = helper.butler.get(ref) 

87 assert third == second 

88 assert third != first 

89 _reset_cache() 

90 

91 

92@skip_no_butler 

93def test_pixel_component_falls_back_to_file(visit_image: VisitImage) -> None: 

94 """Verify pixel component reads bypass the cache and re-open the file.""" 

95 with TemporaryButler(visit_image="VisitImage") as helper: 

96 helper.butler.put(visit_image, helper.visit_image) 

97 _reset_cache() 

98 with _count_ser_opens() as mocked: 

99 helper.butler.get(helper.visit_image.makeComponentRef("summary_stats")) 

100 assert mocked.call_count == 1 

101 image = helper.butler.get(helper.visit_image.makeComponentRef("image")) 

102 assert mocked.call_count == 2 

103 np.testing.assert_array_equal(image.array, visit_image.image.array) 

104 _reset_cache() 

105 

106 

107@skip_no_butler 

108def test_parameterized_read_bypasses_cache(visit_image: VisitImage) -> None: 

109 """Verify a parameterized (bbox) component read bypasses the cache.""" 

110 with TemporaryButler(visit_image="VisitImage") as helper: 

111 helper.butler.put(visit_image, helper.visit_image) 

112 _reset_cache() 

113 bbox = visit_image.bbox 

114 cutout_box = Box.factory[bbox.y.start : bbox.y.start + 2, bbox.x.start : bbox.x.start + 2] 

115 with _count_ser_opens() as mocked: 

116 helper.butler.get(helper.visit_image.makeComponentRef("summary_stats")) 

117 assert mocked.call_count == 1 

118 cutout = helper.butler.get( 

119 helper.visit_image.makeComponentRef("image"), parameters={"bbox": cutout_box} 

120 ) 

121 assert mocked.call_count == 2 

122 assert cutout.bbox == cutout_box 

123 _reset_cache() 

124 

125 

126@skip_no_butler 

127def test_full_read_populates_cache(visit_image: VisitImage) -> None: 

128 """Verify a full object read populates the cache for subsequent component 

129 reads. 

130 """ 

131 with TemporaryButler(visit_image="VisitImage") as helper: 

132 helper.butler.put(visit_image, helper.visit_image) 

133 _reset_cache() 

134 helper.butler.get(helper.visit_image) 

135 with _count_ser_opens() as mocked: 

136 helper.butler.get(helper.visit_image.makeComponentRef("obs_info")) 

137 assert mocked.call_count == 0 

138 _reset_cache() 

139 

140 

141@skip_no_butler 

142def test_cache_invalidated_across_datasets(visit_image: VisitImage) -> None: 

143 """Verify the cache is invalidated when switching between dataset 

144 references. 

145 """ 

146 with TemporaryButler(vi_a="VisitImage", vi_b="VisitImage") as helper: 

147 helper.butler.put(visit_image, helper.vi_a) 

148 helper.butler.put(visit_image, helper.vi_b) 

149 _reset_cache() 

150 with _count_ser_opens() as mocked: 

151 helper.butler.get(helper.vi_a.makeComponentRef("summary_stats")) 

152 assert mocked.call_count == 1 

153 helper.butler.get(helper.vi_b.makeComponentRef("summary_stats")) 

154 assert mocked.call_count == 2 

155 # vi_b evicted vi_a, so reading vi_a again reopens its file. 

156 helper.butler.get(helper.vi_a.makeComponentRef("obs_info")) 

157 assert mocked.call_count == 3 

158 _reset_cache()