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 engine.setpat('+') 179 engine.revar.set(1) 180 Equal(engine.getprog(), None) 181 self.assertEqual(Mbox.showerror.message, 182 'Error: nothing to repeat at position 0\nPattern: +') 183 184 def test_report_error(self): 185 showerror = Mbox.showerror 186 Equal = self.assertEqual 187 pat = '[a-z' 188 msg = 'unexpected end of regular expression' 189 190 Equal(self.engine.report_error(pat, msg), None) 191 Equal(showerror.title, 'Regular expression error') 192 expected_message = ("Error: " + msg + "\nPattern: [a-z") 193 Equal(showerror.message, expected_message) 194 195 Equal(self.engine.report_error(pat, msg, 5), None) 196 Equal(showerror.title, 'Regular expression error') 197 expected_message += "\nOffset: 5" 198 Equal(showerror.message, expected_message) 199 200 201class SearchTest(unittest.TestCase): 202 # Test that search_text makes right call to right method. 203 204 @classmethod 205 def setUpClass(cls): 206## requires('gui') 207## cls.root = Tk() 208## cls.text = Text(master=cls.root) 209 cls.text = mockText() 210 test_text = ( 211 'First line\n' 212 'Line with target\n' 213 'Last line\n') 214 cls.text.insert('1.0', test_text) 215 cls.pat = re.compile('target') 216 217 cls.engine = se.SearchEngine(None) 218 cls.engine.search_forward = lambda *args: ('f', args) 219 cls.engine.search_backward = lambda *args: ('b', args) 220 221## @classmethod 222## def tearDownClass(cls): 223## cls.root.destroy() 224## del cls.root 225 226 def test_search(self): 227 Equal = self.assertEqual 228 engine = self.engine 229 search = engine.search_text 230 text = self.text 231 pat = self.pat 232 233 engine.patvar.set(None) 234 #engine.revar.set(pat) 235 Equal(search(text), None) 236 237 def mark(s): 238 # no selection, cursor after 'Hello' 239 if s == 'insert': return '1.5' 240 raise TclError 241 text.index = mark 242 Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False))) 243 engine.wrapvar.set(False) 244 Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False))) 245 engine.wrapvar.set(True) 246 engine.backvar.set(True) 247 Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False))) 248 engine.backvar.set(False) 249 250 def sel(s): 251 if s == 'sel.first': return '2.10' 252 if s == 'sel.last': return '2.16' 253 raise TclError 254 text.index = sel 255 Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False))) 256 Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True))) 257 engine.backvar.set(True) 258 Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False))) 259 Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True))) 260 261 262class ForwardBackwardTest(unittest.TestCase): 263 # Test that search_forward method finds the target. 264## @classmethod 265## def tearDownClass(cls): 266## cls.root.destroy() 267## del cls.root 268 269 @classmethod 270 def setUpClass(cls): 271 cls.engine = se.SearchEngine(None) 272## requires('gui') 273## cls.root = Tk() 274## cls.text = Text(master=cls.root) 275 cls.text = mockText() 276 # search_backward calls index('end-1c') 277 cls.text.index = lambda index: '4.0' 278 test_text = ( 279 'First line\n' 280 'Line with target\n' 281 'Last line\n') 282 cls.text.insert('1.0', test_text) 283 cls.pat = re.compile('target') 284 cls.res = (2, (10, 16)) # line, slice indexes of 'target' 285 cls.failpat = re.compile('xyz') # not in text 286 cls.emptypat = re.compile(r'\w*') # empty match possible 287 288 def make_search(self, func): 289 def search(pat, line, col, wrap, ok=0): 290 res = func(self.text, pat, line, col, wrap, ok) 291 # res is (line, matchobject) or None 292 return (res[0], res[1].span()) if res else res 293 return search 294 295 def test_search_forward(self): 296 # search for non-empty match 297 Equal = self.assertEqual 298 forward = self.make_search(self.engine.search_forward) 299 pat = self.pat 300 Equal(forward(pat, 1, 0, True), self.res) 301 Equal(forward(pat, 3, 0, True), self.res) # wrap 302 Equal(forward(pat, 3, 0, False), None) # no wrap 303 Equal(forward(pat, 2, 10, False), self.res) 304 305 Equal(forward(self.failpat, 1, 0, True), None) 306 Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9))) 307 #Equal(forward(self.emptypat, 2, 9, True), self.res) 308 # While the initial empty match is correctly ignored, skipping 309 # the rest of the line and returning (3, (0,4)) seems buggy - tjr. 310 Equal(forward(self.emptypat, 2, 10, True), self.res) 311 312 def test_search_backward(self): 313 # search for non-empty match 314 Equal = self.assertEqual 315 backward = self.make_search(self.engine.search_backward) 316 pat = self.pat 317 Equal(backward(pat, 3, 5, True), self.res) 318 Equal(backward(pat, 2, 0, True), self.res) # wrap 319 Equal(backward(pat, 2, 0, False), None) # no wrap 320 Equal(backward(pat, 2, 16, False), self.res) 321 322 Equal(backward(self.failpat, 3, 9, True), None) 323 Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9))) 324 # Accepted because 9 < 10, not because ok=True. 325 # It is not clear that ok=True is useful going back - tjr 326 Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9))) 327 328 329if __name__ == '__main__': 330 unittest.main(verbosity=2) 331