Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/conftest.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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

24 

25=============================================================================== 

26 

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

28 

29""" 

30 

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

32 

33import os 

34import tempfile 

35from typing import Generator, TYPE_CHECKING 

36 

37import pytest 

38from sqlalchemy import event 

39from sqlalchemy.engine import create_engine 

40from sqlalchemy.orm import Session 

41 

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) 

49 

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 

56 

57 

58TEST_DATABASE_FILENAME = os.path.join(CAMCOPS_SERVER_DIRECTORY, 

59 "camcops_test.sqlite") 

60 

61 

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 ) 

70 

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 ) 

79 

80 parser.addoption( 

81 "--mysql", 

82 action="store_true", 

83 dest="mysql", 

84 default=False, 

85 help="Use MySQL database instead of SQLite" 

86 ) 

87 

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 ) 

95 

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 ) 

103 

104 

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

110 

111 

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

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

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

115 

116 

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

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

119 if not database_on_disk: 

120 return True 

121 

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

123 return True 

124 

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

126 

127 

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

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

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

131 

132 

133# noinspection PyUnusedLocal 

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

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

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

137 

138 

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

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

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

142 

143 

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

145def tmpdir_obj(request: "FixtureRequest") -> Generator[ 

146 tempfile.TemporaryDirectory, None, None]: 

147 tmpdir_obj = tempfile.TemporaryDirectory() 

148 

149 yield tmpdir_obj 

150 

151 tmpdir_obj.cleanup() 

152 

153 

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

164 

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) 

173 

174 yield engine 

175 engine.dispose() 

176 

177 

178def create_engine_mysql(db_url: str, 

179 create_db: bool, 

180 echo: bool): 

181 

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) 

188 

189 if create_db: 

190 Base.metadata.drop_all(engine) 

191 

192 return engine 

193 

194 

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 

203 

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) 

209 

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

211 

212 return engine 

213 

214 

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 

223 

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 

229 

230 

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

240 

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) 

246 

247 yield session 

248 

249 session.close() 

250 # roll back the broader transaction 

251 transaction.rollback() 

252 # put back the connection to the connection pool 

253 connection.close() 

254 

255 

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