Coverage for tests/test_formatter_cache.py: 98%
93 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 09:35 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-09 09:35 +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_archive, read_archive
21from lsst.images.tests import TemporaryButler
23try:
24 # The formatter module requires lsst.daf.butler.
25 from lsst.images.formatters import GenericFormatter, _TreeCache
27 HAVE_BUTLER = True
28except ImportError:
29 HAVE_BUTLER = False
31DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "schema_v1")
34def _count_ser_opens() -> unittest.mock._patch:
35 """Patch lsst.images.serialization.open_archive with a counting wrapper.
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)
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."""
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)
53 @staticmethod
54 def _reset_cache() -> None:
55 GenericFormatter._tree_cache.value = _TreeCache()
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)
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)
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)
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)
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)
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)
136if __name__ == "__main__":
137 unittest.main()