Coverage for tests/test_datamodel.py: 10%
194 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-17 10:27 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-17 10:27 +0000
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.value, "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.value, "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."""
126 # Turn on description requirement for this test.
127 Schema.require_description(True)
129 # Make sure that setting the flag for description requirement works
130 # correctly.
131 self.assertTrue(Schema.is_description_required(), "description should be required")
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 )
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})
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": ""})
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"})
158 # Turn off flag or it will affect subsequent tests.
159 Schema.require_description(False)
162class ConstraintTestCase(unittest.TestCase):
163 """Test the `UniqueConstraint`, `Index`, `CheckCosntraint`, and
164 `ForeignKeyConstraint` classes.
165 """
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()
173 # Setting only name should throw an exception.
174 with self.assertRaises(ValidationError):
175 UniqueConstraint(name="testConstraint")
177 # Setting name and id should throw an exception from missing columns.
178 with self.assertRaises(ValidationError):
179 UniqueConstraint(name="testConstraint", id="#test_id")
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']")
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']")
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()
201 # Setting only name should throw an exception.
202 with self.assertRaises(ValidationError):
203 Index(name="testConstraint")
205 # Setting name and id should throw an exception from missing columns.
206 with self.assertRaises(ValidationError):
207 Index(name="testConstraint", id="#test_id")
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']")
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']")
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"])
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()
234 # Setting only name should throw an exception.
235 with self.assertRaises(ValidationError):
236 ForeignKeyConstraint(name="testConstraint")
238 # Setting name and id should throw an exception from missing columns.
239 with self.assertRaises(ValidationError):
240 ForeignKeyConstraint(name="testConstraint", id="#test_id")
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 )
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 )
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()
275 # Setting only name should throw an exception.
276 with self.assertRaises(ValidationError):
277 CheckConstraint(name="testConstraint")
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")
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'")
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'")
303class TableTestCase(unittest.TestCase):
304 """Test the `Table` class."""
306 def test_validation(self) -> None:
307 # Default initialization should throw an exception.
308 with self.assertRaises(ValidationError):
309 Table()
311 # Setting only name should throw an exception.
312 with self.assertRaises(ValidationError):
313 Table(name="testTable")
315 # Setting name and id should throw an exception from missing columns.
316 with self.assertRaises(ValidationError):
317 Index(name="testTable", id="#test_id")
319 testCol = Column(name="testColumn", id="#test_id", datatype="string")
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']")
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])
334class SchemaTestCase(unittest.TestCase):
335 """Test the `Schema` class."""
337 def test_validation(self) -> None:
338 # Default initialization should throw an exception.
339 with self.assertRaises(ValidationError):
340 Schema()
342 # Setting only name should throw an exception.
343 with self.assertRaises(ValidationError):
344 Schema(name="testSchema")
346 # Setting name and id should throw an exception from missing columns.
347 with self.assertRaises(ValidationError):
348 Schema(name="testSchema", id="#test_id")
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])
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']")
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])
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])
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 )
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])
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}'")
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")
401 with self.assertRaises(KeyError):
402 # Test that an invalid id raises an exception.
403 sch["#bad_id"]
406class SchemaVersionTest(unittest.TestCase):
407 """Test the `SchemaVersion` class."""
409 def test_validation(self) -> None:
410 # Default initialization should throw an exception.
411 with self.assertRaises(ValidationError):
412 SchemaVersion()
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'")
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")
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"])
446if __name__ == "__main__": 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true
447 unittest.main()