Coverage for tests/test_datamodel.py: 10%

197 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-01 15: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, "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, "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 

127 class MockValidationInfo: 

128 """Mock context object for passing to validation method.""" 

129 

130 def __init__(self): 

131 self.context = {"require_description": True} 

132 

133 info = MockValidationInfo() 

134 

135 def _check_description(col: Column): 

136 Schema.check_description(col, info) 

137 

138 # Creating a column without a description should throw. 

139 with self.assertRaises(ValueError): 

140 _check_description( 

141 Column( 

142 **{ 

143 "name": "testColumn", 

144 "@id": "#test_col_id", 

145 "datatype": "string", 

146 } 

147 ) 

148 ) 

149 

150 # Creating a column with a description of 'None' should throw. 

151 with self.assertRaises(ValueError): 

152 _check_description( 

153 Column( 

154 **{ 

155 "name": "testColumn", 

156 "@id": "#test_col_id", 

157 "datatype": "string", 

158 "description": None, 

159 } 

160 ) 

161 ) 

162 

163 # Creating a column with an empty description should throw. 

164 with self.assertRaises(ValueError): 

165 _check_description( 

166 Column( 

167 **{ 

168 "name": "testColumn", 

169 "@id": "#test_col_id", 

170 "datatype": "string", 

171 "require_description": True, 

172 "description": "", 

173 } 

174 ) 

175 ) 

176 

177 # Creating a column with a description that is too short should throw. 

178 with self.assertRaises(ValidationError): 

179 _check_description( 

180 Column( 

181 **{ 

182 "name": "testColumn", 

183 "@id": "#test_col_id", 

184 "datatype": "string", 

185 "require_description": True, 

186 "description": "xy", 

187 } 

188 ) 

189 ) 

190 

191 

192class ConstraintTestCase(unittest.TestCase): 

193 """Test the `UniqueConstraint`, `Index`, `CheckConstraint`, and 

194 `ForeignKeyConstraint` classes. 

195 """ 

196 

197 def test_unique_constraint_validation(self) -> None: 

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

199 # Default initialization should throw an exception. 

200 with self.assertRaises(ValidationError): 

201 UniqueConstraint() 

202 

203 # Setting only name should throw an exception. 

204 with self.assertRaises(ValidationError): 

205 UniqueConstraint(name="testConstraint") 

206 

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

208 with self.assertRaises(ValidationError): 

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

210 

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

212 # should load data correctly. 

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

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

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

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

217 

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

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

220 col = UniqueConstraint(**data) 

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

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

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

224 

225 def test_index_validation(self) -> None: 

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

227 # Default initialization should throw an exception. 

228 with self.assertRaises(ValidationError): 

229 Index() 

230 

231 # Setting only name should throw an exception. 

232 with self.assertRaises(ValidationError): 

233 Index(name="testConstraint") 

234 

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

236 with self.assertRaises(ValidationError): 

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

238 

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

240 # should load data correctly. 

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

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

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

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

245 

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

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

248 col = Index(**data) 

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

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

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

252 

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

254 # exception. 

255 with self.assertRaises(ValidationError): 

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

257 

258 def test_foreign_key_validation(self) -> None: 

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

260 # Default initialization should throw an exception. 

261 with self.assertRaises(ValidationError): 

262 ForeignKeyConstraint() 

263 

264 # Setting only name should throw an exception. 

265 with self.assertRaises(ValidationError): 

266 ForeignKeyConstraint(name="testConstraint") 

267 

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

269 with self.assertRaises(ValidationError): 

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

271 

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

273 # should load data correctly. 

274 col = ForeignKeyConstraint( 

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

276 ) 

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

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

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

280 self.assertEqual( 

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

282 ) 

283 

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

285 data = { 

286 "name": "testConstraint", 

287 "id": "#test_id", 

288 "columns": ["testColumn"], 

289 "referenced_columns": ["testColumn"], 

290 } 

291 col = ForeignKeyConstraint(**data) 

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

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

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

295 self.assertEqual( 

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

297 ) 

298 

299 def test_check_constraint_validation(self) -> None: 

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

301 # Default initialization should throw an exception. 

302 with self.assertRaises(ValidationError): 

303 CheckConstraint() 

304 

305 # Setting only name should throw an exception. 

306 with self.assertRaises(ValidationError): 

307 CheckConstraint(name="testConstraint") 

308 

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

310 # expression. 

311 with self.assertRaises(ValidationError): 

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

313 

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

315 # should load data correctly. 

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

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

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

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

320 

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

322 data = { 

323 "name": "testConstraint", 

324 "id": "#test_id", 

325 "expression": "1+2", 

326 } 

327 col = CheckConstraint(**data) 

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

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

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

331 

332 

333class TableTestCase(unittest.TestCase): 

334 """Test the `Table` class.""" 

335 

336 def test_validation(self) -> None: 

337 # Default initialization should throw an exception. 

338 with self.assertRaises(ValidationError): 

339 Table() 

340 

341 # Setting only name should throw an exception. 

342 with self.assertRaises(ValidationError): 

343 Table(name="testTable") 

344 

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

346 with self.assertRaises(ValidationError): 

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

348 

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

350 

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

352 # should load data correctly. 

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

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

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

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

357 

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

359 # exception. 

360 with self.assertRaises(ValidationError): 

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

362 

363 

364class SchemaTestCase(unittest.TestCase): 

365 """Test the `Schema` class.""" 

366 

367 def test_validation(self) -> None: 

368 # Default initialization should throw an exception. 

369 with self.assertRaises(ValidationError): 

370 Schema() 

371 

372 # Setting only name should throw an exception. 

373 with self.assertRaises(ValidationError): 

374 Schema(name="testSchema") 

375 

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

377 with self.assertRaises(ValidationError): 

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

379 

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

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

382 

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

384 # should load data correctly. 

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

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

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

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

389 

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

391 # exception. 

392 with self.assertRaises(ValidationError): 

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

394 

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

396 with self.assertRaises(ValidationError): 

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

398 

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

400 with self.assertRaises(ValidationError): 

401 Schema( 

402 name="testSchema", 

403 id="#test_sch_id", 

404 tables=[ 

405 Table( 

406 name="testTable", 

407 id="#test_tbl_id", 

408 columns=[ 

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

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

411 ], 

412 ) 

413 ], 

414 ) 

415 

416 def test_schema_object_ids(self) -> None: 

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

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

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

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

421 

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

423 # Test that the schema contains the expected id. 

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

425 

426 # Check that types of returned objects are correct. 

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

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

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

430 

431 with self.assertRaises(KeyError): 

432 # Test that an invalid id raises an exception. 

433 sch["#bad_id"] 

434 

435 

436class SchemaVersionTest(unittest.TestCase): 

437 """Test the `SchemaVersion` class.""" 

438 

439 def test_validation(self) -> None: 

440 # Default initialization should throw an exception. 

441 with self.assertRaises(ValidationError): 

442 SchemaVersion() 

443 

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

445 # correctly. 

446 sv = SchemaVersion(current="1.0.0") 

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

448 

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

450 # an object. 

451 data = { 

452 "name": "schema", 

453 "@id": "#schema", 

454 "tables": [], 

455 "version": "1.2.3", 

456 } 

457 schema = Schema.model_validate(data) 

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

459 

460 data = { 

461 "name": "schema", 

462 "@id": "#schema", 

463 "tables": [], 

464 "version": { 

465 "current": "1.2.3", 

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

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

468 }, 

469 } 

470 schema = Schema.model_validate(data) 

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

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

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

474 

475 

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

477 unittest.main()