Coverage for tests/test_apdbSql.py: 48%
163 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:35 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-10-09 03:35 -0700
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
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.yaml")
46# Schema that uses `datetime` for timestamps.
47TEST_SCHEMA_DT = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config/schema-datetime.yaml")
50class ApdbSQLTest(ApdbTest):
51 """A common base class for SQL APDB tests."""
53 dia_object_index: str
55 def make_instance(self, **kwargs: Any) -> ApdbConfig:
56 """Create database and return its config."""
57 kw = {
58 "schema_file": TEST_SCHEMA,
59 "dia_object_index": self.dia_object_index,
60 "enable_replica": self.enable_replica,
61 }
62 kw.update(kwargs)
63 return ApdbSql.init_database(**kw) # type: ignore[arg-type]
65 def getDiaObjects_table(self) -> ApdbTables:
66 """Return type of table returned from getDiaObjects method."""
67 return ApdbTables.DiaObject
69 def pixelization(self, config: ApdbConfig) -> Pixelization:
70 """Return pixelization used by implementation."""
71 assert isinstance(config, ApdbSqlConfig), "Only expect ApdbSqlConfig here"
72 return Pixelization("htm", config.pixelization.htm_level, config.pixelization.htm_max_ranges)
74 def test_connection_timeout(self) -> None:
75 """Test that setting connection timeout does not break things."""
76 config = self.make_instance()
77 assert isinstance(config, ApdbSqlConfig), "Only expect ApdbSqlConfig here"
78 config.connection_config.connection_timeout = 60.0
79 Apdb.from_config(config)
82class ApdbSQLiteTestCase(ApdbSQLTest, unittest.TestCase):
83 """A test case for ApdbSql class using SQLite backend."""
85 fsrc_requires_id_list = True
86 dia_object_index = "baseline"
87 use_mjd = True
89 def setUp(self) -> None:
90 self.tempdir = tempfile.mkdtemp()
91 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
93 def tearDown(self) -> None:
94 shutil.rmtree(self.tempdir, ignore_errors=True)
96 def make_instance(self, **kwargs: Any) -> ApdbConfig:
97 return super().make_instance(db_url=self.db_url, **kwargs)
100class ApdbSQLiteDatetimeTestCase(ApdbSQLTest, unittest.TestCase):
101 """A test case for ApdbSql class using SQLite backend and schema with
102 timestamps in TAI MJD.
103 """
105 fsrc_requires_id_list = True
106 dia_object_index = "baseline"
107 use_mjd = False
109 def setUp(self) -> None:
110 self.tempdir = tempfile.mkdtemp()
111 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
113 def tearDown(self) -> None:
114 shutil.rmtree(self.tempdir, ignore_errors=True)
116 def make_instance(self, **kwargs: Any) -> ApdbConfig:
117 if "schema_file" in kwargs:
118 return super().make_instance(db_url=self.db_url, **kwargs)
119 else:
120 return super().make_instance(db_url=self.db_url, schema_file=TEST_SCHEMA_DT, **kwargs)
123class ApdbSQLiteTestCaseLastObject(ApdbSQLiteTestCase):
124 """A test case for ApdbSql class using SQLite backend and DiaObjectLast
125 table.
126 """
128 dia_object_index = "last_object_table"
130 def getDiaObjects_table(self) -> ApdbTables:
131 """Return type of table returned from getDiaObjects method."""
132 return ApdbTables.DiaObjectLast
135class ApdbSQLiteTestCasePixIdIovIndex(ApdbSQLiteTestCase):
136 """A test case for ApdbSql class using SQLite backend with pix_id_iov
137 indexing.
138 """
140 dia_object_index = "pix_id_iov"
143class ApdbSQLiteTestCaseReplica(ApdbSQLiteTestCase):
144 """Test case for ApdbSql class using SQLite backend with replica tables."""
146 enable_replica = True
147 meta_row_count = 4
150@unittest.skipUnless(testing is not None, "testing.postgresql module not found")
151class ApdbPostgresTestCase(ApdbSQLTest, unittest.TestCase):
152 """A test case for ApdbSql class using Postgres backend."""
154 fsrc_requires_id_list = True
155 dia_object_index = "last_object_table"
156 postgresql: Any
157 enable_replica = True
158 meta_row_count = 4
159 timestamp_type_name = "datetime64[ns]"
161 @classmethod
162 def setUpClass(cls) -> None:
163 # Create the postgres test server.
164 cls.postgresql = testing.postgresql.PostgresqlFactory(cache_initialized_db=True)
165 super().setUpClass()
167 @classmethod
168 def tearDownClass(cls) -> None:
169 # Clean up any lingering SQLAlchemy engines/connections
170 # so they're closed before we shut down the server.
171 gc.collect()
172 cls.postgresql.clear_cache()
173 super().tearDownClass()
175 def setUp(self) -> None:
176 self.server = self.postgresql()
178 def tearDown(self) -> None:
179 self.server = self.postgresql()
181 def make_instance(self, **kwargs: Any) -> ApdbConfig:
182 return super().make_instance(db_url=self.server.url(), **kwargs)
184 def getDiaObjects_table(self) -> ApdbTables:
185 """Return type of table returned from getDiaObjects method."""
186 return ApdbTables.DiaObjectLast
189@unittest.skipUnless(testing is not None, "testing.postgresql module not found")
190class ApdbPostgresNamespaceTestCase(ApdbPostgresTestCase):
191 """A test case for ApdbSql class using Postgres backend with schema name"""
193 # use mixed case to trigger quoting
194 namespace = "ApdbSchema"
196 def make_instance(self, **kwargs: Any) -> ApdbConfig:
197 """Make config class instance used in all tests."""
198 return super().make_instance(namespace=self.namespace, **kwargs)
201class ApdbSchemaUpdateSQLiteTestCase(ApdbSchemaUpdateTest, unittest.TestCase):
202 """A test case for schema updates using SQLite backend."""
204 def setUp(self) -> None:
205 self.tempdir = tempfile.mkdtemp()
206 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
208 def tearDown(self) -> None:
209 shutil.rmtree(self.tempdir, ignore_errors=True)
211 def make_instance(self, **kwargs: Any) -> ApdbConfig:
212 """Make config class instance used in all tests."""
213 kw = {
214 "db_url": self.db_url,
215 "schema_file": TEST_SCHEMA,
216 }
217 kw.update(kwargs)
218 return ApdbSql.init_database(**kw) # type: ignore[arg-type]
221class ApdbSQLiteFromUriTestCase(unittest.TestCase):
222 """A test case for for instantiating ApdbSql via URI."""
224 def setUp(self) -> None:
225 self.tempdir = tempfile.mkdtemp()
226 self.addCleanup(shutil.rmtree, self.tempdir, ignore_errors=True)
227 self.db_url = f"sqlite:///{self.tempdir}/apdb.sqlite3"
228 config = ApdbSql.init_database(db_url=self.db_url, schema_file=TEST_SCHEMA)
229 # TODO: This will need update when we switch to pydantic configs.
230 self.config_path = os.path.join(self.tempdir, "apdb-config.yaml")
231 config.save(self.config_path)
232 self.bad_config_path = os.path.join(self.tempdir, "not-config.yaml")
233 self.index_path = os.path.join(self.tempdir, "apdb-index.yaml")
234 with open(self.index_path, "w") as index_file:
235 print(f'label1: "{self.config_path}"', file=index_file)
236 print(f'"label2/pex_config": "{self.config_path}"', file=index_file)
237 print(f'bad-label: "{self.bad_config_path}"', file=index_file)
238 # File with incorrect format.
239 self.bad_index_path = os.path.join(self.tempdir, "apdb-index-bad.yaml")
240 with open(self.bad_index_path, "w") as index_file:
241 print(f'label1: ["{self.config_path}"]', file=index_file)
242 self.missing_index_path = os.path.join(self.tempdir, "no-apdb-index.yaml")
244 def test_make_apdb_from_path(self) -> None:
245 """Check that we can make APDB instance from config URI."""
246 Apdb.from_uri(self.config_path)
247 with self.assertRaises(FileNotFoundError):
248 Apdb.from_uri(self.bad_config_path)
250 def test_make_apdb_from_labels(self) -> None:
251 """Check that we can make APDB instance from config URI."""
252 # Replace DAX_APDB_INDEX_URI value
253 new_env = {"DAX_APDB_INDEX_URI": self.index_path}
254 with patch.dict(os.environ, new_env, clear=True):
255 Apdb.from_uri("label:label1")
256 Apdb.from_uri("label:label2")
257 # Label does not exist.
258 with self.assertRaises(ValueError):
259 Apdb.from_uri("label:not-a-label")
260 # Label exists but points to a missing config.
261 with self.assertRaises(FileNotFoundError):
262 Apdb.from_uri("label:bad-label")
264 def test_make_apdb_bad_index(self) -> None:
265 """Check what happens when DAX_APDB_INDEX_URI is broken."""
266 # envvar is set but empty.
267 new_env = {"DAX_APDB_INDEX_URI": ""}
268 with patch.dict(os.environ, new_env, clear=True):
269 with self.assertRaises(RuntimeError):
270 Apdb.from_uri("label:label")
272 # envvar is set to something non-existing.
273 new_env = {"DAX_APDB_INDEX_URI": self.missing_index_path}
274 with patch.dict(os.environ, new_env, clear=True):
275 with self.assertRaises(FileNotFoundError):
276 Apdb.from_uri("label:label")
278 # envvar points to an incorrect file.
279 new_env = {"DAX_APDB_INDEX_URI": self.bad_index_path}
280 with patch.dict(os.environ, new_env, clear=True):
281 with self.assertRaises(TypeError):
282 Apdb.from_uri("label:label")
284 def test_remove_database_file(self) -> None:
285 """Check that SQLite does not try to recreate database after it was
286 removed, but config persisted.
287 """
288 os.unlink(f"{self.tempdir}/apdb.sqlite3")
289 with self.assertRaisesRegex(sqlalchemy.exc.OperationalError, "unable to open database file"):
290 Apdb.from_uri(self.config_path)
292 def test_make_apdb_replica(self) -> None:
293 """Check that we can make ApdbReplica instance from config URI."""
294 ApdbReplica.from_uri(self.config_path)
295 with self.assertRaises(FileNotFoundError):
296 Apdb.from_uri(self.bad_config_path)
299class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
300 """Run file leak tests."""
303def setup_module(module: Any) -> None:
304 """Configure pytest."""
305 lsst.utils.tests.init()
308if __name__ == "__main__":
309 lsst.utils.tests.init()
310 unittest.main()