Coverage for tests/test_formatter_cache.py: 98%
94 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 08:46 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 08:46 +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
13import os
14import unittest
15import unittest.mock
17import numpy as np
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
24try:
25 # The formatter module requires lsst.daf.butler.
26 from lsst.images.formatters import GenericFormatter, _TreeCache
28 HAVE_BUTLER = True
29except ImportError:
30 HAVE_BUTLER = False
32DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1")
35def _count_ser_opens() -> unittest.mock._patch:
36 """Patch lsst.images.serialization.open with a counting wrapper.
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)
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."""
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)
54 @staticmethod
55 def _reset_cache() -> None:
56 GenericFormatter._tree_cache.value = _TreeCache()
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)
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)
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)
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)
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)
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)
137if __name__ == "__main__":
138 unittest.main()