Coverage for conftest.py: 86%
113 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/conftest.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Configure server self-tests for Pytest.**
30"""
32# https://gist.githubusercontent.com/kissgyorgy/e2365f25a213de44b9a2/raw/f8b5bbf06c4969bc6bbe5316defef64137c9b1e3/sqlalchemy_conftest.py
34import configparser
35from io import StringIO
36import os
37import tempfile
38from typing import Generator, TYPE_CHECKING
40import pytest
41from sqlalchemy import event
42from sqlalchemy.engine import create_engine
43from sqlalchemy.orm import Session
45import camcops_server.cc_modules.cc_all_models # import side effects (ensure all models registered) # noqa: F401,E501
46from camcops_server.cc_modules.cc_baseconstants import CAMCOPS_SERVER_DIRECTORY
47from camcops_server.cc_modules.cc_config import get_demo_config
48from camcops_server.cc_modules.cc_sqlalchemy import (
49 Base,
50 make_memory_sqlite_engine,
51 make_file_sqlite_engine,
52)
54if TYPE_CHECKING:
55 from sqlalchemy.engine.base import Engine
57 # Should not need to import from _pytest in later versions of pytest
58 # https://github.com/pytest-dev/pytest/issues/7469
59 from _pytest.config.argparsing import Parser
60 from _pytest.fixtures import FixtureRequest
63TEST_DATABASE_FILENAME = os.path.join(
64 CAMCOPS_SERVER_DIRECTORY, "camcops_test.sqlite"
65)
68def pytest_addoption(parser: "Parser"):
69 parser.addoption(
70 "--database-in-memory",
71 action="store_false",
72 dest="database_on_disk",
73 default=True,
74 help="Make SQLite database in memory",
75 )
77 # Borrowed from pytest-django
78 parser.addoption(
79 "--create-db",
80 action="store_true",
81 dest="create_db",
82 default=False,
83 help="Create the database even if it already exists",
84 )
86 parser.addoption(
87 "--mysql",
88 action="store_true",
89 dest="mysql",
90 default=False,
91 help="Use MySQL database instead of SQLite",
92 )
94 parser.addoption(
95 "--db-url",
96 dest="db_url",
97 default=(
98 "mysql+mysqldb://camcops:camcops@localhost:3306/test_camcops"
99 "?charset=utf8"
100 ),
101 help="SQLAlchemy test database URL (MySQL only)",
102 )
104 parser.addoption(
105 "--echo",
106 action="store_true",
107 dest="echo",
108 default=False,
109 help="Log all SQL statments to the default log handler",
110 )
113# noinspection PyUnusedLocal
114def set_sqlite_pragma(dbapi_connection, connection_record):
115 cursor = dbapi_connection.cursor()
116 cursor.execute("PRAGMA foreign_keys=ON")
117 cursor.close()
120@pytest.fixture(scope="session")
121def database_on_disk(request: "FixtureRequest") -> bool:
122 return request.config.getvalue("database_on_disk")
125@pytest.fixture(scope="session")
126def create_db(request: "FixtureRequest", database_on_disk) -> bool:
127 if not database_on_disk:
128 return True
130 if not os.path.exists(TEST_DATABASE_FILENAME):
131 return True
133 return request.config.getvalue("create_db")
136@pytest.fixture(scope="session")
137def echo(request: "FixtureRequest") -> bool:
138 return request.config.getvalue("echo")
141# noinspection PyUnusedLocal
142@pytest.fixture(scope="session")
143def mysql(request: "FixtureRequest") -> bool:
144 return request.config.getvalue("mysql")
147@pytest.fixture(scope="session")
148def db_url(request: "FixtureRequest") -> bool:
149 return request.config.getvalue("db_url")
152@pytest.fixture(scope="session")
153def tmpdir_obj(
154 request: "FixtureRequest",
155) -> Generator[tempfile.TemporaryDirectory, None, None]:
156 tmpdir_obj = tempfile.TemporaryDirectory()
158 yield tmpdir_obj
160 tmpdir_obj.cleanup()
163@pytest.fixture(scope="session")
164def config_file(
165 request: "FixtureRequest", tmpdir_obj: tempfile.TemporaryDirectory
166) -> str:
167 # We're going to be using a test (SQLite) database, but we want to
168 # be very sure that nothing writes to a real database! Also, we will
169 # want to read from this dummy config at some point.
171 tmpconfigfilename = os.path.join(tmpdir_obj.name, "dummy_config.conf")
172 with open(tmpconfigfilename, "w") as file:
173 file.write(get_config_text())
175 return tmpconfigfilename
178def get_config_text() -> str:
179 config_text = get_demo_config()
180 parser = configparser.ConfigParser()
181 parser.read_string(config_text)
183 with StringIO() as buffer:
184 parser.write(buffer)
185 config_text = buffer.getvalue()
187 return config_text
190# https://gist.github.com/kissgyorgy/e2365f25a213de44b9a2
191# Author says "no [license], feel free to use it"
192# noinspection PyUnusedLocal
193@pytest.fixture(scope="session")
194def engine(
195 request: "FixtureRequest",
196 create_db: bool,
197 database_on_disk: bool,
198 echo: bool,
199 mysql: bool,
200 db_url: str,
201) -> Generator["Engine", None, None]:
203 if mysql:
204 engine = create_engine_mysql(db_url, create_db, echo)
205 else:
206 engine = create_engine_sqlite(create_db, echo, database_on_disk)
208 yield engine
209 engine.dispose()
212def create_engine_mysql(db_url: str, create_db: bool, echo: bool):
214 # The database and the user with the given password from db_url
215 # need to exist.
216 # mysql> CREATE DATABASE <db_name>;
217 # mysql> GRANT ALL PRIVILEGES ON <db_name>.*
218 # TO <db_user>@localhost IDENTIFIED BY '<db_password>';
219 engine = create_engine(db_url, echo=echo, pool_pre_ping=True)
221 if create_db:
222 Base.metadata.drop_all(engine)
224 return engine
227def create_engine_sqlite(create_db: bool, echo: bool, database_on_disk: bool):
228 if create_db and database_on_disk:
229 try:
230 os.remove(TEST_DATABASE_FILENAME)
231 except OSError:
232 pass
234 if database_on_disk:
235 engine = make_file_sqlite_engine(TEST_DATABASE_FILENAME, echo=echo)
236 else:
237 engine = make_memory_sqlite_engine(echo=echo)
239 event.listen(engine, "connect", set_sqlite_pragma)
241 return engine
244# noinspection PyUnusedLocal
245@pytest.fixture(scope="session")
246def tables(
247 request: "FixtureRequest", engine: "Engine", create_db: bool
248) -> Generator[None, None, None]:
249 if create_db:
250 Base.metadata.create_all(engine)
251 yield
253 # Any post-session clean up would go here
254 # Foreign key constraint on _security_devices prevents this:
255 # Base.metadata.drop_all(engine)
256 # This would only be useful if we wanted to clean up the database
257 # after running the tests
260# noinspection PyUnusedLocal
261@pytest.fixture
262def dbsession(
263 request: "FixtureRequest", engine: "Engine", tables: None
264) -> Generator[Session, None, None]:
265 """
266 Returns an sqlalchemy session, and after the test tears down everything
267 properly.
268 """
270 connection = engine.connect()
271 # begin the nested transaction
272 transaction = connection.begin()
273 # use the connection with the already started transaction
274 session = Session(bind=connection)
276 yield session
278 session.close()
279 # roll back the broader transaction
280 transaction.rollback()
281 # put back the connection to the connection pool
282 connection.close()
285@pytest.fixture
286def setup(
287 request: "FixtureRequest",
288 engine: "Engine",
289 database_on_disk: bool,
290 mysql: bool,
291 dbsession: Session,
292 tmpdir_obj: tempfile.TemporaryDirectory,
293 config_file: str,
294) -> None:
295 # Pytest prefers function-based tests over unittest.TestCase subclasses and
296 # methods, but it still supports the latter perfectly well.
297 # We use this fixture in cc_unittest.py to store these values into
298 # DemoRequestTestCase and its descendants.
299 request.cls.engine = engine
300 request.cls.database_on_disk = database_on_disk
301 request.cls.dbsession = dbsession
302 request.cls.tmpdir_obj = tmpdir_obj
303 request.cls.db_filename = TEST_DATABASE_FILENAME
304 request.cls.mysql = mysql
305 request.cls.config_file = config_file