Coverage for tests / test_apdbSql.py: 49%
167 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 08:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-22 08:49 +0000
1# This file is part of dax_apdb.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""Unit test for Apdb class."""
24import gc
25import os
26import shutil
27import tempfile
28import unittest
29from typing import Any
30from unittest.mock import patch
32import sqlalchemy
34import lsst.utils.tests
35from lsst.dax.apdb import Apdb, ApdbConfig, ApdbReplica, ApdbTables, ApdbUpdateRecord, ReplicaChunk
36from lsst.dax.apdb.pixelization import Pixelization
37from lsst.dax.apdb.sql import ApdbSql, ApdbSqlConfig
38from lsst.dax.apdb.tests import ApdbSchemaUpdateTest, ApdbTest
40try:
41 import testing.postgresql
42except ImportError:
43 testing = None
45TEST_SCHEMA = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config/schema-apdb.yaml")
46TEST_SCHEMA_SSO = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config/schema-sso.yaml")
47# Schema that uses `datetime` for timestamps and combines APDB and SSP.
48TEST_SCHEMA_DT = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config/schema-datetime.yaml")
51class ApdbSQLTest(ApdbTest):
52 """A common base class for SQL APDB tests."""
54 dia_object_index: str
56 def make_instance(self, **kwargs: Any) -> ApdbConfig:
57 """Create database and return its config."""
58 kw = {
59 "schema_file": TEST_SCHEMA,
60 "ss_schema_file": TEST_SCHEMA_SSO,
61 "dia_object_index": self.dia_object_index,
62 "enable_replica": self.enable_replica,
63 }
64 kw.update(kwargs)
65 return ApdbSql.init_database(**kw) # type: ignore[arg-type]
67 def getDiaObjects_table(self) -> ApdbTables:
68 """Return type of table returned from getDiaObjects method."""
69 return ApdbTables.DiaObject
71 def pixelization(self, config: ApdbConfig) -> Pixelization:
72 """Return pixelization used by implementation."""
73 assert isinstance(config, ApdbSqlConfig), "Only expect ApdbSqlConfig here"
74 return Pixelization("htm", config.pixelization.htm_level, config.pixelization.htm_max_ranges)
76 def test_connection_timeout(self) -> None:
77 """Test that setting connection timeout does not break things."""
78 config = self.make_instance()
79 assert isinstance(config, ApdbSqlConfig), "Only expect ApdbSqlConfig here"
80 config.connection_config.connection_timeout = 60.0
81 Apdb.from_config(config)
83 def store_update_records(self, apdb: Apdb, records: list[ApdbUpdateRecord], chunk: ReplicaChunk) -> None:
84 # Docstring inherited.
85 assert isinstance(apdb, ApdbSql), "Expecting ApdbSql instance"
86 apdb._storeUpdateRecords(records, chunk, store_chunk=True)
89class ApdbSQLiteTestCase(ApdbSQLTest, unittest.TestCase):
90 """A test case for ApdbSql class using SQLite backend."""
92 fsrc_requires_id_list = True
93 dia_object_index = "baseline"
94 use_mjd = True
96 def setUp(self) -> None:
97 self.tempdir = tempfile.mkdtemp()
98 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
100 def tearDown(self) -> None:
101 shutil.rmtree(self.tempdir, ignore_errors=True)
103 def make_instance(self, **kwargs: Any) -> ApdbConfig:
104 return super().make_instance(db_url=self.db_url, **kwargs)
107class ApdbSQLiteDatetimeTestCase(ApdbSQLTest, unittest.TestCase):
108 """A test case for ApdbSql class using SQLite backend and schema with
109 timestamps in TAI MJD.
110 """
112 fsrc_requires_id_list = True
113 dia_object_index = "baseline"
114 use_mjd = False
116 def setUp(self) -> None:
117 self.tempdir = tempfile.mkdtemp()
118 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
120 def tearDown(self) -> None:
121 shutil.rmtree(self.tempdir, ignore_errors=True)
123 def make_instance(self, **kwargs: Any) -> ApdbConfig:
124 if "schema_file" in kwargs:
125 return super().make_instance(db_url=self.db_url, **kwargs)
126 else:
127 return super().make_instance(db_url=self.db_url, schema_file=TEST_SCHEMA_DT, **kwargs)
130class ApdbSQLiteTestCaseLastObject(ApdbSQLiteTestCase):
131 """A test case for ApdbSql class using SQLite backend and DiaObjectLast
132 table.
133 """
135 dia_object_index = "last_object_table"
137 def getDiaObjects_table(self) -> ApdbTables:
138 """Return type of table returned from getDiaObjects method."""
139 return ApdbTables.DiaObjectLast
142class ApdbSQLiteTestCasePixIdIovIndex(ApdbSQLiteTestCase):
143 """A test case for ApdbSql class using SQLite backend with pix_id_iov
144 indexing.
145 """
147 dia_object_index = "pix_id_iov"
150class ApdbSQLiteTestCaseReplica(ApdbSQLiteTestCase):
151 """Test case for ApdbSql class using SQLite backend with replica tables."""
153 enable_replica = True
154 meta_row_count = 4
157@unittest.skipUnless(testing is not None, "testing.postgresql module not found")
158class ApdbPostgresTestCase(ApdbSQLTest, unittest.TestCase):
159 """A test case for ApdbSql class using Postgres backend."""
161 fsrc_requires_id_list = True
162 dia_object_index = "last_object_table"
163 postgresql: Any
164 enable_replica = True
165 meta_row_count = 4
166 timestamp_type_name = "datetime64[ns]"
168 @classmethod
169 def setUpClass(cls) -> None:
170 # Create the postgres test server.
171 cls.postgresql = testing.postgresql.PostgresqlFactory(cache_initialized_db=True)
172 super().setUpClass()
174 @classmethod
175 def tearDownClass(cls) -> None:
176 # Clean up any lingering SQLAlchemy engines/connections
177 # so they're closed before we shut down the server.
178 gc.collect()
179 cls.postgresql.clear_cache()
180 super().tearDownClass()
182 def setUp(self) -> None:
183 self.server = self.postgresql()
185 def tearDown(self) -> None:
186 self.server = self.postgresql()
188 def make_instance(self, **kwargs: Any) -> ApdbConfig:
189 return super().make_instance(db_url=self.server.url(), **kwargs)
191 def getDiaObjects_table(self) -> ApdbTables:
192 """Return type of table returned from getDiaObjects method."""
193 return ApdbTables.DiaObjectLast
196@unittest.skipUnless(testing is not None, "testing.postgresql module not found")
197class ApdbPostgresNamespaceTestCase(ApdbPostgresTestCase):
198 """A test case for ApdbSql class using Postgres backend with schema name"""
200 # use mixed case to trigger quoting
201 namespace = "ApdbSchema"
203 def make_instance(self, **kwargs: Any) -> ApdbConfig:
204 """Make config class instance used in all tests."""
205 return super().make_instance(namespace=self.namespace, **kwargs)
208class ApdbSchemaUpdateSQLiteTestCase(ApdbSchemaUpdateTest, unittest.TestCase):
209 """A test case for schema updates using SQLite backend."""
211 def setUp(self) -> None:
212 self.tempdir = tempfile.mkdtemp()
213 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
215 def tearDown(self) -> None:
216 shutil.rmtree(self.tempdir, ignore_errors=True)
218 def make_instance(self, **kwargs: Any) -> ApdbConfig:
219 """Make config class instance used in all tests."""
220 kw = {
221 "db_url": self.db_url,
222 "schema_file": TEST_SCHEMA,
223 "ss_schema_file": TEST_SCHEMA_SSO,
224 }
225 kw.update(kwargs)
226 return ApdbSql.init_database(**kw) # type: ignore[arg-type]
229class ApdbSQLiteFromUriTestCase(unittest.TestCase):
230 """A test case for for instantiating ApdbSql via URI."""
232 def setUp(self) -> None:
233 self.tempdir = tempfile.mkdtemp()
234 self.addCleanup(shutil.rmtree, self.tempdir, ignore_errors=True)
235 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
236 config = ApdbSql.init_database(
237 db_url=self.db_url, schema_file=TEST_SCHEMA, ss_schema_file=TEST_SCHEMA_SSO
238 )
239 # TODO: This will need update when we switch to pydantic configs.
240 self.config_path = os.path.join(self.tempdir, "apdb-config.yaml")
241 config.save(self.config_path)
242 self.bad_config_path = os.path.join(self.tempdir, "not-config.yaml")
243 self.index_path = os.path.join(self.tempdir, "apdb-index.yaml")
244 with open(self.index_path, "w") as index_file:
245 print(f'label1: "{self.config_path}"', file=index_file)
246 print(f'"label2/pex_config": "{self.config_path}"', file=index_file)
247 print(f'bad-label: "{self.bad_config_path}"', file=index_file)
248 # File with incorrect format.
249 self.bad_index_path = os.path.join(self.tempdir, "apdb-index-bad.yaml")
250 with open(self.bad_index_path, "w") as index_file:
251 print(f'label1: ["{self.config_path}"]', file=index_file)
252 self.missing_index_path = os.path.join(self.tempdir, "no-apdb-index.yaml")
254 def test_make_apdb_from_path(self) -> None:
255 """Check that we can make APDB instance from config URI."""
256 Apdb.from_uri(self.config_path)
257 with self.assertRaises(FileNotFoundError):
258 Apdb.from_uri(self.bad_config_path)
260 def test_make_apdb_from_labels(self) -> None:
261 """Check that we can make APDB instance from config URI."""
262 # Replace DAX_APDB_INDEX_URI value
263 new_env = {"DAX_APDB_INDEX_URI": self.index_path}
264 with patch.dict(os.environ, new_env, clear=True):
265 Apdb.from_uri("label:label1")
266 Apdb.from_uri("label:label2")
267 # Label does not exist.
268 with self.assertRaises(ValueError):
269 Apdb.from_uri("label:not-a-label")
270 # Label exists but points to a missing config.
271 with self.assertRaises(FileNotFoundError):
272 Apdb.from_uri("label:bad-label")
274 def test_make_apdb_bad_index(self) -> None:
275 """Check what happens when DAX_APDB_INDEX_URI is broken."""
276 # envvar is set but empty.
277 new_env = {"DAX_APDB_INDEX_URI": ""}
278 with patch.dict(os.environ, new_env, clear=True):
279 with self.assertRaises(RuntimeError):
280 Apdb.from_uri("label:label")
282 # envvar is set to something non-existing.
283 new_env = {"DAX_APDB_INDEX_URI": self.missing_index_path}
284 with patch.dict(os.environ, new_env, clear=True):
285 with self.assertRaises(FileNotFoundError):
286 Apdb.from_uri("label:label")
288 # envvar points to an incorrect file.
289 new_env = {"DAX_APDB_INDEX_URI": self.bad_index_path}
290 with patch.dict(os.environ, new_env, clear=True):
291 with self.assertRaises(TypeError):
292 Apdb.from_uri("label:label")
294 def test_remove_database_file(self) -> None:
295 """Check that SQLite does not try to recreate database after it was
296 removed, but config persisted.
297 """
298 os.unlink(f"{self.tempdir}/apdb.sqlite3")
299 with self.assertRaisesRegex(sqlalchemy.exc.OperationalError, "unable to open database file"):
300 Apdb.from_uri(self.config_path)
302 def test_make_apdb_replica(self) -> None:
303 """Check that we can make ApdbReplica instance from config URI."""
304 ApdbReplica.from_uri(self.config_path)
305 with self.assertRaises(FileNotFoundError):
306 Apdb.from_uri(self.bad_config_path)
309class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
310 """Run file leak tests."""
313def setup_module(module: Any) -> None:
314 """Configure pytest."""
315 lsst.utils.tests.init()
318if __name__ == "__main__":
319 lsst.utils.tests.init()
320 unittest.main()