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