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