1 "Test searchengine, coverage 99%."
2
3 from idlelib import searchengine as se
4 import unittest
5 # from test.support import requires
6 from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
7 from tkinter import messagebox
8 from idlelib.idle_test.mock_tk import Var, Mbox
9 from idlelib.idle_test.mock_tk import Text as mockText
10 import re
11
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.
17
18 def 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
23
24 def tearDownModule():
25 # Restore 'just in case', though other tests should also replace.
26 se.BooleanVar = BooleanVar
27 se.StringVar = StringVar
28 se.messagebox = messagebox
29
30
31 class ESC[4;38;5;81mMock:
32 def __init__(self, *args, **kwargs): pass
33
34 class ESC[4;38;5;81mGetTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
35 # SearchEngine.get returns singleton created & saved on first call.
36 def test_get(self):
37 saved_Engine = se.SearchEngine
38 se.SearchEngine = Mock # monkey-patch class
39 try:
40 root = Mock()
41 engine = se.get(root)
42 self.assertIsInstance(engine, se.SearchEngine)
43 self.assertIs(root._searchengine, engine)
44 self.assertIs(se.get(root), engine)
45 finally:
46 se.SearchEngine = saved_Engine # restore class to module
47
48 class ESC[4;38;5;81mGetLineColTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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))
52 self.assertEqual(se.get_line_col('1.11'), (1, 11))
53
54 self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
55 self.assertRaises(ValueError, se.get_line_col, ('end'))
56
57 class ESC[4;38;5;81mGetSelectionTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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
69
70 def test_get_selection(self):
71 # text = Text(master=self.root)
72 text = mockText()
73 text.insert('1.0', 'Hello World!')
74
75 # fix text.index result when called in get_selection
76 def sel(s):
77 # select entire text, cursor irrelevant
78 if s == 'sel.first': return '1.0'
79 if s == 'sel.last': return '1.12'
80 raise TclError
81 text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
82 self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
83
84 def mark(s):
85 # no selection, cursor after 'Hello'
86 if s == 'insert': return '1.5'
87 raise TclError
88 text.index = mark # replaces .mark_set('insert', '1.5')
89 self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
90
91
92 class ESC[4;38;5;81mReverseSearchTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
93 # Test helper function that searches backwards within a line.
94 def test_search_reverse(self):
95 Equal = self.assertEqual
96 line = "Here is an 'is' test text."
97 prog = re.compile('is')
98 Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
99 Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
100 Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
101 Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
102 Equal(se.search_reverse(prog, line, 6), None)
103
104
105 class ESC[4;38;5;81mSearchEngineTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
106 # Test class methods that do not use Text widget.
107
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.
112
113 def test_is_get(self):
114 engine = self.engine
115 Equal = self.assertEqual
116
117 Equal(engine.getpat(), '')
118 engine.setpat('hello')
119 Equal(engine.getpat(), 'hello')
120
121 Equal(engine.isre(), False)
122 engine.revar.set(1)
123 Equal(engine.isre(), True)
124
125 Equal(engine.iscase(), False)
126 engine.casevar.set(1)
127 Equal(engine.iscase(), True)
128
129 Equal(engine.isword(), False)
130 engine.wordvar.set(1)
131 Equal(engine.isword(), True)
132
133 Equal(engine.iswrap(), True)
134 engine.wrapvar.set(0)
135 Equal(engine.iswrap(), False)
136
137 Equal(engine.isback(), False)
138 engine.backvar.set(1)
139 Equal(engine.isback(), True)
140
141 def test_setcookedpat(self):
142 engine = self.engine
143 engine.setcookedpat(r'\s')
144 self.assertEqual(engine.getpat(), r'\s')
145 engine.revar.set(1)
146 engine.setcookedpat(r'\s')
147 self.assertEqual(engine.getpat(), r'\\s')
148
149 def test_getcookedpat(self):
150 engine = self.engine
151 Equal = self.assertEqual
152
153 Equal(engine.getcookedpat(), '')
154 engine.setpat('hello')
155 Equal(engine.getcookedpat(), 'hello')
156 engine.wordvar.set(True)
157 Equal(engine.getcookedpat(), r'\bhello\b')
158 engine.wordvar.set(False)
159
160 engine.setpat(r'\s')
161 Equal(engine.getcookedpat(), r'\\s')
162 engine.revar.set(True)
163 Equal(engine.getcookedpat(), r'\s')
164
165 def test_getprog(self):
166 engine = self.engine
167 Equal = self.assertEqual
168
169 engine.setpat('Hello')
170 temppat = engine.getprog()
171 Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
172 engine.casevar.set(1)
173 temppat = engine.getprog()
174 Equal(temppat.pattern, re.compile('Hello').pattern, 0)
175
176 engine.setpat('')
177 Equal(engine.getprog(), None)
178 Equal(Mbox.showerror.message,
179 'Error: Empty regular expression')
180 engine.setpat('+')
181 engine.revar.set(1)
182 Equal(engine.getprog(), None)
183 Equal(Mbox.showerror.message,
184 'Error: nothing to repeat\nPattern: +\nOffset: 0')
185
186 def test_report_error(self):
187 showerror = Mbox.showerror
188 Equal = self.assertEqual
189 pat = '[a-z'
190 msg = 'unexpected end of regular expression'
191
192 Equal(self.engine.report_error(pat, msg), None)
193 Equal(showerror.title, 'Regular expression error')
194 expected_message = ("Error: " + msg + "\nPattern: [a-z")
195 Equal(showerror.message, expected_message)
196
197 Equal(self.engine.report_error(pat, msg, 5), None)
198 Equal(showerror.title, 'Regular expression error')
199 expected_message += "\nOffset: 5"
200 Equal(showerror.message, expected_message)
201
202
203 class ESC[4;38;5;81mSearchTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
204 # Test that search_text makes right call to right method.
205
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')
218
219 cls.engine = se.SearchEngine(None)
220 cls.engine.search_forward = lambda *args: ('f', args)
221 cls.engine.search_backward = lambda *args: ('b', args)
222
223 ## @classmethod
224 ## def tearDownClass(cls):
225 ## cls.root.destroy()
226 ## del cls.root
227
228 def test_search(self):
229 Equal = self.assertEqual
230 engine = self.engine
231 search = engine.search_text
232 text = self.text
233 pat = self.pat
234
235 engine.patvar.set(None)
236 #engine.revar.set(pat)
237 Equal(search(text), None)
238
239 def mark(s):
240 # no selection, cursor after 'Hello'
241 if s == 'insert': return '1.5'
242 raise TclError
243 text.index = mark
244 Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
245 engine.wrapvar.set(False)
246 Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
247 engine.wrapvar.set(True)
248 engine.backvar.set(True)
249 Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
250 engine.backvar.set(False)
251
252 def sel(s):
253 if s == 'sel.first': return '2.10'
254 if s == 'sel.last': return '2.16'
255 raise TclError
256 text.index = sel
257 Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
258 Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
259 engine.backvar.set(True)
260 Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
261 Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
262
263
264 class ESC[4;38;5;81mForwardBackwardTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
265 # Test that search_forward method finds the target.
266 ## @classmethod
267 ## def tearDownClass(cls):
268 ## cls.root.destroy()
269 ## del cls.root
270
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'
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
289
290 def make_search(self, func):
291 def search(pat, line, col, wrap, ok=0):
292 res = func(self.text, pat, line, col, wrap, ok)
293 # res is (line, matchobject) or None
294 return (res[0], res[1].span()) if res else res
295 return search
296
297 def test_search_forward(self):
298 # search for non-empty match
299 Equal = self.assertEqual
300 forward = self.make_search(self.engine.search_forward)
301 pat = self.pat
302 Equal(forward(pat, 1, 0, True), self.res)
303 Equal(forward(pat, 3, 0, True), self.res) # wrap
304 Equal(forward(pat, 3, 0, False), None) # no wrap
305 Equal(forward(pat, 2, 10, False), self.res)
306
307 Equal(forward(self.failpat, 1, 0, True), None)
308 Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
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)
313
314 def test_search_backward(self):
315 # search for non-empty match
316 Equal = self.assertEqual
317 backward = self.make_search(self.engine.search_backward)
318 pat = self.pat
319 Equal(backward(pat, 3, 5, True), self.res)
320 Equal(backward(pat, 2, 0, True), self.res) # wrap
321 Equal(backward(pat, 2, 0, False), None) # no wrap
322 Equal(backward(pat, 2, 16, False), self.res)
323
324 Equal(backward(self.failpat, 3, 9, True), None)
325 Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
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)))
329
330
331 if __name__ == '__main__':
332 unittest.main(verbosity=2)