Coverage for tests/test_check.py: 14%

128 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-02-27 11:44 +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/>. 

21 

22import contextlib 

23import copy 

24import os 

25import unittest 

26from collections.abc import Iterator, MutableMapping 

27from typing import Any 

28 

29import yaml 

30 

31from felis import DEFAULT_FRAME, CheckingVisitor 

32 

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

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

35 

36 

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 

43 

44 

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] 

58 

59 

60class VisitorTestCase(unittest.TestCase): 

61 """Tests for CheckingVisitor class.""" 

62 

63 schema_obj: MutableMapping[str, Any] = {} 

64 

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) 

70 

71 def test_check(self) -> None: 

72 """Check YAML consistency using CheckingVisitor visitor.""" 

73 visitor = CheckingVisitor() 

74 visitor.visit_schema(self.schema_obj) 

75 

76 def test_error_schema(self) -> None: 

77 """Check for errors at schema level.""" 

78 schema = copy.deepcopy(self.schema_obj) 

79 

80 # Missing @id 

81 with remove_key(schema, "@id"): 

82 with self.assertRaisesRegex(ValueError, "No @id defined for object"): 

83 CheckingVisitor().visit_schema(schema) 

84 

85 # Delete tables. 

86 with remove_key(schema, "tables"): 

87 with self.assertRaisesRegex(KeyError, "'tables'"): 

88 CheckingVisitor().visit_schema(schema) 

89 

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] 

94 

95 # Missing @id 

96 with remove_key(table, "@id"): 

97 with self.assertRaisesRegex(ValueError, "No @id defined for object"): 

98 CheckingVisitor().visit_schema(schema) 

99 

100 # Missing name. 

101 with remove_key(table, "name"): 

102 with self.assertRaisesRegex(ValueError, "No name for table object"): 

103 CheckingVisitor().visit_schema(schema) 

104 

105 # Missing columns. 

106 with remove_key(table, "columns"): 

107 with self.assertRaisesRegex(KeyError, "'columns'"): 

108 CheckingVisitor().visit_schema(schema) 

109 

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"]) 

116 

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] 

121 

122 # Missing @id 

123 with remove_key(column, "@id"): 

124 with self.assertRaisesRegex(ValueError, "No @id defined for object"): 

125 CheckingVisitor().visit_schema(schema) 

126 

127 # Missing name. 

128 with remove_key(column, "name"): 

129 with self.assertRaisesRegex(ValueError, "No name for table object"): 

130 CheckingVisitor().visit_schema(schema) 

131 

132 # Missing datatype. 

133 with remove_key(column, "datatype"): 

134 with self.assertRaisesRegex(ValueError, "No datatype defined"): 

135 CheckingVisitor().visit_schema(schema) 

136 

137 # Incorrect datatype. 

138 with replace_key(column, "datatype", "nibble"): 

139 with self.assertRaisesRegex(ValueError, "Incorrect Type Name"): 

140 CheckingVisitor().visit_schema(schema) 

141 

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"]) 

148 

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] 

153 

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) 

159 

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) 

168 

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) 

179 

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"]) 

191 

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 } 

199 

200 schema_obj["version"] = 1 

201 with self.assertRaisesRegex(TypeError, "version description is not a string or object"): 

202 CheckingVisitor().visit_schema(schema_obj) 

203 

204 schema_obj["version"] = {} 

205 with self.assertRaisesRegex(ValueError, "missing 'current' key in schema version"): 

206 CheckingVisitor().visit_schema(schema_obj) 

207 

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) 

211 

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']"]) 

216 

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) 

220 

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) 

224 

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) 

228 

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) 

232 

233 

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

235 unittest.main()