Coverage for idle_test/test_searchengine.py: 97%

212 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

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 

7from tkinter import messagebox 

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.messagebox = 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.messagebox = messagebox 

29 

30 

31class Mock: 

32 def __init__(self, *args, **kwargs): pass 1aj

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 1j

38 se.SearchEngine = Mock # monkey-patch class 1j

39 try: 1j

40 root = Mock() 1j

41 engine = se.get(root) 1j

42 self.assertIsInstance(engine, se.SearchEngine) 1j

43 self.assertIs(root._searchengine, engine) 1j

44 self.assertIs(se.get(root), engine) 1j

45 finally: 

46 se.SearchEngine = saved_Engine # restore class to module 1j

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)) 1m

52 self.assertEqual(se.get_line_col('1.11'), (1, 11)) 1m

53 

54 self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend')) 1m

55 self.assertRaises(ValueError, se.get_line_col, ('end')) 1m

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() 1g

73 text.insert('1.0', 'Hello World!') 1g

74 

75 # fix text.index result when called in get_selection 

76 def sel(s): 1g

77 # select entire text, cursor irrelevant 

78 if s == 'sel.first': return '1.0' 1g

79 if s == 'sel.last': return '1.12' 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never false1g

80 raise TclError 

81 text.index = sel # replaces .tag_add('sel', '1.0, '1.12') 1g

82 self.assertEqual(se.get_selection(text), ('1.0', '1.12')) 1g

83 

84 def mark(s): 1g

85 # no selection, cursor after 'Hello' 

86 if s == 'insert': return '1.5' 1g

87 raise TclError 1g

88 text.index = mark # replaces .mark_set('insert', '1.5') 1g

89 self.assertEqual(se.get_selection(text), ('1.5', '1.5')) 1g

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 1k

96 line = "Here is an 'is' test text." 1k

97 prog = re.compile('is') 1k

98 Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14)) 1k

99 Equal(se.search_reverse(prog, line, 14).span(), (12, 14)) 1k

100 Equal(se.search_reverse(prog, line, 13).span(), (5, 7)) 1k

101 Equal(se.search_reverse(prog, line, 7).span(), (5, 7)) 1k

102 Equal(se.search_reverse(prog, line, 6), None) 1k

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 1c

115 Equal = self.assertEqual 1c

116 

117 Equal(engine.getpat(), '') 1c

118 engine.setpat('hello') 1c

119 Equal(engine.getpat(), 'hello') 1c

120 

121 Equal(engine.isre(), False) 1c

122 engine.revar.set(1) 1c

123 Equal(engine.isre(), True) 1c

124 

125 Equal(engine.iscase(), False) 1c

126 engine.casevar.set(1) 1c

127 Equal(engine.iscase(), True) 1c

128 

129 Equal(engine.isword(), False) 1c

130 engine.wordvar.set(1) 1c

131 Equal(engine.isword(), True) 1c

132 

133 Equal(engine.iswrap(), True) 1c

134 engine.wrapvar.set(0) 1c

135 Equal(engine.iswrap(), False) 1c

136 

137 Equal(engine.isback(), False) 1c

138 engine.backvar.set(1) 1c

139 Equal(engine.isback(), True) 1c

140 

141 def test_setcookedpat(self): 

142 engine = self.engine 1l

143 engine.setcookedpat(r'\s') 1l

144 self.assertEqual(engine.getpat(), r'\s') 1l

145 engine.revar.set(1) 1l

146 engine.setcookedpat(r'\s') 1l

147 self.assertEqual(engine.getpat(), r'\\s') 1l

148 

149 def test_getcookedpat(self): 

150 engine = self.engine 1h

151 Equal = self.assertEqual 1h

152 

153 Equal(engine.getcookedpat(), '') 1h

154 engine.setpat('hello') 1h

155 Equal(engine.getcookedpat(), 'hello') 1h

156 engine.wordvar.set(True) 1h

157 Equal(engine.getcookedpat(), r'\bhello\b') 1h

158 engine.wordvar.set(False) 1h

159 

160 engine.setpat(r'\s') 1h

161 Equal(engine.getcookedpat(), r'\\s') 1h

162 engine.revar.set(True) 1h

163 Equal(engine.getcookedpat(), r'\s') 1h

164 

165 def test_getprog(self): 

166 engine = self.engine 1d

167 Equal = self.assertEqual 1d

168 

