Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of daf_butler. 

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 

22"""Simple unit test for exprParser subpackage module. 

23""" 

24 

25import unittest 

26 

27from lsst.daf.butler.registry.queries.exprParser import exprTree, TreeVisitor, ParserYacc, ParseError 

28 

29 

30class _Visitor(TreeVisitor): 

31 """Trivial implementation of TreeVisitor. 

32 """ 

33 def visitNumericLiteral(self, value, node): 

34 return f"N({value})" 

35 

36 def visitStringLiteral(self, value, node): 

37 return f"S({value})" 

38 

39 def visitRangeLiteral(self, start, stop, stride, node): 

40 if stride is None: 

41 return f"R({start}..{stop})" 

42 else: 

43 return f"R({start}..{stop}:{stride})" 

44 

45 def visitIdentifier(self, name, node): 

46 return f"ID({name})" 

47 

48 def visitUnaryOp(self, operator, operand, node): 

49 return f"U({operator} {operand})" 

50 

51 def visitBinaryOp(self, operator, lhs, rhs, node): 

52 return f"B({lhs} {operator} {rhs})" 

53 

54 def visitIsIn(self, lhs, values, not_in, node): 

55 values = ", ".join([str(val) for val in values]) 

56 if not_in: 

57 return f"!IN({lhs} ({values}))" 

58 else: 

59 return f"IN({lhs} ({values}))" 

60 

61 def visitParens(self, expression, node): 

62 return f"P({expression})" 

63 

64 

65class ParserLexTestCase(unittest.TestCase): 

66 """A test case for ParserYacc 

67 """ 

68 

69 def setUp(self): 

70 pass 

71 

72 def tearDown(self): 

73 pass 

74 

75 def testInstantiate(self): 

76 """Tests for making ParserLex instances 

77 """ 

78 parser = ParserYacc() # noqa: F841 

79 

80 def testEmpty(self): 

81 """Tests for empty expression 

82 """ 

83 parser = ParserYacc() 

84 

85 # empty expression is allowed, returns None 

86 tree = parser.parse("") 

87 self.assertIsNone(tree) 

88 

89 def testParseLiteral(self): 

90 """Tests for literals (strings/numbers) 

91 """ 

92 parser = ParserYacc() 

93 

94 tree = parser.parse('1') 

95 self.assertIsInstance(tree, exprTree.NumericLiteral) 

96 self.assertEqual(tree.value, '1') 

97 

98 tree = parser.parse('.5e-2') 

99 self.assertIsInstance(tree, exprTree.NumericLiteral) 

100 self.assertEqual(tree.value, '.5e-2') 

101 

102 tree = parser.parse("'string'") 

103 self.assertIsInstance(tree, exprTree.StringLiteral) 

104 self.assertEqual(tree.value, 'string') 

105 

106 tree = parser.parse("10..20") 

107 self.assertIsInstance(tree, exprTree.RangeLiteral) 

108 self.assertEqual(tree.start, 10) 

109 self.assertEqual(tree.stop, 20) 

110 self.assertEqual(tree.stride, None) 

111 

112 tree = parser.parse("-10 .. 10:5") 

113 self.assertIsInstance(tree, exprTree.RangeLiteral) 

114 self.assertEqual(tree.start, -10) 

115 self.assertEqual(tree.stop, 10) 

116 self.assertEqual(tree.stride, 5) 

117 

118 def testParseIdentifiers(self): 

119 """Tests for identifiers 

120 """ 

121 parser = ParserYacc() 

122 

123 tree = parser.parse('a') 

124 self.assertIsInstance(tree, exprTree.Identifier) 

125 self.assertEqual(tree.name, 'a') 

126 

127 tree = parser.parse('a.b') 

128 self.assertIsInstance(tree, exprTree.Identifier) 

129 self.assertEqual(tree.name, 'a.b') 

130 

131 def testParseParens(self): 

132 """Tests for identifiers 

133 """ 

134 parser = ParserYacc() 

135 

136 tree = parser.parse('(a)') 

137 self.assertIsInstance(tree, exprTree.Parens) 

