Coverage for tests/test_formatter_cache.py: 98%

94 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-01 09:12 +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 unittest 

15import unittest.mock 

16 

17import numpy as np 

18 

19from lsst.images import Box 

20from lsst.images.serialization import open as real_ser_open 

21from lsst.images.serialization import read 

22from lsst.images.tests import TemporaryButler 

23 

24try: 

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

26 from lsst.images.formatters import GenericFormatter, _TreeCache 

27 

28 HAVE_BUTLER = True 

29except ImportError: 

30 HAVE_BUTLER = False 

31 

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

33 

34 

35def _count_ser_opens() -> unittest.mock._patch: 

36 """Patch lsst.images.serialization.open with a counting wrapper. 

37 

38 The formatter resolves ``ser.open`` through the module object at call 

39 time, so patching the module attribute counts the opens it performs 

40 while delegating to the real implementation. 

41 """ 

42 return unittest.mock.patch("lsst.images.serialization.open", side_effect=real_ser_open) 

43 

44 

45@unittest.skipUnless(HAVE_BUTLER, "lsst.daf.butler could not be imported.") 

46class FormatterComponentCacheTestCase(unittest.TestCase): 

47 """Component reads through the butler reuse the cached tree.""" 

48 

49 def setUp(self) -> None: 

50 self.visit_image = read(os.path.join(DATA_DIR, "visit_image.json")) 

51 self._reset_cache() 

52 self.addCleanup(self._reset_cache) 

53 

54 @staticmethod 

55 def _reset_cache() -> None: 

56 GenericFormatter._tree_cache.value = _TreeCache() 

57 

58 def test_free_component_reads_share_one_open(self) -> None: 

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

60 helper.butler.put(self.visit_image, helper.visit_image) 

61 self._reset_cache() 

62 with _count_ser_opens() as mocked: 

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

64 self.assertEqual(mocked.call_count, 1) 

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

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

67 self.assertEqual(mocked.call_count, 1) 

68 self.assertEqual(summary_stats, self.visit_image.summary_stats) 

69 self.assertEqual(obs_info, self.visit_image.obs_info) 

70 self.assertIsNotNone(sky_projection) 

71 

72 def test_cached_components_are_independent(self) -> None: 

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

74 helper.butler.put(self.visit_image, helper.visit_image) 

75 self._reset_cache() 

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

77 first = helper.butler.get(ref) 

78 second = helper.butler.get(ref) 

79 self.assertEqual(first, second) 

80 self.assertIsNot(first, second) 

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

82 first.zeroPoint = -100.0 

83 third = helper.butler.get(ref) 

84 self.assertEqual(third, second) 

85 self.assertNotEqual(third, first) 

86 

87 def test_pixel_component_falls_back_to_file(self) -> None: 

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

89 helper.butler.put(self.visit_image, helper.visit_image) 

90 self._reset_cache() 

91 with _count_ser_opens() as mocked: 

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

93 self.assertEqual(mocked.call_count, 1) 

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

95 self.assertEqual(mocked.call_count, 2) 

96 np.testing.assert_array_equal(image.array, self.visit_image.image.array) 

97 

98 def test_parameterized_read_bypasses_cache(self) -> None: 

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

100 helper.butler.put(self.visit_image, helper.visit_image) 

101 self._reset_cache() 

102 bbox = self.visit_image.bbox 

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

104 with _count_ser_opens() as mocked: 

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

106 self.assertEqual(mocked.call_count, 1) 

107 cutout = helper.butler.get( 

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

109 ) 

110 self.assertEqual(mocked.call_count, 2) 

111 self.assertEqual(cutout.bbox, cutout_box) 

112 

113 def test_full_read_populates_cache(self) -> None: 

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

115 helper.butler.put(self.visit_image, helper.visit_image) 

116 self._reset_cache() 

117 helper.butler.get(helper.visit_image) 

118 with _count_ser_opens() as mocked: 

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

120 self.assertEqual(mocked.call_count, 0) 

121 

122 def test_cache_invalidated_across_datasets(self) -> None: 

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

124 helper.butler.put(self.visit_image, helper.vi_a) 

125 helper.butler.put(self.visit_image, helper.vi_b) 

126 self._reset_cache() 

127 with _count_ser_opens() as mocked: 

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

129 self.assertEqual(mocked.call_count, 1) 

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

131 self.assertEqual(mocked.call_count, 2) 

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

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

134 self.assertEqual(mocked.call_count, 3) 

135 

136 

137if __name__ == "__main__": 

138 unittest.main()