169 engine.setpat('Hello') 1d

170 temppat = engine.getprog() 1d

171 Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern) 1d

172 engine.casevar.set(1) 1d

173 temppat = engine.getprog() 1d

174 Equal(temppat.pattern, re.compile('Hello').pattern, 0) 1d

175 

176 engine.setpat('') 1d

177 Equal(engine.getprog(), None) 1d

178 Equal(Mbox.showerror.message, 1d

179 'Error: Empty regular expression') 

180 engine.setpat('+') 1d

181 engine.revar.set(1) 1d

182 Equal(engine.getprog(), None) 1d

183 Equal(Mbox.showerror.message, 1d

184 'Error: nothing to repeat\nPattern: +\nOffset: 0') 

185 

186 def test_report_error(self): 

187 showerror = Mbox.showerror 1i

188 Equal = self.assertEqual 1i

189 pat = '[a-z' 1i

190 msg = 'unexpected end of regular expression' 1i

191 

192 Equal(self.engine.report_error(pat, msg), None) 1i

193 Equal(showerror.title, 'Regular expression error') 1i

194 expected_message = ("Error: " + msg + "\nPattern: [a-z") 1i

195 Equal(showerror.message, expected_message) 1i

196 

197 Equal(self.engine.report_error(pat, msg, 5), None) 1i

198 Equal(showerror.title, 'Regular expression error') 1i

199 expected_message += "\nOffset: 5" 1i

200 Equal(showerror.message, expected_message) 1i

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) 1ab

221 cls.engine.search_backward = lambda *args: ('b', args) 1ab

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 1b

230 engine = self.engine 1b

231 search = engine.search_text 1b

232 text = self.text 1b

233 pat = self.pat 1b

234 

235 engine.patvar.set(None) 1b

236 #engine.revar.set(pat) 

237 Equal(search(text), None) 1b

238 

239 def mark(s): 1b

240 # no selection, cursor after 'Hello' 

241 if s == 'insert': return '1.5' 1b

242 raise TclError 1b

243 text.index = mark 1b

244 Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False))) 1b

245 engine.wrapvar.set(False) 1b

246 Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False))) 1b

247 engine.wrapvar.set(True) 1b

248 engine.backvar.set(True) 1b

249 Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False))) 1b

250 engine.backvar.set(False) 1b

251 

252 def sel(s): 1b

253 if s == 'sel.first': return '2.10' 1b

254 if s == 'sel.last': return '2.16' 254 ↛ 255line 254 didn't jump to line 255, because the condition on line 254 was never false1b

255 raise TclError 

256 text.index = sel 1b

257 Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False))) 1b

258 Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True))) 1b

259 engine.backvar.set(True) 1b

260 Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False))) 1b

261 Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True))) 1b

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' 1ae

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): 1ef

292 res = func(self.text, pat, line, col, wrap, ok) 1ef

293 # res is (line, matchobject) or None 

294 return (res[0], res[1].span()) if res else res 1ef

295 return search 1ef

296 

297 def test_search_forward(self): 

298 # search for non-empty match 

299 Equal = self.assertEqual 1f

300 forward = self.make_search(self.engine.search_forward) 1f

301 pat = self.pat 1f

302 Equal(forward(pat, 1, 0, True), self.res) 1f

303 Equal(forward(pat, 3, 0, True), self.res) # wrap 1f

304 Equal(forward(pat, 3, 0, False), None) # no wrap 1f

305 Equal(forward(pat, 2, 10, False), self.res) 1f

306 

307 Equal(forward(self.failpat, 1, 0, True), None) 1f

308 Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9))) 1f

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) 1f

313 

314 def test_search_backward(self): 

315 # search for non-empty match 

316 Equal = self.assertEqual 1e

317 backward = self.make_search(self.engine.search_backward) 1e

318 pat = self.pat 1e

319 Equal(backward(pat, 3, 5, True), self.res) 1e

320 Equal(backward(pat, 2, 0, True), self.res) # wrap 1e

321 Equal(backward(pat, 2, 0, False), None) # no wrap 1e

322 Equal(backward(pat, 2, 16, False), self.res) 1e

323 

324 Equal(backward(self.failpat, 3, 9, True), None) 1e

325 Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9))) 1e

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))) 1e

329 

330 

331if __name__ == '__main__': 331 ↛ 332line 331 didn't jump to line 332, because the condition on line 331 was never true

332 unittest.main(verbosity=2)