138 self.assertIsInstance(tree.expr, exprTree.Identifier) 

139 self.assertEqual(tree.expr.name, 'a') 

140 

141 def testUnaryOps(self): 

142 """Tests for unary plus and minus 

143 """ 

144 parser = ParserYacc() 

145 

146 tree = parser.parse('+a') 

147 self.assertIsInstance(tree, exprTree.UnaryOp) 

148 self.assertEqual(tree.op, '+') 

149 self.assertIsInstance(tree.operand, exprTree.Identifier) 

150 self.assertEqual(tree.operand.name, 'a') 

151 

152 tree = parser.parse('- x.y') 

153 self.assertIsInstance(tree, exprTree.UnaryOp) 

154 self.assertEqual(tree.op, '-') 

155 self.assertIsInstance(tree.operand, exprTree.Identifier) 

156 self.assertEqual(tree.operand.name, 'x.y') 

157 

158 def testBinaryOps(self): 

159 """Tests for binary operators 

160 """ 

161 parser = ParserYacc() 

162 

163 tree = parser.parse('a + b') 

164 self.assertIsInstance(tree, exprTree.BinaryOp) 

165 self.assertEqual(tree.op, '+') 

166 self.assertIsInstance(tree.lhs, exprTree.Identifier) 

167 self.assertIsInstance(tree.rhs, exprTree.Identifier) 

168 self.assertEqual(tree.lhs.name, 'a') 

169 self.assertEqual(tree.rhs.name, 'b') 

170 

171 tree = parser.parse('a - 2') 

172 self.assertIsInstance(tree, exprTree.BinaryOp) 

173 self.assertEqual(tree.op, '-') 

174 self.assertIsInstance(tree.lhs, exprTree.Identifier) 

175 self.assertIsInstance(tree.rhs, exprTree.NumericLiteral) 

176 self.assertEqual(tree.lhs.name, 'a') 

177 self.assertEqual(tree.rhs.value, '2') 

178 

179 tree = parser.parse('2 * 2') 

180 self.assertIsInstance(tree, exprTree.BinaryOp) 

181 self.assertEqual(tree.op, '*') 

182 self.assertIsInstance(tree.lhs, exprTree.NumericLiteral) 

183 self.assertIsInstance(tree.rhs, exprTree.NumericLiteral) 

184 self.assertEqual(tree.lhs.value, '2') 

185 self.assertEqual(tree.rhs.value, '2') 

186 

187 tree = parser.parse('1.e5/2') 

188 self.assertIsInstance(tree, exprTree.BinaryOp) 

189 self.assertEqual(tree.op, '/') 

190 self.assertIsInstance(tree.lhs, exprTree.NumericLiteral) 

191 self.assertIsInstance(tree.rhs, exprTree.NumericLiteral) 

192 self.assertEqual(tree.lhs.value, '1.e5') 

193 self.assertEqual(tree.rhs.value, '2') 

194 

195 tree = parser.parse('333%76') 

196 self.assertIsInstance(tree, exprTree.BinaryOp) 

197 self.assertEqual(tree.op, '%') 

198 self.assertIsInstance(tree.lhs, exprTree.NumericLiteral) 

199 self.assertIsInstance(tree.rhs, exprTree.NumericLiteral) 

200 self.assertEqual(tree.lhs.value, '333') 

201 self.assertEqual(tree.rhs.value, '76') 

202 

203 def testIsIn(self): 

204 """Tests for IN 

205 """ 

206 parser = ParserYacc() 

207 

208 tree = parser.parse("a in (1,2,'X')") 

209 self.assertIsInstance(tree, exprTree.IsIn) 

210 self.assertFalse(tree.not_in) 

211 self.assertIsInstance(tree.lhs, exprTree.Identifier) 

212 self.assertEqual(tree.lhs.name, 'a') 

213 self.assertIsInstance(tree.values, list) 

214 self.assertEqual(len(tree.values), 3) 

215 self.assertIsInstance(tree.values[0], exprTree.NumericLiteral) 

