Coverage for tests/test_check.py: 14%
128 statements
« prev ^ index » next coverage.py v7.4.2, created at 2024-02-22 10:56 +0000
« prev ^ index » next coverage.py v7.4.2, created at 2024-02-22 10:56 +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 contextlib
23import copy
24import os
25import unittest
26from collections.abc import Iterator, MutableMapping
27from typing import Any
29import yaml
31from felis import DEFAULT_FRAME, CheckingVisitor
33TESTDIR = os.path.abspath(os.path.dirname(__file__))
34TEST_YAML = os.path.join(TESTDIR, "data", "test.yml")
37@contextlib.contextmanager
38def remove_key(mapping: MutableMapping[str, Any], key: str) -> Iterator[MutableMapping[str, Any]]:
39 """Remove the key from the dictionary."""
40 value = mapping.pop(key)
41 yield mapping
42 mapping[key] = value
45@contextlib.contextmanager
46def replace_key(
47 mapping: MutableMapping[str, Any], key: str, value: Any
48) -> Iterator[MutableMapping[str, Any]]:
49 """Replace key value in the dictionary."""
50 if key in mapping:
51 value, mapping[key] = mapping[key], value
52 yield mapping
53 value, mapping[key] = mapping[key], value
54 else:
55 mapping[key] = value
56 yield mapping
57 del mapping[key]
60class VisitorTestCase(unittest.TestCase):
61 """Tests for CheckingVisitor class."""
63 schema_obj: MutableMapping[str, Any] = {}
65 def setUp(self) -> None:
66 """Load data from test file."""
67 with open(TEST_YAML) as test_yaml:
68 self.schema_obj = yaml.load(test_yaml, Loader=yaml.SafeLoader)
69 self.schema_obj.update(DEFAULT_FRAME)
71 def test_check(self) -> None:
72 """Check YAML consistency using CheckingVisitor visitor."""
73 visitor = CheckingVisitor()
74 visitor.visit_schema(self.schema_obj)
76 def test_error_schema(self) -> None:
77 """Check for errors at schema level."""
78 schema = copy.deepcopy(self.schema_obj)
80 # Missing @id
81 with remove_key(schema, "@id"):
82 with self.assertRaisesRegex(ValueError, "No @id defined for object"):
83 CheckingVisitor().visit_schema(schema)
85 # Delete tables.
86 with remove_key(schema, "tables"):
87 with self.assertRaisesRegex(KeyError, "'tables'"):
88 CheckingVisitor().visit_schema(schema)
90 def test_error_table(self) -> None:
91 """Check for errors at table level."""
92 schema = copy.deepcopy(self.schema_obj)
93 table = schema["tables"][0]
95 # Missing @id
96 with remove_key(table, "@id"):
97 with self.assertRaisesRegex(ValueError, "No @id defined for object"):
98 CheckingVisitor().visit_schema(schema)
100 # Missing name.
101 with remove_key(table, "name"):
102 with self.assertRaisesRegex(ValueError, "No name for table object"):
103 CheckingVisitor().visit_schema(schema)
105 # Missing columns.
106 with remove_key(table, "columns"):
107 with self.assertRaisesRegex(KeyError, "'columns'"):
108 CheckingVisitor().visit_schema(schema)
110 # Duplicate table @id causes warning.
111 table2 = schema["tables"][1]
112 with replace_key(table, "@id", "#duplicateID"), replace_key(table2, "@id", "#duplicateID"):
113 with self.assertLogs(logger="felis", level="WARNING") as cm:
114 CheckingVisitor().visit_schema(schema)
115 self.assertEqual(cm.output, ["WARNING:felis:Duplication of @id #duplicateID"])
117 def test_error_column(self) -> None:
118 """Check for errors at column level."""
119 schema = copy.deepcopy(self.schema_obj)
120 column = schema["tables"][0]["columns"][0]
122 # Missing @id
123 with remove_key(column, "@id"):
124 with self.assertRaisesRegex(ValueError, "No @id defined for object"):
125 CheckingVisitor().visit_schema(schema)
127 # Missing name.
128 with remove_key(column, "name"):
129 with self.assertRaisesRegex(ValueError, "No name for table object"):
130 CheckingVisitor().visit_schema(schema)
132 # Missing datatype.
133 with remove_key(column, "datatype"):
134 with self.assertRaisesRegex(ValueError, "No datatype defined"):
135 CheckingVisitor().visit_schema(schema)
137 # Incorrect datatype.
138 with replace_key(column, "datatype", "nibble"):
139 with self.assertRaisesRegex(ValueError, "Incorrect Type Name"):
140 CheckingVisitor().visit_schema(schema)
142 # Duplicate @id causes warning.
143 table2 = schema["tables"][1]
144 with replace_key(column, "@id", "#duplicateID"), replace_key(table2, "@id", "#duplicateID"):
145 with self.assertLogs(logger="felis", level="WARNING") as cm:
146 CheckingVisitor().visit_schema(schema)
147 self.assertEqual(cm.output, ["WARNING:felis:Duplication of @id #duplicateID"])
149 def test_error_index(self) -> None:
150 """Check for errors at index level."""
151 schema = copy.deepcopy(self.schema_obj)
152 table = schema["tables"][0]
154 # Missing @id
155 index = {"name": "IDX_index", "columns": [table["columns"][0]["@id"]]}
156 with replace_key(table, "indexes", [index]):
157 with self.assertRaisesRegex(ValueError, "No @id defined for object"):
158 CheckingVisitor().visit_schema(schema)
160 # Missing name.
161 index = {
162 "@id": "#IDX_index",
163 "columns": [table["columns"][0]["@id"]],
164 }
165 with replace_key(table, "indexes", [index]):
166 with self.assertRaisesRegex(ValueError, "No name for table object"):
167 CheckingVisitor().visit_schema(schema)
169 # Both columns and expressions specified.
170 index = {
171 "@id": "#IDX_index",
172 "name": "IDX_index",
173 "columns": [table["columns"][0]["@id"]],
174 "expressions": ["1+2"],
175 }
176 with replace_key(table, "indexes", [index]):
177 with self.assertRaisesRegex(ValueError, "Defining columns and expressions is not valid"):
178 CheckingVisitor().visit_schema(schema)
180 # Duplicate @id causes warning.
181 index = {
182 "@id": "#duplicateID",
183 "name": "IDX_index",
184 "columns": [table["columns"][0]["@id"]],
185 }
186 table2 = schema["tables"][1]
187 with replace_key(table, "indexes", [index]), replace_key(table2, "@id", "#duplicateID"):
188 with self.assertLogs(logger="felis", level="WARNING") as cm:
189 CheckingVisitor().visit_schema(schema)
190 self.assertEqual(cm.output, ["WARNING:felis:Duplication of @id #duplicateID"])
192 def test_version_errors(self) -> None:
193 """Test errors in version specification."""
194 schema_obj: dict[str, Any] = {
195 "name": "schema",
196 "@id": "#schema",
197 "tables": [],
198 }
200 schema_obj["version"] = 1
201 with self.assertRaisesRegex(TypeError, "version description is not a string or object"):
202 CheckingVisitor().visit_schema(schema_obj)
204 schema_obj["version"] = {}
205 with self.assertRaisesRegex(ValueError, "missing 'current' key in schema version"):
206 CheckingVisitor().visit_schema(schema_obj)
208 schema_obj["version"] = {"current": 1}
209 with self.assertRaisesRegex(TypeError, "schema version 'current' value is not a string"):
210 CheckingVisitor().visit_schema(schema_obj)
212 schema_obj["version"] = {"current": "v1", "extra": "v2"}
213 with self.assertLogs("felis", "ERROR") as cm:
214 CheckingVisitor().visit_schema(schema_obj)
215 self.assertEqual(cm.output, ["ERROR:felis:unexpected keys in schema version description: ['extra']"])
217 schema_obj["version"] = {"current": "v1", "compatible": "v2"}
218 with self.assertRaisesRegex(TypeError, "schema version 'compatible' value is not a list"):
219 CheckingVisitor().visit_schema(schema_obj)
221 schema_obj["version"] = {"current": "v1", "compatible": ["1", "2", 3]}
222 with self.assertRaisesRegex(TypeError, "items in 'compatible' value are not strings"):
223 CheckingVisitor().visit_schema(schema_obj)
225 schema_obj["version"] = {"current": "v1", "read_compatible": "v2"}
226 with self.assertRaisesRegex(TypeError, "schema version 'read_compatible' value is not a list"):
227 CheckingVisitor().visit_schema(schema_obj)
229 schema_obj["version"] = {"current": "v1", "read_compatible": ["1", "2", 3]}
230 with self.assertRaisesRegex(TypeError, "items in 'read_compatible' value are not strings"):
231 CheckingVisitor().visit_schema(schema_obj)
234if __name__ == "__main__": 234 ↛ 235line 234 didn't jump to line 235, because the condition on line 234 was never true
235 unittest.main()