Coverage for tests/test_datamodel.py: 10%
197 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:20 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-25 10:20 -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/>.
22import os
23import unittest
25import yaml
26from pydantic import ValidationError
28from felis.datamodel import (
29 CheckConstraint,
30 Column,
31 DataType,
32 ForeignKeyConstraint,
33 Index,
34 Schema,
35 SchemaVersion,
36 Table,
37 UniqueConstraint,
38)
40TESTDIR = os.path.abspath(os.path.dirname(__file__))
41TEST_YAML = os.path.join(TESTDIR, "data", "test.yml")
44class DataModelTestCase(unittest.TestCase):
45 """Test validation of a test schema from a YAML file."""
47 schema_obj: Schema
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)
56class ColumnTestCase(unittest.TestCase):
57 """Test the `Column` class."""
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()
65 # Setting only name should throw an exception.
66 with self.assertRaises(ValidationError):
67 Column(name="testColumn")
69 # Setting name and id should throw an exception from missing datatype.
70 with self.assertRaises(ValidationError):
71 Column(name="testColumn", id="#test_id")
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'")
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'")
87 # Setting a bad IVOA UCD should throw an error.
88 with self.assertRaises(ValidationError):
89 Column(**data, ivoa_ucd="bad")
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'")
95 units_data = data.copy()
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)
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'")
107 units_data = data.copy()
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)
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'")
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)
124 def test_require_description(self) -> None:
125 """Test the require_description flag for the `Column` class."""
127 class MockValidationInfo:
128 """Mock context object for passing to validation method."""
130 def __init__(self):
131 self.context = {"require_description": True}
133 info = MockValidationInfo()
135 def _check_description(col: Column):
136 Schema.check_description(col, info)
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 )
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 )
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 )
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 )
192class ConstraintTestCase(unittest.TestCase):
193 """Test the `UniqueConstraint`, `Index`, `CheckConstraint`, and
194 `ForeignKeyConstraint` classes.
195 """
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()
203 # Setting only name should throw an exception.
204 with self.assertRaises(ValidationError):
205 UniqueConstraint(name="testConstraint")
207 # Setting name and id should throw an exception from missing columns.
208 with self.assertRaises(ValidationError):
209 UniqueConstraint(name="testConstraint", id="#test_id")
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']")
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']")
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()
231 # Setting only name should throw an exception.
232 with self.assertRaises(ValidationError):
233 Index(name="testConstraint")
235 # Setting name and id should throw an exception from missing columns.
236 with self.assertRaises(ValidationError):
237 Index(name="testConstraint", id="#test_id")
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']")
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']")
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"])
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()
264 # Setting only name should throw an exception.
265 with self.assertRaises(ValidationError):
266 ForeignKeyConstraint(name="testConstraint")
268 # Setting name and id should throw an exception from missing columns.
269 with self.assertRaises(ValidationError):
270 ForeignKeyConstraint(name="testConstraint", id="#test_id")
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 )
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 )
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()
305 # Setting only name should throw an exception.
306 with self.assertRaises(ValidationError):
307 CheckConstraint(name="testConstraint")
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")
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'")
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'")
333class TableTestCase(unittest.TestCase):
334 """Test the `Table` class."""
336 def test_validation(self) -> None:
337 # Default initialization should throw an exception.
338 with self.assertRaises(ValidationError):
339 Table()
341 # Setting only name should throw an exception.
342 with self.assertRaises(ValidationError):
343 Table(name="testTable")
345 # Setting name and id should throw an exception from missing columns.
346 with self.assertRaises(ValidationError):
347 Index(name="testTable", id="#test_id")
349 testCol = Column(name="testColumn", id="#test_id", datatype="string")
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']")
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])
364class SchemaTestCase(unittest.TestCase):
365 """Test the `Schema` class."""
367 def test_validation(self) -> None:
368 # Default initialization should throw an exception.
369 with self.assertRaises(ValidationError):
370 Schema()
372 # Setting only name should throw an exception.
373 with self.assertRaises(ValidationError):
374 Schema(name="testSchema")
376 # Setting name and id should throw an exception from missing columns.
377 with self.assertRaises(ValidationError):
378 Schema(name="testSchema", id="#test_id")
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])
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']")
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])
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])
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 )
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])
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}'")
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")
431 with self.assertRaises(KeyError):
432 # Test that an invalid id raises an exception.
433 sch["#bad_id"]
436class SchemaVersionTest(unittest.TestCase):
437 """Test the `SchemaVersion` class."""
439 def test_validation(self) -> None:
440 # Default initialization should throw an exception.
441 with self.assertRaises(ValidationError):
442 SchemaVersion()
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'")
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")
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"])
476if __name__ == "__main__": 476 ↛ 477line 476 didn't jump to line 477, because the condition on line 476 was never true
477 unittest.main()