(root)/
Python-3.12.0/
Lib/
idlelib/
idle_test/
test_searchengine.py
       1  "Test searchengine, coverage 99%."
       2  
       3  from idlelib import searchengine as se
       4  import unittest
       5  # from test.support import requires
       6  from tkinter import  BooleanVar, StringVar, TclError  # ,Tk, Text
       7  from tkinter import messagebox
       8  from idlelib.idle_test.mock_tk import Var, Mbox
       9  from idlelib.idle_test.mock_tk import Text as mockText
      10  import 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  
      18  def 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  
      24  def 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  
      31  class ESC[4;38;5;81mMock:
      32      def __init__(self, *args, **kwargs): pass
      33  
      34  class ESC[4;38;5;81mGetTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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  
      48  class ESC[4;38;5;81mGetLineColTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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  
      57  class ESC[4;38;5;81mGetSelectionTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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  
      92  class ESC[4;38;5;81mReverseSearchTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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  
     105  class ESC[4;38;5;81mSearchEngineTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     203  class ESC[4;38;5;81mSearchTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     264  class ESC[4;38;5;81mForwardBackwardTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     331  if __name__ == '__main__':
     332      unittest.main(verbosity=2)