Coverage for tests/test_datamodel.py: 10%

194 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-14 10:16 -0700

1# This file is part of felis. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import os 

23import unittest 

24 

25import yaml 

26from pydantic import ValidationError 

27 

28from felis.datamodel import ( 

29 CheckConstraint, 

30 Column, 

31 DataType, 

32 ForeignKeyConstraint, 

33 Index, 

34 Schema, 

35 SchemaVersion, 

36 Table, 

37 UniqueConstraint, 

38) 

39 

40TESTDIR = os.path.abspath(os.path.dirname(__file__)) 

41TEST_YAML = os.path.join(TESTDIR, "data", "test.yml") 

42 

43 

44class DataModelTestCase(unittest.TestCase): 

45 """Test validation of a test schema from a YAML file.""" 

46 

47 schema_obj: Schema 

48 

49 def test_validation(self) -> None: 

50 """Load test file and validate it using the data model.""" 

51 with open(TEST_YAML) as test_yaml: 

52 data = yaml.safe_load(test_yaml) 

53 self.schema_obj = Schema.model_validate(data) 

54 

55 

56class ColumnTestCase(unittest.TestCase): 

57 """Test the `Column` class.""" 

58 

59 def test_validation(self) -> None: 

60 """Test validation of the `Column` class.""" 

61 # Default initialization should throw an exception. 

62 with self.assertRaises(ValidationError): 

63 Column() 

64 

65 # Setting only name should throw an exception. 

66 with self.assertRaises(ValidationError): 

67 Column(name="testColumn") 

68 

69 # Setting name and id should throw an exception from missing datatype. 

70 with self.assertRaises(ValidationError): 

71 Column(name="testColumn", id="#test_id") 

72 

73 # Setting name, id, and datatype should not throw an exception and 

74 # should load data correctly. 

75 col = Column(name="testColumn", id="#test_id", datatype="string") 

76 self.assertEqual(col.name, "testColumn", "name should be 'testColumn'") 

77 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

78 self.assertEqual(col.datatype, DataType.STRING.value, "datatype should be 'DataType.STRING'") 

79 

80 # Creating from data dictionary should work and load data correctly. 

81 data = {"name": "testColumn", "id": "#test_id", "datatype": "string"} 

82 col = Column(**data) 

83 self.assertEqual(col.name, "testColumn", "name should be 'testColumn'") 

84 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

85 self.assertEqual(col.datatype, DataType.STRING.value, "datatype should be 'DataType.STRING'") 

86 

87 # Setting a bad IVOA UCD should throw an error. 

88 with self.assertRaises(ValidationError): 

89 Column(**data, ivoa_ucd="bad") 

90 

91 # Setting a valid IVOA UCD should not throw an error. 

92 col = Column(**data, ivoa_ucd="meta.id") 

93 self.assertEqual(col.ivoa_ucd, "meta.id", "ivoa_ucd should be 'meta.id'") 

94 

95 units_data = data.copy() 

96 

97 # Setting a bad IVOA unit should throw an error. 

98 units_data["ivoa:unit"] = "bad" 

99 with self.assertRaises(ValidationError): 

100 Column(**units_data) 

101 

102 # Setting a valid IVOA unit should not throw an error. 

103 units_data["ivoa:unit"] = "m" 

104 col = Column(**units_data) 

105 self.assertEqual(col.ivoa_unit, "m", "ivoa_unit should be 'm'") 

106 

107 units_data = data.copy() 

108 

109 # Setting a bad FITS TUNIT should throw an error. 

110 units_data["fits:tunit"] = "bad" 

111 with self.assertRaises(ValidationError): 

112 Column(**units_data) 

113 

114 # Setting a valid FITS TUNIT should not throw an error. 

115 units_data["fits:tunit"] = "m" 

116 col = Column(**units_data) 

117 self.assertEqual(col.fits_tunit, "m", "fits_tunit should be 'm'") 

118 

119 # Setting both IVOA unit and FITS TUNIT should throw an error. 

120 units_data["ivoa:unit"] = "m" 

121 with self.assertRaises(ValidationError): 

122 Column(**units_data) 

123 

124 def test_require_description(self) -> None: 

125 """Test the require_description flag for the `Column` class.""" 

126 # Turn on description requirement for this test. 

127 Schema.require_description(True) 

128 

