Coverage for conftest.py: 86%

113 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/conftest.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Configure server self-tests for Pytest.** 

29 

30""" 

31 

32# https://gist.githubusercontent.com/kissgyorgy/e2365f25a213de44b9a2/raw/f8b5bbf06c4969bc6bbe5316defef64137c9b1e3/sqlalchemy_conftest.py 

33 

34import configparser 

35from io import StringIO 

36import os 

37import tempfile 

38from typing import Generator, TYPE_CHECKING 

39 

40import pytest 

41from sqlalchemy import event 

42from sqlalchemy.engine import create_engine 

43from sqlalchemy.orm import Session 

44 

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) 

53 

54if TYPE_CHECKING: 

55 from sqlalchemy.engine.base import Engine 

56 

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 

61 

62 

63TEST_DATABASE_FILENAME = os.path.join( 

64 CAMCOPS_SERVER_DIRECTORY, "camcops_test.sqlite" 

65) 

66 

67 

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 ) 

76 

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 ) 

85 

86 parser.addoption( 

87 "--mysql", 

88 action="store_true", 

89 dest="mysql", 

90 default=False, 

91 help="Use MySQL database instead of SQLite", 

92 ) 

93 

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 ) 

103 

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 ) 

111 

112 

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() 

118 

119 

120@pytest.fixture(scope="session") 

121def database_on_disk(request: "FixtureRequest") -> bool: 

122 return request.config.getvalue("database_on_disk") 

123 

124 

125@pytest.fixture(scope="session") 

126def create_db(request: "FixtureRequest", database_on_disk) -> bool: 

127 if not database_on_disk: 

128 return True 

129 

130 if not os.path.exists(TEST_DATABASE_FILENAME): 

131 return True 

132 

133 return request.config.getvalue("create_db") 

134 

135 

136@pytest.fixture(scope="session") 

137def echo(request: "FixtureRequest") -> bool: 

138 return request.config.getvalue("echo") 

139 

140 

141# noinspection PyUnusedLocal 

142@pytest.fixture(scope="session") 

143def mysql(request: "FixtureRequest") -> bool: 

144 return request.config.getvalue("mysql") 

145 

146 

147@pytest.fixture(scope="session") 

148def db_url(request: "FixtureRequest") -> bool: 

149 return request.config.getvalue("db_url") 

150 

151 

152@pytest.fixture(scope="session") 

153def tmpdir_obj( 

154 request: "FixtureRequest", 

155) -> Generator[tempfile.TemporaryDirectory, None, None]: 

156 tmpdir_obj = tempfile.TemporaryDirectory() 

157 

158 yield tmpdir_obj 

159 

160 tmpdir_obj.cleanup() 

161 

162 

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. 

170 

171 tmpconfigfilename = os.path.join(tmpdir_obj.name, "dummy_config.conf") 

172 with open(tmpconfigfilename, "w") as file: 

173 file.write(get_config_text()) 

174 

175 return tmpconfigfilename 

176 

177 

178def get_config_text() -> str: 

179 config_text = get_demo_config() 

180 parser = configparser.ConfigParser() 

181 parser.read_string(config_text) 

182 

183 with StringIO() as buffer: 

184 parser.write(buffer) 

185 config_text = buffer.getvalue() 

186 

187 return config_text 

188 

189 

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]: 

202 

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) 

207 

208 yield engine 

209 engine.dispose() 

210 

211 

212def create_engine_mysql(db_url: str, create_db: bool, echo: bool): 

213 

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) 

220 

221 if create_db: 

222 Base.metadata.drop_all(engine) 

223 

224 return engine 

225 

226 

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 

233 

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) 

238 

239 event.listen(engine, "connect", set_sqlite_pragma) 

240 

241 return engine 

242 

243 

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 

252 

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 

258 

259 

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 """ 

269 

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) 

275 

276 yield session 

277 

278 session.close() 

279 # roll back the broader transaction 

280 transaction.rollback() 

281 # put back the connection to the connection pool 

282 connection.close() 

283 

284 

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