Coverage for tests / test_exprParserLex.py: 9%

196 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-22 08:55 +0000

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <https://www.gnu.org/licenses/>. 

27 

28"""Simple unit test for expr_parser/parserLex module.""" 

29 

30import re 

31import unittest 

32 

33from lsst.daf.butler.queries.expressions.parser import ParserLex, ParserLexError 

34 

35 

36class ParserLexTestCase(unittest.TestCase): 

37 """A test case for ParserLex""" 

38 

39 def _assertToken(self, token, type, value, lineno=None, lexpos=None): 

40 self.assertIsNotNone(token) 

41 self.assertEqual(token.type, type) 

42 self.assertEqual(token.value, value) 

43 if lineno is not None: 

44 self.assertEqual(token.lineno, lineno) 

45 if lexpos is not None: 

46 self.assertEqual(token.lexpos, lexpos) 

47 

48 def setUp(self): 

49 pass 

50 

51 def tearDown(self): 

52 pass 

53 

54 def testInstantiate(self): 

55 """Tests for making ParserLex instances""" 

56 default_reflags = re.IGNORECASE | re.VERBOSE 

57 lexer = ParserLex.make_lexer() 

58 self.assertEqual(lexer.lexreflags, default_reflags) 

59 

60 lexer = ParserLex.make_lexer(reflags=re.DOTALL) 

61 self.assertEqual(lexer.lexreflags, re.DOTALL | default_reflags) 

62 

63 def testSimpleTokens(self): 

64 """Test for simple tokens""" 

65 lexer = ParserLex.make_lexer() 

66 

67 lexer.input("=!= <<= >>= +-*/()") 

68 self._assertToken(lexer.token(), "EQ", "=") 

69 self._assertToken(lexer.token(), "NE", "!=") 

70 self._assertToken(lexer.token(), "LT", "<") 

71 self._assertToken(lexer.token(), "LE", "<=") 

72 self._assertToken(lexer.token(), "GT", ">") 

73 self._assertToken(lexer.token(), "GE", ">=") 

74 self._assertToken(lexer.token(), "ADD", "+") 

75 self._assertToken(lexer.token(), "SUB", "-") 

76 self._assertToken(lexer.token(), "MUL", "*") 

77 self._assertToken(lexer.token(), "DIV", "/") 

78 self._assertToken(lexer.token(), "LPAREN", "(") 

79 self._assertToken(lexer.token(), "RPAREN", ")") 

80 self.assertIsNone(lexer.token()) 

81 

82 def testReservedTokens(self): 

83 """Test for reserved words""" 

84 lexer = ParserLex.make_lexer() 

85 

86 tokens = "NOT IN OR AND OVERLAPS" 

87 lexer.input(tokens) 

88 for token in tokens.split(): 

89 self._assertToken(lexer.token(), token, token) 

90 self.assertIsNone(lexer.token()) 

91 

92 tokens = "not in or and overlaps" 

93 lexer.input(tokens) 

94 for token in tokens.split(): 

95 self._assertToken(lexer.token(), token.upper(), token.upper()) 

96 self.assertIsNone(lexer.token()) 

97 

98 # not reserved 

99 token = "NOTIN" 

100 lexer.input(token) 

101 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", token) 

102 self.assertIsNone(lexer.token()) 

103 

104 def testStringLiteral(self): 

105 """Test for string literals""" 

106 lexer = ParserLex.make_lexer() 

107 

108 lexer.input("''") 

109 self._assertToken(lexer.token(), "STRING_LITERAL", "") 

110 self.assertIsNone(lexer.token()) 

111 

112 lexer.input("'string'") 

113 self._assertToken(lexer.token(), "STRING_LITERAL", "string") 

114 self.assertIsNone(lexer.token()) 

115 

116 lexer.input("'string' 'string'\n'string'") 

117 self._assertToken(lexer.token(), "STRING_LITERAL", "string") 

118 self._assertToken(lexer.token(), "STRING_LITERAL", "string") 

119 self._assertToken(lexer.token(), "STRING_LITERAL", "string") 

120 self.assertIsNone(lexer.token()) 

121 

122 # odd newline inside string 

123 lexer.input("'string\nstring'") 

124 with self.assertRaises(ParserLexError): 

125 lexer.token() 

126 

127 lexer.input("'string") 

128 with self.assertRaises(ParserLexError): 

129 lexer.token() 

130 

131 def testNumericLiteral(self): 

132 """Test for numeric literals""" 

