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

Roundup Issue Tracker: http://roundup-tracker.org/