129 # Make sure that setting the flag for description requirement works 

130 # correctly. 

131 self.assertTrue(Schema.is_description_required(), "description should be required") 

132 

133 # Creating a column without a description when required should throw an 

134 # error. 

135 with self.assertRaises(ValidationError): 

136 Column( 

137 **{ 

138 "name": "testColumn", 

139 "@id": "#test_col_id", 

140 "datatype": "string", 

141 } 

142 ) 

143 

144 # Creating a column with a None description when required should throw. 

145 with self.assertRaises(ValidationError): 

146 Column(**{"name": "testColumn", "@id": "#test_col_id", "datatype": "string", "description": None}) 

147 

148 # Creating a column with an empty description when required should 

149 # throw. 

150 with self.assertRaises(ValidationError): 

151 Column(**{"name": "testColumn", "@id": "#test_col_id", "datatype": "string", "description": ""}) 

152 

153 # Creating a column with a description that is not long enough should 

154 # throw. 

155 with self.assertRaises(ValidationError): 

156 Column(**{"name": "testColumn", "@id": "#test_col_id", "datatype": "string", "description": "xy"}) 

157 

158 # Turn off flag or it will affect subsequent tests. 

159 Schema.require_description(False) 

160 

161 

162class ConstraintTestCase(unittest.TestCase): 

163 """Test the `UniqueConstraint`, `Index`, `CheckCosntraint`, and 

164 `ForeignKeyConstraint` classes. 

165 """ 

166 

167 def test_unique_constraint_validation(self) -> None: 

168 """Test validation of the `UniqueConstraint` class.""" 

169 # Default initialization should throw an exception. 

170 with self.assertRaises(ValidationError): 

171 UniqueConstraint() 

172 

173 # Setting only name should throw an exception. 

174 with self.assertRaises(ValidationError): 

175 UniqueConstraint(name="testConstraint") 

176 

177 # Setting name and id should throw an exception from missing columns. 

178 with self.assertRaises(ValidationError): 

179 UniqueConstraint(name="testConstraint", id="#test_id") 

180 

181 # Setting name, id, and columns should not throw an exception and 

182 # should load data correctly. 

183 col = UniqueConstraint(name="testConstraint", id="#test_id", columns=["testColumn"]) 

184 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

185 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

186 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

187 

188 # Creating from data dictionary should work and load data correctly. 

189 data = {"name": "testConstraint", "id": "#test_id", "columns": ["testColumn"]} 

190 col = UniqueConstraint(**data) 

191 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

192 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

193 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

194 

195 def test_index_validation(self) -> None: 

196 """Test validation of the `Index` class.""" 

197 # Default initialization should throw an exception. 

198 with self.assertRaises(ValidationError): 

199 Index() 

200 

201 # Setting only name should throw an exception. 

202 with self.assertRaises(ValidationError): 

203 Index(name="testConstraint") 

204 

205 # Setting name and id should throw an exception from missing columns. 

206 with self.assertRaises(ValidationError): 

207 Index(name="testConstraint", id="#test_id") 

208 

209 # Setting name, id, and columns should not throw an exception and 

210 # should load data correctly. 

211 col = Index(name="testConstraint", id="#test_id", columns=["testColumn"]) 

212 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

213 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

214 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

215 

216 # Creating from data dictionary should work and load data correctly. 

217 data = {"name": "testConstraint", "id": "#test_id", "columns": ["testColumn"]} 

218 col = Index(**data) 

219 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

220 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

221 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

222 

223 # Setting both columns and expressions on an index should throw an 

224 # exception. 

225 with self.assertRaises(ValidationError): 

226 Index(name="testConstraint", id="#test_id", columns=["testColumn"], expressions=["1+2"]) 

227 

228 def test_foreign_key_validation(self) -> None: 

229 """Test validation of the `ForeignKeyConstraint` class.""" 

230 # Default initialization should throw an exception. 

231 with self.assertRaises(ValidationError): 

232 ForeignKeyConstraint() 

233 

234 # Setting only name should throw an exception. 

235 with self.assertRaises(ValidationError): 

236 ForeignKeyConstraint(name="testConstraint") 

237 

238 # Setting name and id should throw an exception from missing columns. 

239 with self.assertRaises(ValidationError): 

240 ForeignKeyConstraint(name="testConstraint", id="#test_id") 

241 

