1'''Test functions and SearchEngine class in idlelib.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.support import requires 11from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text 12import tkinter.messagebox as 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(r'\s') 143 self.assertEqual(engine.getpat(), r'\s') 144 engine.revar.set(1) 145 engine.setcookedpat(r'\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(r'\s') 160 Equal(engine.getcookedpat(), r'\\s') 161 engine.revar.set(True) 162 Equal(engine.getcookedpat(), r'\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 at position 0\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(r'\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