133 lexer = ParserLex.make_lexer() 

134 

135 lexer.input("0 100 999. 100.1 1e10 1e-10 1.e+20 .2E5") 

136 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "0") 

137 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "100") 

138 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "999.") 

139 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "100.1") 

140 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "1e10") 

141 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "1e-10") 

142 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "1.e+20") 

143 self._assertToken(lexer.token(), "NUMERIC_LITERAL", ".2E5") 

144 self.assertIsNone(lexer.token()) 

145 

146 def testRangeLiteral(self): 

147 """Test for range literals""" 

148 lexer = ParserLex.make_lexer() 

149 

150 lexer.input("0..10 -10..-1 -10..10:2 0 .. 10 0 .. 10 : 2 ") 

151 self._assertToken(lexer.token(), "RANGE_LITERAL", (0, 10, None)) 

152 self._assertToken(lexer.token(), "RANGE_LITERAL", (-10, -1, None)) 

153 self._assertToken(lexer.token(), "RANGE_LITERAL", (-10, 10, 2)) 

154 self._assertToken(lexer.token(), "RANGE_LITERAL", (0, 10, None)) 

155 self._assertToken(lexer.token(), "RANGE_LITERAL", (0, 10, 2)) 

156 self.assertIsNone(lexer.token()) 

157 

158 def testTimeLiteral(self): 

159 """Test for time literals""" 

160 lexer = ParserLex.make_lexer() 

161 

162 # string can contain anything, lexer does not check it 

163 lexer.input("T'2020-03-30' T'2020-03-30 00:00:00' T'2020-03-30T00:00:00' T'123.456' T'time'") 

164 self._assertToken(lexer.token(), "TIME_LITERAL", "2020-03-30") 

165 self._assertToken(lexer.token(), "TIME_LITERAL", "2020-03-30 00:00:00") 

166 self._assertToken(lexer.token(), "TIME_LITERAL", "2020-03-30T00:00:00") 

167 self._assertToken(lexer.token(), "TIME_LITERAL", "123.456") 

168 self._assertToken(lexer.token(), "TIME_LITERAL", "time") 

169 self.assertIsNone(lexer.token()) 

170 

171 def testIdentifier(self): 

172 """Test for numeric literals""" 

173 lexer = ParserLex.make_lexer() 

174 

175 lexer.input("ID id _012 a_b_C") 

176 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "ID") 

177 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "id") 

178 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "_012") 

179 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "a_b_C") 

180 self.assertIsNone(lexer.token()) 

181 

182 lexer.input("a.b a.b.c _._ _._._") 

183 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "a.b") 

184 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "a.b.c") 

185 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "_._") 

186 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "_._._") 

187 self.assertIsNone(lexer.token()) 

188 

189 lexer.input(".id") 

190 with self.assertRaises(ParserLexError): 

191 lexer.token() 

192 

193 lexer.input("id.") 

194 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "id") 

195 with self.assertRaises(ParserLexError): 

196 lexer.token() 

197 

198 lexer.input("id.id.id.id") 

199 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "id.id.id") 

200 with self.assertRaises(ParserLexError): 

201 lexer.token() 

202 

203 def testNinds(self): 

204 """Test for bind names""" 

205 lexer = ParserLex.make_lexer() 

206 

207 lexer.input(":name :visit :in :___") 

208 self._assertToken(lexer.token(), "BIND_NAME", "name") 

209 self._assertToken(lexer.token(), "BIND_NAME", "visit") 

210 self._assertToken(lexer.token(), "BIND_NAME", "in") 

211 self._assertToken(lexer.token(), "BIND_NAME", "___") 

212 

213 lexer.input(":1") 

214 with self.assertRaises(ParserLexError): 

215 lexer.token() 

216 

217 lexer.input(":.id") 

218 with self.assertRaises(ParserLexError): 

219 lexer.token() 

220 

221 lexer.input(":id.") 

222 self._assertToken(lexer.token(), "BIND_NAME", "id") 

223 with self.assertRaises(ParserLexError): 

224 lexer.token() 

225 

226 def testExpression(self): 

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

228 lexer = ParserLex.make_lexer() 

229 

230 expr = ( 

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

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

233 "patch.cell_y < 4 AND band='i' " 

234 "or visit IN (1..50:2)" 

235 ) 

