Coverage for conftest.py : 83%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/conftest.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27**Configure server self-tests for Pytest.**
29"""
31# https://gist.githubusercontent.com/kissgyorgy/e2365f25a213de44b9a2/raw/f8b5bbf06c4969bc6bbe5316defef64137c9b1e3/sqlalchemy_conftest.py
33import os
34import tempfile
35from typing import Generator, TYPE_CHECKING
37import pytest
38from sqlalchemy import event
39from sqlalchemy.engine import create_engine
40from sqlalchemy.orm import Session
42import camcops_server.cc_modules.cc_all_models # import side effects (ensure all models registered) # noqa: F401,E501
43from camcops_server.cc_modules.cc_baseconstants import CAMCOPS_SERVER_DIRECTORY
44from camcops_server.cc_modules.cc_sqlalchemy import (
45 Base,
46 make_memory_sqlite_engine,
47 make_file_sqlite_engine,
48)
50if TYPE_CHECKING:
51 from sqlalchemy.engine.base import Engine
52 # Should not need to import from _pytest in later versions of pytest
53 # https://github.com/pytest-dev/pytest/issues/7469
54 from _pytest.config.argparsing import Parser
55 from _pytest.fixtures import FixtureRequest
58TEST_DATABASE_FILENAME = os.path.join(CAMCOPS_SERVER_DIRECTORY,
59 "camcops_test.sqlite")
62def pytest_addoption(parser: "Parser"):
63 parser.addoption(
64 "--database-in-memory",
65 action="store_false",
66 dest="database_on_disk",
67 default=True,
68 help="Make SQLite database in memory"
69 )
71 # Borrowed from pytest-django
72 parser.addoption(
73 "--create-db",
74 action="store_true",
75 dest="create_db",
76 default=False,
77 help="Create the database even if it already exists"
78 )
80 parser.addoption(
81 "--mysql",
82 action="store_true",
83 dest="mysql",
84 default=False,
85 help="Use MySQL database instead of SQLite"
86 )
88 parser.addoption(
89 "--db-url",
90 dest="db_url",
91 default=("mysql+mysqldb://camcops:camcops@localhost:3306/test_camcops"
92 "?charset=utf8"),
93 help="SQLAlchemy test database URL (MySQL only)"
94 )
96 parser.addoption(
97 "--echo",
98 action="store_true",
99 dest="echo",
100 default=False,
101 help="Log all SQL statments to the default log handler"
102 )
105# noinspection PyUnusedLocal
106def set_sqlite_pragma(dbapi_connection, connection_record):
107 cursor = dbapi_connection.cursor()
108 cursor.execute("PRAGMA foreign_keys=ON")
109 cursor.close()
112@pytest.fixture(scope="session")
113def database_on_disk(request: "FixtureRequest") -> bool:
114 return request.config.getvalue("database_on_disk")
117@pytest.fixture(scope="session")
118def create_db(request: "FixtureRequest", database_on_disk) -> bool:
119 if not database_on_disk:
120 return True
122 if not os.path.exists(TEST_DATABASE_FILENAME):
123 return True
125 return request.config.getvalue("create_db")
128@pytest.fixture(scope="session")
129def echo(request: "FixtureRequest") -> bool:
130 return request.config.getvalue("echo")
133# noinspection PyUnusedLocal
134@pytest.fixture(scope="session")
135def mysql(request: "FixtureRequest") -> bool:
136 return request.config.getvalue("mysql")
139@pytest.fixture(scope="session")
140def db_url(request: "FixtureRequest") -> bool:
141 return request.config.getvalue("db_url")
144@pytest.fixture(scope="session")
145def tmpdir_obj(request: "FixtureRequest") -> Generator[
146 tempfile.TemporaryDirectory, None, None]:
147 tmpdir_obj = tempfile.TemporaryDirectory()
149 yield tmpdir_obj
151 tmpdir_obj.cleanup()
154# https://gist.github.com/kissgyorgy/e2365f25a213de44b9a2
155# Author says "no [license], feel free to use it"
156# noinspection PyUnusedLocal
157@pytest.fixture(scope="session")
158def engine(request: "FixtureRequest",
159 create_db: bool,
160 database_on_disk: bool,
161 echo: bool,
162 mysql: bool,
163 db_url: str) -> Generator["Engine", None, None]:
165 if mysql:
166 engine = create_engine_mysql(db_url,
167 create_db,
168 echo)
169 else:
170 engine = create_engine_sqlite(create_db,
171 echo,
172 database_on_disk)
174 yield engine
175 engine.dispose()
178def create_engine_mysql(db_url: str,
179 create_db: bool,
180 echo: bool):
182 # The database and the user with the given password from db_url
183 # need to exist.
184 # mysql> CREATE DATABASE <db_name>;
185 # mysql> GRANT ALL PRIVILEGES ON <db_name>.*
186 # TO <db_user>@localhost IDENTIFIED BY '<db_password>';
187 engine = create_engine(db_url, echo=echo, pool_pre_ping=True)
189 if create_db:
190 Base.metadata.drop_all(engine)
192 return engine
195def create_engine_sqlite(create_db: bool,
196 echo: bool,
197 database_on_disk: bool):
198 if create_db and database_on_disk:
199 try:
200 os.remove(TEST_DATABASE_FILENAME)
201 except OSError:
202 pass
204 if database_on_disk:
205 engine = make_file_sqlite_engine(TEST_DATABASE_FILENAME,
206 echo=echo)
207 else:
208 engine = make_memory_sqlite_engine(echo=echo)
210 event.listen(engine, "connect", set_sqlite_pragma)
212 return engine
215# noinspection PyUnusedLocal
216@pytest.fixture(scope="session")
217def tables(request: "FixtureRequest",
218 engine: "Engine",
219 create_db: bool) -> Generator[None, None, None]:
220 if create_db:
221 Base.metadata.create_all(engine)
222 yield
224 # Any post-session clean up would go here
225 # Foreign key constraint on _security_devices prevents this:
226 # Base.metadata.drop_all(engine)
227 # This would only be useful if we wanted to clean up the database
228 # after running the tests
231# noinspection PyUnusedLocal
232@pytest.fixture
233def dbsession(request: "FixtureRequest",
234 engine: "Engine",
235 tables: None) -> Generator[Session, None, None]:
236 """
237 Returns an sqlalchemy session, and after the test tears down everything
238 properly.
239 """
241 connection = engine.connect()
242 # begin the nested transaction
243 transaction = connection.begin()
244 # use the connection with the already started transaction
245 session = Session(bind=connection)
247 yield session
249 session.close()
250 # roll back the broader transaction
251 transaction.rollback()
252 # put back the connection to the connection pool
253 connection.close()
256@pytest.fixture
257def setup(request: "FixtureRequest",
258 engine: "Engine",
259 database_on_disk: bool,
260 mysql: bool,
261 dbsession: Session,
262 tmpdir_obj: tempfile.TemporaryDirectory) -> None:
263 # Pytest prefers function-based tests over unittest.TestCase subclasses and
264 # methods, but it still supports the latter perfectly well.
265 # We use this fixture in cc_unittest.py to store these values into
266 # DemoRequestTestCase and its descendants.
267 request.cls.engine = engine
268 request.cls.database_on_disk = database_on_disk
269 request.cls.dbsession = dbsession
270 request.cls.tmpdir_obj = tmpdir_obj
271 request.cls.db_filename = TEST_DATABASE_FILENAME
272 request.cls.mysql = mysql