Coverage for idle_test/test_searchengine.py: 97%
212 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"Test searchengine, coverage 99%."
3from idlelib import searchengine as se
4import unittest
5# from test.support import requires
6from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
7from tkinter import messagebox
8from idlelib.idle_test.mock_tk import Var, Mbox
9from idlelib.idle_test.mock_tk import Text as mockText
10import re
12# With mock replacements, the module does not use any gui widgets.
13# The use of tk.Text is avoided (for now, until mock Text is improved)
14# by patching instances with an index function returning what is needed.
15# This works because mock Text.get does not use .index.
16# The tkinter imports are used to restore searchengine.
18def setUpModule():
19 # Replace s-e module tkinter imports other than non-gui TclError.
20 se.BooleanVar = Var
21 se.StringVar = Var
22 se.messagebox = Mbox
24def tearDownModule():
25 # Restore 'just in case', though other tests should also replace.
26 se.BooleanVar = BooleanVar
27 se.StringVar = StringVar
28 se.messagebox = messagebox
31class Mock:
32 def __init__(self, *args, **kwargs): pass 1aj
34class GetTest(unittest.TestCase):
35 # SearchEngine.get returns singleton created & saved on first call.
36 def test_get(self):
37 saved_Engine = se.SearchEngine 1j
38 se.SearchEngine = Mock # monkey-patch class 1j
39 try: 1j
40 root = Mock() 1j
41 engine = se.get(root) 1j
42 self.assertIsInstance(engine, se.SearchEngine) 1j
43 self.assertIs(root._searchengine, engine) 1j
44 self.assertIs(se.get(root), engine) 1j
45 finally:
46 se.SearchEngine = saved_Engine # restore class to module 1j
48class GetLineColTest(unittest.TestCase):
49 # Test simple text-independent helper function
50 def test_get_line_col(self):
51 self.assertEqual(se.get_line_col('1.0'), (1, 0)) 1m
52 self.assertEqual(se.get_line_col('1.11'), (1, 11)) 1m
54 self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend')) 1m
55 self.assertRaises(ValueError, se.get_line_col, ('end')) 1m
57class GetSelectionTest(unittest.TestCase):
58 # Test text-dependent helper function.
59## # Need gui for text.index('sel.first/sel.last/insert').
60## @classmethod
61## def setUpClass(cls):
62## requires('gui')
63## cls.root = Tk()
64##
65## @classmethod
66## def tearDownClass(cls):
67## cls.root.destroy()
68## del cls.root
70 def test_get_selection(self):
71 # text = Text(master=self.root)
72 text = mockText() 1g
73 text.insert('1.0', 'Hello World!') 1g
75 # fix text.index result when called in get_selection
76 def sel(s): 1g
77 # select entire text, cursor irrelevant
78 if s == 'sel.first': return '1.0' 1g
79 if s == 'sel.last': return '1.12' 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never false1g
80 raise TclError
81 text.index = sel # replaces .tag_add('sel', '1.0, '1.12') 1g
82 self.assertEqual(se.get_selection(text), ('1.0', '1.12')) 1g
84 def mark(s): 1g
85 # no selection, cursor after 'Hello'
86 if s == 'insert': return '1.5' 1g
87 raise TclError 1g
88 text.index = mark # replaces .mark_set('insert', '1.5') 1g
89 self.assertEqual(se.get_selection(text), ('1.5', '1.5')) 1g
92class ReverseSearchTest(unittest.TestCase):
93 # Test helper function that searches backwards within a line.
94 def test_search_reverse(self):
95 Equal = self.assertEqual 1k
96 line = "Here is an 'is' test text." 1k
97 prog = re.compile('is') 1k
98 Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14)) 1k
99 Equal(se.search_reverse(prog, line, 14).span(), (12, 14)) 1k
100 Equal(se.search_reverse(prog, line, 13).span(), (5, 7)) 1k
101 Equal(se.search_reverse(prog, line, 7).span(), (5, 7)) 1k
102 Equal(se.search_reverse(prog, line, 6), None) 1k
105class SearchEngineTest(unittest.TestCase):
106 # Test class methods that do not use Text widget.
108 def setUp(self):
109 self.engine = se.SearchEngine(root=None)
110 # Engine.root is only used to create error message boxes.
111 # The mock replacement ignores the root argument.
113 def test_is_get(self):
114 engine = self.engine 1c
115 Equal = self.assertEqual 1c
117 Equal(engine.getpat(), '') 1c
118 engine.setpat('hello') 1c
119 Equal(engine.getpat(), 'hello') 1c
121 Equal(engine.isre(), False) 1c
122 engine.revar.set(1) 1c
123 Equal(engine.isre(), True) 1c
125 Equal(engine.iscase(), False) 1c
126 engine.casevar.set(1) 1c
127 Equal(engine.iscase(), True) 1c
129 Equal(engine.isword(), False) 1c
130 engine.wordvar.set(1) 1c
131 Equal(engine.isword(), True) 1c
133 Equal(engine.iswrap(), True) 1c
134 engine.wrapvar.set(0) 1c
135 Equal(engine.iswrap(), False) 1c
137 Equal(engine.isback(), False) 1c
138 engine.backvar.set(1) 1c
139 Equal(engine.isback(), True) 1c
141 def test_setcookedpat(self):
142 engine = self.engine 1l
143 engine.setcookedpat(r'\s') 1l
144 self.assertEqual(engine.getpat(), r'\s') 1l
145 engine.revar.set(1) 1l
146 engine.setcookedpat(r'\s') 1l
147 self.assertEqual(engine.getpat(), r'\\s') 1l
149 def test_getcookedpat(self):
150 engine = self.engine 1h
151 Equal = self.assertEqual 1h
153 Equal(engine.getcookedpat(), '') 1h
154 engine.setpat('hello') 1h
155 Equal(engine.getcookedpat(), 'hello') 1h
156 engine.wordvar.set(True) 1h
157 Equal(engine.getcookedpat(), r'\bhello\b') 1h
158 engine.wordvar.set(False) 1h
160 engine.setpat(r'\s') 1h
161 Equal(engine.getcookedpat(), r'\\s') 1h
162 engine.revar.set(True) 1h
163 Equal(engine.getcookedpat(), r'\s') 1h
165 def test_getprog(self):
166 engine = self.engine 1d
167 Equal = self.assertEqual 1d
169 engine.setpat('Hello') 1d
170 temppat = engine.getprog() 1d
171 Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern) 1d
172 engine.casevar.set(1) 1d
173 temppat = engine.getprog() 1d
174 Equal(temppat.pattern, re.compile('Hello').pattern, 0) 1d
176 engine.setpat('') 1d
177 Equal(engine.getprog(), None) 1d
178 Equal(Mbox.showerror.message, 1d
179 'Error: Empty regular expression')
180 engine.setpat('+') 1d
181 engine.revar.set(1) 1d
182 Equal(engine.getprog(), None) 1d
183 Equal(Mbox.showerror.message, 1d
184 'Error: nothing to repeat\nPattern: +\nOffset: 0')
186 def test_report_error(self):
187 showerror = Mbox.showerror 1i
188 Equal = self.assertEqual 1i
189 pat = '[a-z' 1i
190 msg = 'unexpected end of regular expression' 1i
192 Equal(self.engine.report_error(pat, msg), None) 1i
193 Equal(showerror.title, 'Regular expression error') 1i
194 expected_message = ("Error: " + msg + "\nPattern: [a-z") 1i
195 Equal(showerror.message, expected_message) 1i
197 Equal(self.engine.report_error(pat, msg, 5), None) 1i
198 Equal(showerror.title, 'Regular expression error') 1i
199 expected_message += "\nOffset: 5" 1i
200 Equal(showerror.message, expected_message) 1i
203class SearchTest(unittest.TestCase):
204 # Test that search_text makes right call to right method.
206 @classmethod
207 def setUpClass(cls):
208## requires('gui')
209## cls.root = Tk()
210## cls.text = Text(master=cls.root)
211 cls.text = mockText()
212 test_text = (
213 'First line\n'
214 'Line with target\n'
215 'Last line\n')
216 cls.text.insert('1.0', test_text)
217 cls.pat = re.compile('target')
219 cls.engine = se.SearchEngine(None)
220 cls.engine.search_forward = lambda *args: ('f', args) 1ab
221 cls.engine.search_backward = lambda *args: ('b', args) 1ab
223## @classmethod
224## def tearDownClass(cls):
225## cls.root.destroy()
226## del cls.root
228 def test_search(self):
229 Equal = self.assertEqual 1b
230 engine = self.engine 1b
231 search = engine.search_text 1b
232 text = self.text 1b
233 pat = self.pat 1b
235 engine.patvar.set(None) 1b
236 #engine.revar.set(pat)
237 Equal(search(text), None) 1b
239 def mark(s): 1b
240 # no selection, cursor after 'Hello'
241 if s == 'insert': return '1.5' 1b
242 raise TclError 1b
243 text.index = mark 1b
244 Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False))) 1b
245 engine.wrapvar.set(False) 1b
246 Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False))) 1b
247 engine.wrapvar.set(True) 1b
248 engine.backvar.set(True) 1b
249 Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False))) 1b
250 engine.backvar.set(False) 1b
252 def sel(s): 1b
253 if s == 'sel.first': return '2.10' 1b
254 if s == 'sel.last': return '2.16' 254 ↛ 255line 254 didn't jump to line 255, because the condition on line 254 was never false1b
255 raise TclError
256 text.index = sel 1b
257 Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False))) 1b
258 Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True))) 1b
259 engine.backvar.set(True) 1b
260 Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False))) 1b
261 Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True))) 1b
264class ForwardBackwardTest(unittest.TestCase):
265 # Test that search_forward method finds the target.
266## @classmethod
267## def tearDownClass(cls):
268## cls.root.destroy()
269## del cls.root
271 @classmethod
272 def setUpClass(cls):
273 cls.engine = se.SearchEngine(None)
274## requires('gui')
275## cls.root = Tk()
276## cls.text = Text(master=cls.root)
277 cls.text = mockText()
278 # search_backward calls index('end-1c')
279 cls.text.index = lambda index: '4.0' 1ae
280 test_text = (
281 'First line\n'
282 'Line with target\n'
283 'Last line\n')
284 cls.text.insert('1.0', test_text)
285 cls.pat = re.compile('target')
286 cls.res = (2, (10, 16)) # line, slice indexes of 'target'
287 cls.failpat = re.compile('xyz') # not in text
288 cls.emptypat = re.compile(r'\w*') # empty match possible
290 def make_search(self, func):
291 def search(pat, line, col, wrap, ok=0): 1ef
292 res = func(self.text, pat, line, col, wrap, ok) 1ef
293 # res is (line, matchobject) or None
294 return (res[0], res[1].span()) if res else res 1ef
295 return search 1ef
297 def test_search_forward(self):
298 # search for non-empty match
299 Equal = self.assertEqual 1f
300 forward = self.make_search(self.engine.search_forward) 1f
301 pat = self.pat 1f
302 Equal(forward(pat, 1, 0, True), self.res) 1f
303 Equal(forward(pat, 3, 0, True), self.res) # wrap 1f
304 Equal(forward(pat, 3, 0, False), None) # no wrap 1f
305 Equal(forward(pat, 2, 10, False), self.res) 1f
307 Equal(forward(self.failpat, 1, 0, True), None) 1f
308 Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9))) 1f
309 #Equal(forward(self.emptypat, 2, 9, True), self.res)
310 # While the initial empty match is correctly ignored, skipping
311 # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
312 Equal(forward(self.emptypat, 2, 10, True), self.res) 1f
314 def test_search_backward(self):
315 # search for non-empty match
316 Equal = self.assertEqual 1e
317 backward = self.make_search(self.engine.search_backward) 1e
318 pat = self.pat 1e
319 Equal(backward(pat, 3, 5, True), self.res) 1e
320 Equal(backward(pat, 2, 0, True), self.res) # wrap 1e
321 Equal(backward(pat, 2, 0, False), None) # no wrap 1e
322 Equal(backward(pat, 2, 16, False), self.res) 1e
324 Equal(backward(self.failpat, 3, 9, True), None) 1e
325 Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9))) 1e
326 # Accepted because 9 < 10, not because ok=True.
327 # It is not clear that ok=True is useful going back - tjr
328 Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9))) 1e
331if __name__ == '__main__': 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true
332 unittest.main(verbosity=2)