test_searchengine.py 11 KB

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