216 self.assertEqual(tree.values[0].value, '1') 

217 self.assertIsInstance(tree.values[1], exprTree.NumericLiteral) 

218 self.assertEqual(tree.values[1].value, '2') 

219 self.assertIsInstance(tree.values[2], exprTree.StringLiteral) 

220 self.assertEqual(tree.values[2].value, 'X') 

221 

222 tree = parser.parse("10 not in (1000, 2000..3000:100)") 

223 self.assertIsInstance(tree, exprTree.IsIn) 

224 self.assertTrue(tree.not_in) 

225 self.assertIsInstance(tree.lhs, exprTree.NumericLiteral) 

226 self.assertEqual(tree.lhs.value, '10') 

227 self.assertIsInstance(tree.values, list) 

228 self.assertEqual(len(tree.values), 2) 

229 self.assertIsInstance(tree.values[0], exprTree.NumericLiteral) 

230 self.assertEqual(tree.values[0].value, '1000') 

231 self.assertIsInstance(tree.values[1], exprTree.RangeLiteral) 

232 self.assertEqual(tree.values[1].start, 2000) 

233 self.assertEqual(tree.values[1].stop, 3000) 

234 self.assertEqual(tree.values[1].stride, 100) 

235 

236 tree = parser.parse("10 in (-1000, -2000)") 

237 self.assertIsInstance(tree, exprTree.IsIn) 

238 self.assertFalse(tree.not_in) 

239 self.assertIsInstance(tree.lhs, exprTree.NumericLiteral) 

240 self.assertEqual(tree.lhs.value, '10') 

241 self.assertIsInstance(tree.values, list) 

242 self.assertEqual(len(tree.values), 2) 

243 self.assertIsInstance(tree.values[0], exprTree.NumericLiteral) 

244 self.assertEqual(tree.values[0].value, '-1000') 

245 self.assertIsInstance(tree.values[1], exprTree.NumericLiteral) 

246 self.assertEqual(tree.values[1].value, '-2000') 

247 

248 def testCompareOps(self): 

249 """Tests for comparison operators 

250 """ 

251 parser = ParserYacc() 

252 

253 for op in ('=', '!=', '<', '<=', '>', '>='): 

254 tree = parser.parse('a {} 10'.format(op)) 

255 self.assertIsInstance(tree, exprTree.BinaryOp) 

256 self.assertEqual(tree.op, op) 

257 self.assertIsInstance(tree.lhs, exprTree.Identifier) 

258 self.assertIsInstance(tree.rhs, exprTree.NumericLiteral) 

259 self.assertEqual(tree.lhs.name, 'a') 

260 self.assertEqual(tree.rhs.value, '10') 

261 

262 def testBoolOps(self): 

263 """Tests for boolean operators 

264 """ 

265 parser = ParserYacc() 

266 

267 for op in ('OR', 'AND'): 

268 tree = parser.parse('a {} b'.format(op)) 

269 self.assertIsInstance(tree, exprTree.BinaryOp) 

270 self.assertEqual(tree.op, op) 

271 self.assertIsInstance(tree.lhs, exprTree.Identifier) 

272 self.assertIsInstance(tree.rhs, exprTree.Identifier) 

273 self.assertEqual(tree.lhs.name, 'a') 

274 self.assertEqual(tree.rhs.name, 'b') 

275 

276 tree = parser.parse('NOT b') 

277 self.assertIsInstance(tree, exprTree.UnaryOp) 

278 self.assertEqual(tree.op, 'NOT') 

279 self.assertIsInstance(tree.operand, exprTree.Identifier) 

280 self.assertEqual(tree.operand.name, 'b') 

281 

282 def testExpression(self): 

283 """Test for more or less complete expression""" 

284 parser = ParserYacc() 

285 

286 expression = ("((instrument='HSC' AND detector != 9) OR instrument='CFHT') " 

287 "AND tract=8766 AND patch.cell_x > 5 AND " 

288 "patch.cell_y < 4 AND abstract_filter='i'") 

289 

290 tree = parser.parse(expression) 

