Mercurial > p > roundup > code
comparison test/db_test_base.py @ 8241:741ea8a86012
fix: issue2551374. Error handling for filter expressions.
Errors in filter expressions are now reported. The UI needs some work
but even the current code is helpful when debugging filter
expressions.
mlink_expr:
defines/raises ExpressionError(error string template,
context=dict())
raises ExpressionError when it detects errors when popping arguments
off stack
raises ExpressionError when more than one element left on the stack
before returning
also ruff fix to group boolean expression with parens
back_anydbm.py, rdbms_common.py:
catches ExpressionError, augments context with class and
attribute being searched. raises the exception
for both link and multilink relations
client.py
catches ExpressionError returning a basic error page. The page is a
dead end. There are no links or anything for the user to move
forward. The user has to go back, possibly refresh the page (because
the submit button may be disalbled) re-enter the query and try
again.
This needs to be improved.
test_liveserver.py
test the error page generated by client.py
db_test_base
unit tests for filter with too few arguments, too many arguments,
check all repr and str formats.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Mon, 30 Dec 2024 20:22:55 -0500 |
| parents | 079958914ed7 |
| children | 05d8806b25ad |
comparison
equal
deleted
inserted
replaced
| 8240:1189c742e4b3 | 8241:741ea8a86012 |
|---|---|
| 39 from roundup.cgi.templating import HTMLProperty, _HTMLItem, anti_csrf_nonce | 39 from roundup.cgi.templating import HTMLProperty, _HTMLItem, anti_csrf_nonce |
| 40 from roundup.cgi import client, actions | 40 from roundup.cgi import client, actions |
| 41 from roundup.cgi.engine_zopetal import RoundupPageTemplate | 41 from roundup.cgi.engine_zopetal import RoundupPageTemplate |
| 42 from roundup.cgi.templating import HTMLItem | 42 from roundup.cgi.templating import HTMLItem |
| 43 from roundup.exceptions import UsageError, Reject | 43 from roundup.exceptions import UsageError, Reject |
| 44 from roundup.mlink_expr import ExpressionError | |
| 44 | 45 |
| 45 from roundup.anypy.strings import b2s, s2b, u2s | 46 from roundup.anypy.strings import b2s, s2b, u2s |
| 46 from roundup.anypy.cmp_ import NoneAndDictComparable | 47 from roundup.anypy.cmp_ import NoneAndDictComparable |
| 47 from roundup.anypy.email_ import message_from_bytes | 48 from roundup.anypy.email_ import message_from_bytes |
| 48 | 49 |
| 2046 ae(filt(None, {a: None}, ('+','id'), grp), ['3','4']) | 2047 ae(filt(None, {a: None}, ('+','id'), grp), ['3','4']) |
| 2047 ae(filt(None, {a: [None]}, ('+','id'), grp), ['3','4']) | 2048 ae(filt(None, {a: [None]}, ('+','id'), grp), ['3','4']) |
| 2048 ae(filt(None, {a: ['-1', None]}, ('+','id'), grp), ['3','4']) | 2049 ae(filt(None, {a: ['-1', None]}, ('+','id'), grp), ['3','4']) |
| 2049 ae(filt(None, {a: ['1', None]}, ('+','id'), grp), ['1', '3','4']) | 2050 ae(filt(None, {a: ['1', None]}, ('+','id'), grp), ['1', '3','4']) |
| 2050 | 2051 |
| 2052 def testFilteringBrokenLinkExpression(self): | |
| 2053 ae, iiter = self.filteringSetup() | |
| 2054 a = 'assignedto' | |
| 2055 for filt in iiter(): | |
| 2056 with self.assertRaises(ExpressionError) as e: | |
| 2057 filt(None, {a: ['1', '-3']}, ('+','status')) | |
| 2058 self.assertIn("position 2", str(e.exception)) | |
| 2059 # verify all tokens are consumed | |
| 2060 self.assertNotIn("%", str(e.exception)) | |
| 2061 self.assertNotIn("%", repr(e.exception)) | |
| 2062 | |
| 2063 with self.assertRaises(ExpressionError) as e: | |
| 2064 filt(None, {a: ['0', '1', '2', '3', '-3', '-4']}, | |
| 2065 ('+','status')) | |
| 2066 self.assertIn("values on the stack are: [Value 0,", | |
| 2067 str(e.exception)) | |
| 2068 self.assertNotIn("%", str(e.exception)) | |
| 2069 self.assertNotIn("%", repr(e.exception)) | |
| 2070 | |
| 2071 with self.assertRaises(ExpressionError) as e: | |
| 2072 # expression tests _repr_ for every operator and | |
| 2073 # three values | |
| 2074 filt(None, {a: ['-1', '1', '2', '3', '-3', '-2', '-4']}, | |
| 2075 ('+','status')) | |
| 2076 result = str(e.exception) | |
| 2077 self.assertIn(" AND ", result) | |
| 2078 self.assertIn(" OR ", result) | |
| 2079 self.assertIn("NOT(", result) | |
| 2080 self.assertIn("ISEMPTY(-1)", result) | |
| 2081 # trigger a KeyError and verify fallback format | |
| 2082 # is correct. It includes the template with %(name)s tokens, | |
| 2083 del(e.exception.context['class']) | |
| 2084 self.assertIn("values on the stack are: %(stack)s", | |
| 2085 str(e.exception)) | |
| 2086 self.assertIn("values on the stack are: %(stack)s", | |
| 2087 repr(e.exception)) | |
| 2088 | |
| 2051 def testFilteringLinkExpression(self): | 2089 def testFilteringLinkExpression(self): |
| 2052 ae, iiter = self.filteringSetup() | 2090 ae, iiter = self.filteringSetup() |
| 2053 a = 'assignedto' | 2091 a = 'assignedto' |
| 2054 for filt in iiter(): | 2092 for filt in iiter(): |
| 2055 ae(filt(None, {}, ('+',a)), ['3','4','1','2']) | 2093 ae(filt(None, {}, ('+',a)), ['3','4','1','2']) |
| 2072 ae(filt(None, {a: ['1','2','-4']}, ('+',a)), ['1','2']) | 2110 ae(filt(None, {a: ['1','2','-4']}, ('+',a)), ['1','2']) |
| 2073 ae(filt(None, {a: ['1','-2','2','-2','-3']}, ('+',a)), ['3','4']) | 2111 ae(filt(None, {a: ['1','-2','2','-2','-3']}, ('+',a)), ['3','4']) |
| 2074 ae(filt(None, {a: ['1','-2','2','-2','-4']}, ('+',a)), | 2112 ae(filt(None, {a: ['1','-2','2','-2','-4']}, ('+',a)), |
| 2075 ['3','4','1','2']) | 2113 ['3','4','1','2']) |
| 2076 | 2114 |
| 2077 def NOtestFilteringLinkInvalid(self): | |
| 2078 """Test invalid filter expressions. | |
| 2079 Code must be written to generate an exception for these. | |
| 2080 Then this code must be changed to test the exception. | |
| 2081 See issue 2551374 in the roundup tracker. | |
| 2082 """ | |
| 2083 ae, iiter = self.filteringSetup() | |
| 2084 a = 'assignedto' | |
| 2085 # filt(??, filter expression, sort order) | |
| 2086 # ae = self.assertEqual(val1, val2) | |
| 2087 for filt in iiter(): | |
| 2088 # -2 is missing an operand | |
| 2089 ae(filt(None, {a: ['-2','2','-2','-4']}, | |
| 2090 ('+',a)), []) | |
| 2091 # 3 is left on the stack | |
| 2092 ae(filt(None, {a: ['3','2','-2']}, | |
| 2093 ('+',a)), []) | |
| 2094 | |
| 2095 | |
| 2096 def testFilteringRevLink(self): | 2115 def testFilteringRevLink(self): |
| 2097 ae, iiter = self.filteringSetupTransitiveSearch('user') | 2116 ae, iiter = self.filteringSetupTransitiveSearch('user') |
| 2098 # We have | 2117 # We have |
| 2099 # issue assignedto | 2118 # issue assignedto |
| 2100 # 1: 6 | 2119 # 1: 6 |
| 2225 for filt in iiter(): | 2244 for filt in iiter(): |
| 2226 ae(filt(None, {'nosy': '3'}, ('+','id'), (None,None)), ['4']) | 2245 ae(filt(None, {'nosy': '3'}, ('+','id'), (None,None)), ['4']) |
| 2227 ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2']) | 2246 ae(filt(None, {'nosy': '-1'}, ('+','id'), (None,None)), ['1', '2']) |
| 2228 ae(filt(None, {'nosy': ['1','2']}, ('+', 'status'), | 2247 ae(filt(None, {'nosy': ['1','2']}, ('+', 'status'), |
| 2229 ('-', 'deadline')), ['4', '3']) | 2248 ('-', 'deadline')), ['4', '3']) |
| 2249 | |
| 2250 def testFilteringBrokenMultilinkExpression(self): | |
| 2251 ae, iiter = self.filteringSetup() | |
| 2252 kw1 = self.db.keyword.create(name='Key1') | |
| 2253 kw2 = self.db.keyword.create(name='Key2') | |
| 2254 kw3 = self.db.keyword.create(name='Key3') | |
| 2255 kw4 = self.db.keyword.create(name='Key4') | |
| 2256 self.db.issue.set('1', keywords=[kw1, kw2]) | |
| 2257 self.db.issue.set('2', keywords=[kw1, kw3]) | |
| 2258 self.db.issue.set('3', keywords=[kw2, kw3, kw4]) | |
| 2259 self.db.issue.set('4', keywords=[kw1, kw2, kw4]) | |
| 2260 self.db.commit() | |
| 2261 kw = 'keywords' | |
| 2262 for filt in iiter(): | |
| 2263 with self.assertRaises(ExpressionError) as e: | |
| 2264 filt(None, {kw: ['1', '-3']}, ('+','status')) | |
| 2265 self.assertIn("searching issue by keywords", str(e.exception)) | |
| 2266 self.assertIn("position 2", str(e.exception)) | |
| 2267 # verify all tokens are consumed | |
| 2268 self.assertNotIn("%", str(e.exception)) | |
| 2269 self.assertNotIn("%", repr(e.exception)) | |
| 2270 | |
| 2271 with self.assertRaises(ExpressionError) as e: | |
| 2272 filt(None, {kw: ['0', '1', '2', '3', '-3', '-4']}, | |
| 2273 ('+','status')) | |
| 2274 self.assertIn("searching issue by keywords", str(e.exception)) | |
| 2275 self.assertIn("values on the stack are: [Value 0,", | |
| 2276 str(e.exception)) | |
| 2277 self.assertNotIn("%", str(e.exception)) | |
| 2278 self.assertNotIn("%", repr(e.exception)) | |
| 2279 | |
| 2280 with self.assertRaises(ExpressionError) as e: | |
| 2281 # expression tests _repr_ for every operator and | |
| 2282 # three values | |
| 2283 filt(None, {kw: ['-1', '1', '2', '3', '-3', '-2', '-4']}, | |
| 2284 ('+','status')) | |
| 2285 result = str(e.exception) | |
| 2286 self.assertIn("searching issue by keywords", result) | |
| 2287 self.assertIn(" AND ", result) | |
| 2288 self.assertIn(" OR ", result) | |
| 2289 self.assertIn("NOT(", result) | |
| 2290 self.assertIn("ISEMPTY(-1)", result) | |
| 2291 # trigger a KeyError and verify fallback format | |
| 2292 # is correct. It includes the template with %(name)s tokens, | |
| 2293 del(e.exception.context['class']) | |
| 2294 self.assertIn("values on the stack are: %(stack)s", | |
| 2295 str(e.exception)) | |
| 2296 self.assertIn("values on the stack are: %(stack)s", | |
| 2297 repr(e.exception)) | |
| 2298 | |
| 2230 | 2299 |
| 2231 def testFilteringMultilinkExpression(self): | 2300 def testFilteringMultilinkExpression(self): |
| 2232 ae, iiter = self.filteringSetup() | 2301 ae, iiter = self.filteringSetup() |
| 2233 kw1 = self.db.keyword.create(name='Key1') | 2302 kw1 = self.db.keyword.create(name='Key1') |
| 2234 kw2 = self.db.keyword.create(name='Key2') | 2303 kw2 = self.db.keyword.create(name='Key2') |
