Coverage for cc_modules/tests/cc_sqla_coltypes_tests.py: 25%

114 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/cc_modules/tests/cc_sqla_coltypes_tests.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 

29import datetime 

30from typing import Union 

31import unittest 

32 

33from cardinal_pythonlib.datetimefunc import coerce_to_pendulum 

34from cardinal_pythonlib.sqlalchemy.session import SQLITE_MEMORY_URL 

35import pendulum 

36from pendulum import DateTime as Pendulum, Duration 

37import phonenumbers 

38from semantic_version import Version 

39from sqlalchemy.engine import create_engine 

40from sqlalchemy.sql.expression import select 

41from sqlalchemy.sql.functions import func 

42from sqlalchemy.sql.schema import Column, MetaData, Table 

43from sqlalchemy.sql.sqltypes import DateTime, Integer 

44 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 isotzdatetime_to_utcdatetime, 

47 PendulumDateTimeAsIsoTextColType, 

48 PendulumDurationAsIsoTextColType, 

49 PhoneNumberColType, 

50 SemanticVersionColType, 

51 unknown_field_to_utcdatetime, 

52) 

53 

54 

55# ============================================================================= 

56# Unit testing 

57# ============================================================================= 

58 

59 

60class SqlaColtypesTest(unittest.TestCase): 

61 """ 

62 Unit tests. 

63 """ 

64 

65 # don't inherit from ExtendedTestCase; circular import 

66 

67 def setUp(self) -> None: 

68 super().setUp() 

69 

70 engine = create_engine(SQLITE_MEMORY_URL, echo=True) 

71 self.meta = MetaData() 

72 self.meta.bind = engine # adds execute() method to select() etc. 

73 # ... https://docs.sqlalchemy.org/en/latest/core/connections.html 

74 

75 @staticmethod 

76 def _assert_dt_equal( 

77 a: Union[datetime.datetime, Pendulum], 

78 b: Union[datetime.datetime, Pendulum], 

79 ) -> None: 

80 # Accept that one may have been truncated or rounded to milliseconds. 

81 a = coerce_to_pendulum(a) 

82 b = coerce_to_pendulum(b) 

83 diff = a - b 

84 assert diff.microseconds < 1000, f"{a!r} != {b!r}" 

85 

86 def test_iso_datetime_field(self) -> None: 

87 id_colname = "id" 

88 dt_local_colname = "dt_local" 

89 dt_utc_colname = "dt_utc" 

90 iso_colname = "iso" 

91 id_col = Column(id_colname, Integer, primary_key=True) 

92 dt_local_col = Column(dt_local_colname, DateTime) 

93 dt_utc_col = Column(dt_utc_colname, DateTime) 

94 iso_col = Column(iso_colname, PendulumDateTimeAsIsoTextColType) 

95 

96 table = Table( 

97 "testtable", self.meta, id_col, dt_local_col, dt_utc_col, iso_col 

98 ) 

99 table.create() 

100 

101 now = Pendulum.now() 

102 now_utc = now.in_tz(pendulum.UTC) 

103 yesterday = now.subtract(days=1) 

104 yesterday_utc = yesterday.in_tz(pendulum.UTC) 

105 

106 table.insert().values( 

107 [ 

108 { 

109 id_colname: 1, 

110 dt_local_colname: now, 

111 dt_utc_colname: now_utc, 

112 iso_colname: now, 

113 }, 

114 { 

115 id_colname: 2, 

116 dt_local_colname: yesterday, 

117 dt_utc_colname: yesterday_utc, 

118 iso_colname: yesterday, 

119 }, 

120 ] 

121 ).execute() 

122 select_fields = [ 

123 id_col, 

124 dt_local_col, 

125 dt_utc_col, 

126 iso_col, 

127 func.length(dt_local_col).label("len_dt_local_col"), 

128 func.length(dt_utc_col).label("len_dt_utc_col"), 

129 func.length(iso_col).label("len_iso_col"), 

130 isotzdatetime_to_utcdatetime(iso_col).label("iso_to_utcdt"), 

131 unknown_field_to_utcdatetime(dt_utc_col).label( 

132 "uk_utcdt_to_utcdt" 

133 ), 

134 unknown_field_to_utcdatetime(iso_col).label("uk_iso_to_utc_dt"), 

135 ] 

136 rows = list( 

137 select(select_fields).select_from(table).order_by(id_col).execute() 

138 ) 

139 self._assert_dt_equal(rows[0][dt_local_col], now) 

140 self._assert_dt_equal(rows[0][dt_utc_col], now_utc) 

141 self._assert_dt_equal(rows[0][iso_colname], now) 

142 self._assert_dt_equal(rows[0]["iso_to_utcdt"], now_utc) 

143 self._assert_dt_equal(rows[0]["uk_utcdt_to_utcdt"], now_utc) 