242 # Setting name, id, and columns should not throw an exception and 

243 # should load data correctly. 

244 col = ForeignKeyConstraint( 

245 name="testConstraint", id="#test_id", columns=["testColumn"], referenced_columns=["testColumn"] 

246 ) 

247 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

248 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

249 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

250 self.assertEqual( 

251 col.referenced_columns, ["testColumn"], "referenced_columns should be ['testColumn']" 

252 ) 

253 

254 # Creating from data dictionary should work and load data correctly. 

255 data = { 

256 "name": "testConstraint", 

257 "id": "#test_id", 

258 "columns": ["testColumn"], 

259 "referenced_columns": ["testColumn"], 

260 } 

261 col = ForeignKeyConstraint(**data) 

262 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

263 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

264 self.assertEqual(col.columns, ["testColumn"], "columns should be ['testColumn']") 

265 self.assertEqual( 

266 col.referenced_columns, ["testColumn"], "referenced_columns should be ['testColumn']" 

267 ) 

268 

269 def test_check_constraint_validation(self) -> None: 

270 """Check validation of the `CheckConstraint` class.""" 

271 # Default initialization should throw an exception. 

272 with self.assertRaises(ValidationError): 

273 CheckConstraint() 

274 

275 # Setting only name should throw an exception. 

276 with self.assertRaises(ValidationError): 

277 CheckConstraint(name="testConstraint") 

278 

279 # Setting name and id should throw an exception from missing 

280 # expression. 

281 with self.assertRaises(ValidationError): 

282 CheckConstraint(name="testConstraint", id="#test_id") 

283 

284 # Setting name, id, and expression should not throw an exception and 

285 # should load data correctly. 

286 col = CheckConstraint(name="testConstraint", id="#test_id", expression="1+2") 

287 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

288 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

289 self.assertEqual(col.expression, "1+2", "expression should be '1+2'") 

290 

291 # Creating from data dictionary should work and load data correctly. 

292 data = { 

293 "name": "testConstraint", 

294 "id": "#test_id", 

295 "expression": "1+2", 

296 } 

297 col = CheckConstraint(**data) 

298 self.assertEqual(col.name, "testConstraint", "name should be 'testConstraint'") 

299 self.assertEqual(col.id, "#test_id", "id should be '#test_id'") 

300 self.assertEqual(col.expression, "1+2", "expression should be '1+2'") 

301 

302 

303class TableTestCase(unittest.TestCase): 

304 """Test the `Table` class.""" 

305 

306 def test_validation(self) -> None: 

307 # Default initialization should throw an exception. 

308 with self.assertRaises(ValidationError): 

309 Table() 

310 

311 # Setting only name should throw an exception. 

312 with self.assertRaises(ValidationError): 

313 Table(name="testTable") 

314 

315 # Setting name and id should throw an exception from missing columns. 

316 with self.assertRaises(ValidationError): 

317 Index(name="testTable", id="#test_id") 

318 

319 testCol = Column(name="testColumn", id="#test_id", datatype="string") 

320 

321 # Setting name, id, and columns should not throw an exception and 

322 # should load data correctly. 

323 tbl = Table(name="testTable", id="#test_id", columns=[testCol]) 

324 self.assertEqual(tbl.name, "testTable", "name should be 'testTable'") 

325 self.assertEqual(tbl.id, "#test_id", "id should be '#test_id'") 

326 self.assertEqual(tbl.columns, [testCol], "columns should be ['testColumn']") 

327 

328 # Creating a table with duplicate column names should raise an 

329 # exception. 

330 with self.assertRaises(ValidationError): 

331 Table(name="testTable", id="#test_id", columns=[testCol, testCol]) 

332 

333 

334class SchemaTestCase(unittest.TestCase): 

335 """Test the `Schema` class.""" 

336 

337 def test_validation(self) -> None: 

338 # Default initialization should throw an exception. 

339 with self.assertRaises(ValidationError): 

340 Schema() 

341 

342 # Setting only name should throw an exception. 

343 with self.assertRaises(ValidationError): 

344 Schema(name="testSchema") 

345 

346 # Setting name and id should throw an exception from missing columns. 

347 with self.assertRaises(ValidationError): 

348 Schema(name="testSchema", id="#test_id") 

349 

350 test_col = Column(name="testColumn", id="#test_col_id", datatype="string") 

