1"Test searchengine, coverage 99%."
2
3from idlelib import searchengine as se
4import unittest
5# from test.support import requires
6from tkinter import  BooleanVar, StringVar, TclError  # ,Tk, Text
7import tkinter.messagebox as tkMessageBox
8from idlelib.idle_test.mock_tk import Var, Mbox
9from idlelib.idle_test.mock_tk import Text as mockText
10import 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
18def setUpModule():
19    # Replace s-e module tkinter imports other than non-gui TclError.
20    se.BooleanVar = Var
21    se.StringVar = Var
22    se.tkMessageBox = Mbox
23
24def tearDownModule():
25    # Restore 'just in case', though other tests should also replace.
26    se.BooleanVar = BooleanVar
27    se.StringVar = StringVar
28    se.tkMessageBox = tkMessageBox
29
30
31class Mock:
32    def __init__(self, *args, **kwargs): pass
33
34class GetTest(unittest.TestCase):
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
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))
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
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
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
92class ReverseSearchTest(unittest.TestCase):
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
105class SearchEngineTest(unittest.TestCase):
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
203class SearchTest(unittest.TestCase):
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
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
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
331if __name__ == '__main__':
332    unittest.main(verbosity=2)
333