144 self._assert_dt_equal(rows[0]["uk_iso_to_utc_dt"], now_utc) 

145 self._assert_dt_equal(rows[1][dt_local_col], yesterday) 

146 self._assert_dt_equal(rows[1][dt_utc_col], yesterday_utc) 

147 self._assert_dt_equal(rows[1][iso_colname], yesterday) 

148 self._assert_dt_equal(rows[1]["iso_to_utcdt"], yesterday_utc) 

149 self._assert_dt_equal(rows[1]["uk_utcdt_to_utcdt"], yesterday_utc) 

150 self._assert_dt_equal(rows[1]["uk_iso_to_utc_dt"], yesterday_utc) 

151 

152 @staticmethod 

153 def _assert_duration_equal(a: Duration, b: Duration) -> None: 

154 assert a.total_seconds() == b.total_seconds(), f"{a!r} != {b!r}" 

155 

156 def test_iso_duration_field(self) -> None: 

157 id_colname = "id" 

158 duration_colname = "duration_iso" 

159 id_col = Column(id_colname, Integer, primary_key=True) 

160 duration_col = Column( 

161 duration_colname, PendulumDurationAsIsoTextColType 

162 ) 

163 

164 table = Table("testtable", self.meta, id_col, duration_col) 

165 table.create() 

166 

167 d1 = Duration(years=1, months=3, seconds=3, microseconds=4) 

168 d2 = Duration(seconds=987.654321) 

169 d3 = Duration(days=-5) 

170 

171 table.insert().values( 

172 [ 

173 {id_colname: 1, duration_colname: d1}, 

174 {id_colname: 2, duration_colname: d2}, 

175 {id_colname: 3, duration_colname: d3}, 

176 ] 

177 ).execute() 

178 select_fields = [id_col, duration_col] 

179 rows = list( 

180 select(select_fields).select_from(table).order_by(id_col).execute() 

181 ) 

182 self._assert_duration_equal(rows[0][duration_col], d1) 

183 self._assert_duration_equal(rows[1][duration_col], d2) 

184 self._assert_duration_equal(rows[2][duration_col], d3) 

185 

186 @staticmethod 

187 def _assert_version_equal(a: Version, b: Version) -> None: 

188 assert a == b, f"{a!r} != {b!r}" 

189 

190 def test_semantic_version_field(self) -> None: 

191 id_colname = "id" 

192 version_colname = "version" 

193 id_col = Column(id_colname, Integer, primary_key=True) 

194 version_col = Column(version_colname, SemanticVersionColType) 

195 

196 table = Table("testtable", self.meta, id_col, version_col) 

197 table.create() 

198 

199 v1 = Version("1.1.0") 

200 v2 = Version("2.0.1") 

201 v3 = Version("14.0.0") 

202 

203 table.insert().values( 

204 [ 

205 {id_colname: 1, version_colname: v1}, 

206 {id_colname: 2, version_colname: v2}, 

207 {id_colname: 3, version_colname: v3}, 

208 ] 

209 ).execute() 

210 select_fields = [id_col, version_col] 

211 rows = list( 

212 select(select_fields).select_from(table).order_by(id_col).execute() 

213 ) 

214 self._assert_version_equal(rows[0][version_col], v1) 

215 self._assert_version_equal(rows[1][version_col], v2) 

216 self._assert_version_equal(rows[2][version_col], v3) 

217 

218 def test_phone_number_field(self) -> None: 

219 id_colname = "id" 

220 phone_number_colname = "phone_number" 

221 id_col = Column(id_colname, Integer, primary_key=True) 

222 phone_number_col = Column(phone_number_colname, PhoneNumberColType) 

223 

224 table = Table("testtable", self.meta, id_col, phone_number_col) 

225 table.create() 

226 

227 # https://en.wikipedia.org/wiki/Fictitious_telephone_number 

228 p1 = phonenumbers.parse("+44 (0)113 496 0123") 

229 p2 = phonenumbers.parse("+33 1 99 00 12 34 56") 

230 p3 = phonenumbers.parse("07700 900123", "GB") 

231 p4 = None 

232 

233 table.insert().values( 

234 [ 

235 {id_colname: 1, phone_number_colname: p1}, 

236 {id_colname: 2, phone_number_colname: p2}, 

237 {id_colname: 3, phone_number_colname: p3}, 

238 {id_colname: 4, phone_number_colname: p4}, 

239 ] 

240 ).execute() 

241 select_fields = [id_col, phone_number_col] 

242 rows = list( 

243 select(select_fields).select_from(table).order_by(id_col).execute() 

244 ) 

245 self.assertEqual(rows[0][phone_number_col], p1) 

246 self.assertEqual(rows[1][phone_number_col], p2) 

247 self.assertEqual(rows[2][phone_number_col], p3) 

248 self.assertIsNone(rows[3][phone_number_col])