291 self.assertIsInstance(tree, exprTree.BinaryOp) 

292 self.assertEqual(tree.op, 'AND') 

293 self.assertIsInstance(tree.lhs, exprTree.BinaryOp) 

294 # AND is left-associative, so rhs operand will be the 

295 # last sub-expressions 

296 self.assertIsInstance(tree.rhs, exprTree.BinaryOp) 

297 self.assertEqual(tree.rhs.op, '=') 

298 self.assertIsInstance(tree.rhs.lhs, exprTree.Identifier) 

299 self.assertEqual(tree.rhs.lhs.name, 'abstract_filter') 

300 self.assertIsInstance(tree.rhs.rhs, exprTree.StringLiteral) 

301 self.assertEqual(tree.rhs.rhs.value, 'i') 

302 

303 def testException(self): 

304 """Test for exceptional cases""" 

305 

306 def _assertExc(exc, expr, token, pos, lineno, posInLine): 

307 """Check exception attribute values""" 

308 self.assertEqual(exc.expression, expr) 

309 self.assertEqual(exc.token, token) 

310 self.assertEqual(exc.pos, pos) 

311 self.assertEqual(exc.lineno, lineno) 

312 self.assertEqual(exc.posInLine, posInLine) 

313 

314 parser = ParserYacc() 

315 

316 expression = "(1, 2, 3)" 

317 with self.assertRaises(ParseError) as catcher: 

318 parser.parse(expression) 

319 _assertExc(catcher.exception, expression, ",", 2, 1, 2) 

320 

321 expression = "\n(1\n,\n 2, 3)" 

322 with self.assertRaises(ParseError) as catcher: 

323 parser.parse(expression) 

324 _assertExc(catcher.exception, expression, ",", 4, 3, 0) 

325 

326 def testStr(self): 

327 """Test for formatting""" 

328 parser = ParserYacc() 

329 

330 tree = parser.parse("(a+b)") 

331 self.assertEqual(str(tree), '(a + b)') 

332 

333 tree = parser.parse("1 in (1,'x',3)") 

334 self.assertEqual(str(tree), "1 IN (1, 'x', 3)") 

335 

336 tree = parser.parse("a not in (1,'x',3)") 

337 self.assertEqual(str(tree), "a NOT IN (1, 'x', 3)") 

338 

339 tree = parser.parse("(A or B) And NoT (x+3 > y)") 

340 self.assertEqual(str(tree), "(A OR B) AND NOT (x + 3 > y)") 

341 

342 tree = parser.parse("A in (100, 200..300:50)") 

343 self.assertEqual(str(tree), "A IN (100, 200..300:50)") 

344 

345 def testVisit(self): 

346 """Test for visitor methods""" 

347 

348 # test should cover all visit* methods 

349 parser = ParserYacc() 

350 visitor = _Visitor() 

351 

352 tree = parser.parse("(a+b)") 

353 result = tree.visit(visitor) 

354 self.assertEqual(result, "P(B(ID(a) + ID(b)))") 

355 

356 tree = parser.parse("(A or B) and not (x + 3 > y)") 

357 result = tree.visit(visitor) 

358 self.assertEqual(result, "B(P(B(ID(A) OR ID(B))) AND U(NOT P(B(B(ID(x) + N(3)) > ID(y)))))") 

359 

360 tree = parser.parse("x in (1,2) AND y NOT IN (1.1, .25, 1e2) OR z in ('a', 'b')") 

361 result = tree.visit(visitor) 

362 self.assertEqual(result, "B(B(IN(ID(x) (N(1), N(2))) AND !IN(ID(y) (N(1.1), N(.25), N(1e2))))" 

363 " OR IN(ID(z) (S(a), S(b))))") 

364 

365 tree = parser.parse("x in (1,2,5..15) AND y NOT IN (-100..100:10)") 

366 result = tree.visit(visitor) 

367 self.assertEqual(result, "B(IN(ID(x) (N(1), N(2), R(5..15))) AND !IN(ID(y) (R(-100..100:10))))") 

368 

369 

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

371 unittest.main()