351 test_tbl = Table(name="testTable", id="#test_tbl_id", columns=[test_col]) 

352 

353 # Setting name, id, and columns should not throw an exception and 

354 # should load data correctly. 

355 sch = Schema(name="testSchema", id="#test_sch_id", tables=[test_tbl]) 

356 self.assertEqual(sch.name, "testSchema", "name should be 'testSchema'") 

357 self.assertEqual(sch.id, "#test_sch_id", "id should be '#test_sch_id'") 

358 self.assertEqual(sch.tables, [test_tbl], "tables should be ['testTable']") 

359 

360 # Creating a schema with duplicate table names should raise an 

361 # exception. 

362 with self.assertRaises(ValidationError): 

363 Schema(name="testSchema", id="#test_id", tables=[test_tbl, test_tbl]) 

364 

365 # Using an undefined YAML field should raise an exception. 

366 with self.assertRaises(ValidationError): 

367 Schema(**{"name": "testSchema", "id": "#test_sch_id", "bad_field": "1234"}, tables=[test_tbl]) 

368 

369 # Creating a schema containing duplicate IDs should raise an error. 

370 with self.assertRaises(ValidationError): 

371 Schema( 

372 name="testSchema", 

373 id="#test_sch_id", 

374 tables=[ 

375 Table( 

376 name="testTable", 

377 id="#test_tbl_id", 

378 columns=[ 

379 Column(name="testColumn", id="#test_col_id", datatype="string"), 

380 Column(name="testColumn2", id="#test_col_id", datatype="string"), 

381 ], 

382 ) 

383 ], 

384 ) 

385 

386 def test_schema_object_ids(self) -> None: 

387 """Test that the id_map is properly populated.""" 

388 test_col = Column(name="testColumn", id="#test_col_id", datatype="string") 

389 test_tbl = Table(name="testTable", id="#test_table_id", columns=[test_col]) 

390 sch = Schema(name="testSchema", id="#test_schema_id", tables=[test_tbl]) 

391 

392 for id in ["#test_col_id", "#test_table_id", "#test_schema_id"]: 

393 # Test that the schema contains the expected id. 

394 self.assertTrue(id in sch, f"schema should contain '{id}'") 

395 

396 # Check that types of returned objects are correct. 

397 self.assertIsInstance(sch["#test_col_id"], Column, "schema[id] should return a Column") 

398 self.assertIsInstance(sch["#test_table_id"], Table, "schema[id] should return a Table") 

399 self.assertIsInstance(sch["#test_schema_id"], Schema, "schema[id] should return a Schema") 

400 

401 with self.assertRaises(KeyError): 

402 # Test that an invalid id raises an exception. 

403 sch["#bad_id"] 

404 

405 

406class SchemaVersionTest(unittest.TestCase): 

407 """Test the `SchemaVersion` class.""" 

408 

409 def test_validation(self) -> None: 

410 # Default initialization should throw an exception. 

411 with self.assertRaises(ValidationError): 

412 SchemaVersion() 

413 

414 # Setting current should not throw an exception and should load data 

415 # correctly. 

416 sv = SchemaVersion(current="1.0.0") 

417 self.assertEqual(sv.current, "1.0.0", "current should be '1.0.0'") 

418 

419 # Check that schema version can be specified as a single string or 

420 # an object. 

421 data = { 

422 "name": "schema", 

423 "@id": "#schema", 

424 "tables": [], 

425 "version": "1.2.3", 

426 } 

427 schema = Schema.model_validate(data) 

428 self.assertEqual(schema.version, "1.2.3") 

429 

430 data = { 

431 "name": "schema", 

432 "@id": "#schema", 

433 "tables": [], 

434 "version": { 

435 "current": "1.2.3", 

436 "compatible": ["1.2.0", "1.2.1", "1.2.2"], 

437 "read_compatible": ["1.1.0", "1.1.1"], 

438 }, 

439 } 

440 schema = Schema.model_validate(data) 

441 self.assertEqual(schema.version.current, "1.2.3") 

442 self.assertEqual(schema.version.compatible, ["1.2.0", "1.2.1", "1.2.2"]) 

443 self.assertEqual(schema.version.read_compatible, ["1.1.0", "1.1.1"]) 

444 

445 

446if __name__ == "__main__": 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true

447 unittest.main()