236 tokens = ( 

237 ("LPAREN", "("), 

238 ("LPAREN", "("), 

239 ("SIMPLE_IDENTIFIER", "instrument"), 

240 ("EQ", "="), 

241 ("STRING_LITERAL", "HSC"), 

242 ("AND", "AND"), 

243 ("SIMPLE_IDENTIFIER", "detector"), 

244 ("NE", "!="), 

245 ("NUMERIC_LITERAL", "9"), 

246 ("RPAREN", ")"), 

247 ("OR", "OR"), 

248 ("SIMPLE_IDENTIFIER", "instrument"), 

249 ("EQ", "="), 

250 ("STRING_LITERAL", "CFHT"), 

251 ("RPAREN", ")"), 

252 ("AND", "AND"), 

253 ("SIMPLE_IDENTIFIER", "tract"), 

254 ("EQ", "="), 

255 ("NUMERIC_LITERAL", "8766"), 

256 ("AND", "AND"), 

257 ("QUALIFIED_IDENTIFIER", "patch.cell_x"), 

258 ("GT", ">"), 

259 ("NUMERIC_LITERAL", "5"), 

260 ("AND", "AND"), 

261 ("QUALIFIED_IDENTIFIER", "patch.cell_y"), 

262 ("LT", "<"), 

263 ("NUMERIC_LITERAL", "4"), 

264 ("AND", "AND"), 

265 ("SIMPLE_IDENTIFIER", "band"), 

266 ("EQ", "="), 

267 ("STRING_LITERAL", "i"), 

268 ("OR", "OR"), 

269 ("SIMPLE_IDENTIFIER", "visit"), 

270 ("IN", "IN"), 

271 ("LPAREN", "("), 

272 ("RANGE_LITERAL", (1, 50, 2)), 

273 ("RPAREN", ")"), 

274 ) 

275 lexer.input(expr) 

276 for type, value in tokens: 

277 self._assertToken(lexer.token(), type, value) 

278 self.assertIsNone(lexer.token()) 

279 

280 def testExceptions(self): 

281 """Test for exception contents""" 

282 

283 def _assertExc(exc, expr, remain, pos, lineno): 

284 """Check exception attribute values""" 

285 self.assertEqual(exc.expression, expr) 

286 self.assertEqual(exc.remain, remain) 

287 self.assertEqual(exc.pos, pos) 

288 self.assertEqual(exc.lineno, lineno) 

289 

290 lexer = ParserLex.make_lexer() 

291 expr = "a.b.c.d" 

292 lexer.input(expr) 

293 self._assertToken(lexer.token(), "QUALIFIED_IDENTIFIER", "a.b.c") 

294 with self.assertRaises(ParserLexError) as catcher: 

295 lexer.token() 

296 _assertExc(catcher.exception, expr, ".d", 5, 1) 

297 

298 lexer = ParserLex.make_lexer() 

299 expr = "a \n& b" 

300 lexer.input(expr) 

301 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "a") 

302 with self.assertRaises(ParserLexError) as catcher: 

303 lexer.token() 

304 _assertExc(catcher.exception, expr, "& b", 3, 2) 

305 

306 lexer = ParserLex.make_lexer() 

307 expr = "a\n=\n1e5.e2" 

308 lexer.input(expr) 

309 self._assertToken(lexer.token(), "SIMPLE_IDENTIFIER", "a") 

310 self._assertToken(lexer.token(), "EQ", "=") 

311 self._assertToken(lexer.token(), "NUMERIC_LITERAL", "1e5") 

312 with self.assertRaises(ParserLexError) as catcher: 

313 lexer.token() 

314 _assertExc(catcher.exception, expr, ".e2", 7, 3) 

315 

316 # zero stride in range literal 

317 lexer = ParserLex.make_lexer() 

318 expr = "1..2:0" 

319 lexer.input(expr) 

320 self._assertToken(lexer.token(), "RANGE_LITERAL", (1, 2, None)) 

321 with self.assertRaises(ParserLexError) as catcher: 

322 lexer.token() 

323 _assertExc(catcher.exception, expr, ":0", 4, 1) 

324 

325 # negative stride in range literal 

326 lexer = ParserLex.make_lexer() 

327 expr = "1..2:-10" 

328 lexer.input(expr) 

329 self._assertToken(lexer.token(), "RANGE_LITERAL", (1, 2, None)) 

330 with self.assertRaises(ParserLexError) as catcher: 

331 lexer.token() 

332 _assertExc(catcher.exception, expr, ":-10", 4, 1) 

333 

334 

335if __name__ == "__main__": 

336 unittest.main()