Coverage for tests/test_formatter_cache.py: 98%

93 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 unittest 

15import unittest.mock 

16 

17import numpy as np 

18 

19from lsst.images import Box 

20from lsst.images.serialization import open_archive, read_archive 

21from lsst.images.tests import TemporaryButler 

22 

23try: 

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

25 from lsst.images.formatters import GenericFormatter, _TreeCache 

26 

27 HAVE_BUTLER = True 

28except ImportError: 

29 HAVE_BUTLER = False 

30 

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

32 

33 

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

35 """Patch lsst.images.serialization.open_archive with a counting wrapper. 

36 

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

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

39 while delegating to the real implementation. 

40 """ 

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

42 

43 

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

45class FormatterComponentCacheTestCase(unittest.TestCase): 

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

47 

48 def setUp(self) -> None: 

49 self.visit_image = read_archive(os.path.join(DATA_DIR, "visit_image.json")) 

50 self._reset_cache() 

51 self.addCleanup(self._reset_cache) 

52 

53 @staticmethod 

54 def _reset_cache() -> None: 

55 GenericFormatter._tree_cache.value = _TreeCache() 

56 

57 def test_free_component_reads_share_one_open(self) -> None: 

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

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

60 self._reset_cache() 

61 with _count_ser_opens() as mocked: 

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

63 self.assertEqual(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 self.assertEqual(mocked.call_count, 1) 

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

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

69 self.assertIsNotNone(sky_projection) 

70 

71 def test_cached_components_are_independent(self) -> None: 

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

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

74 self._reset_cache() 

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

76 first = helper.butler.get(ref) 

77 second = helper.butler.get(ref) 

78 self.assertEqual(first, second) 

79 self.assertIsNot(first, second) 

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

81 first.zeroPoint = -100.0 

82 third = helper.butler.get(ref) 

83 self.assertEqual(third, second) 

84 self.assertNotEqual(third, first) 

85 

86 def test_pixel_component_falls_back_to_file(self) -> None: 

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

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

89 self._reset_cache() 

90 with _count_ser_opens() as mocked: 

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

92 self.assertEqual(mocked.call_count, 1) 

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

94 self.assertEqual(mocked.call_count, 2) 

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

96 

97 def test_parameterized_read_bypasses_cache(self) -> None: 

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

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

100 self._reset_cache() 

101 bbox = self.visit_image.bbox 

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

103 with _count_ser_opens() as mocked: 

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

105 self.assertEqual(mocked.call_count, 1) 

106 cutout = helper.butler.get( 

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

108 ) 

109 self.assertEqual(mocked.call_count, 2) 

110 self.assertEqual(cutout.bbox, cutout_box) 

111 

112 def test_full_read_populates_cache(self) -> None: 

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

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

115 self._reset_cache() 

116 helper.butler.get(helper.visit_image) 

117 with _count_ser_opens() as mocked: 

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

119 self.assertEqual(mocked.call_count, 0) 

120 

121 def test_cache_invalidated_across_datasets(self) -> None: 

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

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

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

125 self._reset_cache() 

126 with _count_ser_opens() as mocked: 

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

128 self.assertEqual(mocked.call_count, 1) 

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

130 self.assertEqual(mocked.call_count, 2) 

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

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

133 self.assertEqual(mocked.call_count, 3) 

134 

135 

136if __name__ == "__main__": 

137 unittest.main()