[Python-checkins] cpython (merge 3.3 -> default): Merge from 3.3 #18489 Search Engine tests

terry.reedy python-checkins at python.org
Sat Aug 31 22:29:25 CEST 2013


http://hg.python.org/cpython/rev/7605847c15a4
changeset:   85478:7605847c15a4
parent:      85475:833246d42825
parent:      85477:dfbf0f9034cc
user:        Terry Jan Reedy <tjreedy at udel.edu>
date:        Sat Aug 31 16:28:53 2013 -0400
summary:
  Merge from 3.3 #18489 Search Engine tests

files:
  Lib/idlelib/SearchEngine.py                |   72 +-
  Lib/idlelib/idle_test/test_searchengine.py |  326 ++++++++++
  Misc/NEWS                                  |    5 +
  3 files changed, 369 insertions(+), 34 deletions(-)


diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py
--- a/Lib/idlelib/SearchEngine.py
+++ b/Lib/idlelib/SearchEngine.py
@@ -1,6 +1,6 @@
 '''Define SearchEngine for search dialogs.'''
 import re
-from tkinter import *
+from tkinter import StringVar, BooleanVar, TclError
 import tkinter.messagebox as tkMessageBox
 
 def get(root):
@@ -22,14 +22,13 @@
 
         The dialogs bind these to the UI elements present in the dialogs.
         '''
-        self.root = root
-        self.patvar = StringVar(root)           # search pattern
-        self.revar = BooleanVar(root)           # regular expression?
-        self.casevar = BooleanVar(root)         # match case?
-        self.wordvar = BooleanVar(root)         # match whole word?
-        self.wrapvar = BooleanVar(root)         # wrap around buffer?
-        self.wrapvar.set(1)                     # (on by default)
-        self.backvar = BooleanVar(root)         # search backwards?
+        self.root = root  # need for report_error()
+        self.patvar = StringVar(root, '')   # search pattern
+        self.revar = BooleanVar(root, False)   # regular expression?
+        self.casevar = BooleanVar(root, False)   # match case?
+        self.wordvar = BooleanVar(root, False)   # match whole word?
+        self.wrapvar = BooleanVar(root, True)   # wrap around buffer?
+        self.backvar = BooleanVar(root, False)   # search backwards?
 
     # Access methods
 
@@ -56,9 +55,16 @@
 
     # Higher level access methods
 
+    def setcookedpat(self, pat):
+        "Set pattern after escaping if re."
+        # called only in SearchDialog.py: 66
+        if self.isre():
+            pat = re.escape(pat)
+        self.setpat(pat)
+
     def getcookedpat(self):
         pat = self.getpat()
-        if not self.isre():
+        if not self.isre():  # if True, see setcookedpat
             pat = re.escape(pat)
         if self.isword():
             pat = r"\b%s\b" % pat
@@ -90,33 +96,28 @@
         # Derived class could override this with something fancier
         msg = "Error: " + str(msg)
         if pat:
-            msg = msg + "\np\Pattern: " + str(pat)
+            msg = msg + "\nPattern: " + str(pat)
         if col >= 0:
             msg = msg + "\nOffset: " + str(col)
         tkMessageBox.showerror("Regular expression error",
                                msg, master=self.root)
 
-    def setcookedpat(self, pat):
-        if self.isre():
-            pat = re.escape(pat)
-        self.setpat(pat)
+    def search_text(self, text, prog=None, ok=0):
+        '''Return (lineno, matchobj) or None for forward/backward search.
 
-    def search_text(self, text, prog=None, ok=0):
-        '''Return (lineno, matchobj) for prog in text widget, or None.
+        This function calls the right function with the right arguments.
+        It directly return the result of that call.
 
-        If prog is given, it should be a precompiled pattern.
-        Wrap (yes/no) and direction (forward/back) settings are used.
+        Text is a text widget. Prog is a precompiled pattern.
+        The ok parameteris a bit complicated as it has two effects.
 
-        The search starts at the selection (if there is one) or at the
-        insert mark (otherwise).  If the search is forward, it starts
-        at the right of the selection; for a backward search, it
-        starts at the left end.  An empty match exactly at either end
-        of the selection (or at the insert mark if there is no
-        selection) is ignored  unless the ok flag is true -- this is
-        done to guarantee progress.
+        If there is a selection, the search begin at either end,
+        depending on the direction setting and ok, with ok meaning that
+        the search starts with the selection. Otherwise, search begins
+        at the insert mark.
 
-        If the search is allowed to wrap around, it will return the
-        original selection if (and only if) it is the only match.
+        To aid progress, the search functions do not return an empty
+        match at the starting position unless ok is True.
         '''
 
         if not prog:
@@ -188,15 +189,18 @@
         return None
 
 def search_reverse(prog, chars, col):
-    '''Search backwards in a string (line of text).
+    '''Search backwards and return an re match object or None.
 
     This is done by searching forwards until there is no match.
+    Prog: compiled re object with a search method returning a match.
+    Chars: line of text, without \n.
+    Col: stop index for the search; the limit for match.end().
     '''
     m = prog.search(chars)
     if not m:
         return None
     found = None
-    i, j = m.span()
+    i, j = m.span()  # m.start(), m.end() == match slice indexes
     while i < col and j <= col:
         found = m
         if i == j:
@@ -226,7 +230,7 @@
     line, col = map(int, index.split(".")) # Fails on invalid index
     return line, col
 
-##if __name__ == "__main__":
-##    from test import support; support.use_resources = ['gui']
-##    import unittest
-##    unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
+if __name__ == "__main__":
+    from test import support; support.use_resources = ['gui']
+    import unittest
+    unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
new file mode 100644
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_searchengine.py
@@ -0,0 +1,326 @@
+'''Test functions and SearchEngine class in SearchEngine.py.'''
+
+# With mock replacements, the module does not use any gui widgets.
+# The use of tk.Text is avoided (for now, until mock Text is improved)
+# by patching instances with an index function returning what is needed.
+# This works because mock Text.get does not use .index.
+
+import re
+import unittest
+from test.support import requires
+from tkinter import  BooleanVar, StringVar, TclError  # ,Tk, Text
+import tkinter.messagebox as tkMessageBox
+from idlelib import SearchEngine as se
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib.idle_test.mock_tk import Text as mockText
+
+def setUpModule():
+    # Replace s-e module tkinter imports other than non-gui TclError.
+    se.BooleanVar = Var
+    se.StringVar = Var
+    se.tkMessageBox = Mbox
+
+def tearDownModule():
+    # Restore 'just in case', though other tests should also replace.
+    se.BooleanVar = BooleanVar
+    se.StringVar = StringVar
+    se.tkMessageBox = tkMessageBox
+
+
+class Mock:
+    def __init__(self, *args, **kwargs): pass
+
+class GetTest(unittest.TestCase):
+    # SearchEngine.get returns singleton created & saved on first call.
+    def test_get(self):
+        saved_Engine = se.SearchEngine
+        se.SearchEngine = Mock  # monkey-patch class
+        try:
+            root = Mock()
+            engine = se.get(root)
+            self.assertIsInstance(engine, se.SearchEngine)
+            self.assertIs(root._searchengine, engine)
+            self.assertIs(se.get(root), engine)
+        finally:
+            se.SearchEngine = saved_Engine  # restore class to module
+
+class GetLineColTest(unittest.TestCase):
+    #  Test simple text-independent helper function
+    def test_get_line_col(self):
+        self.assertEqual(se.get_line_col('1.0'), (1, 0))
+        self.assertEqual(se.get_line_col('1.11'), (1, 11))
+
+        self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
+        self.assertRaises(ValueError, se.get_line_col, ('end'))
+
+class GetSelectionTest(unittest.TestCase):
+    # Test text-dependent helper function.
+##    # Need gui for text.index('sel.first/sel.last/insert').
+##    @classmethod
+##    def setUpClass(cls):
+##        requires('gui')
+##        cls.root = Tk()
+##
+##    @classmethod
+##    def tearDownClass(cls):
+##        cls.root.destroy()
+
+    def test_get_selection(self):
+        # text = Text(master=self.root)
+        text = mockText()
+        text.insert('1.0',  'Hello World!')
+
+        # fix text.index result when called in get_selection
+        def sel(s):
+            # select entire text, cursor irrelevant
+            if s == 'sel.first': return '1.0'
+            if s == 'sel.last': return '1.12'
+            raise TclError
+        text.index = sel  # replaces .tag_add('sel', '1.0, '1.12')
+        self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
+
+        def mark(s):
+            # no selection, cursor after 'Hello'
+            if s == 'insert': return '1.5'
+            raise TclError
+        text.index = mark  # replaces .mark_set('insert', '1.5')
+        self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
+
+
+class ReverseSearchTest(unittest.TestCase):
+    # Test helper function that searches backwards within a line.
+    def test_search_reverse(self):
+        Equal = self.assertEqual
+        line = "Here is an 'is' test text."
+        prog = re.compile('is')
+        Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
+        Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
+        Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
+        Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
+        Equal(se.search_reverse(prog, line, 6), None)
+
+
+class SearchEngineTest(unittest.TestCase):
+    # Test class methods that do not use Text widget.
+
+    def setUp(self):
+        self.engine = se.SearchEngine(root=None)
+        # Engine.root is only used to create error message boxes.
+        # The mock replacement ignores the root argument.
+
+    def test_is_get(self):
+        engine = self.engine
+        Equal = self.assertEqual
+
+        Equal(engine.getpat(), '')
+        engine.setpat('hello')
+        Equal(engine.getpat(), 'hello')
+
+        Equal(engine.isre(), False)
+        engine.revar.set(1)
+        Equal(engine.isre(), True)
+
+        Equal(engine.iscase(), False)
+        engine.casevar.set(1)
+        Equal(engine.iscase(), True)
+
+        Equal(engine.isword(), False)
+        engine.wordvar.set(1)
+        Equal(engine.isword(), True)
+
+        Equal(engine.iswrap(), True)
+        engine.wrapvar.set(0)
+        Equal(engine.iswrap(), False)
+
+        Equal(engine.isback(), False)
+        engine.backvar.set(1)
+        Equal(engine.isback(), True)
+
+    def test_setcookedpat(self):
+        engine = self.engine
+        engine.setcookedpat('\s')
+        self.assertEqual(engine.getpat(), '\s')
+        engine.revar.set(1)
+        engine.setcookedpat('\s')
+        self.assertEqual(engine.getpat(), r'\\s')
+
+    def test_getcookedpat(self):
+        engine = self.engine
+        Equal = self.assertEqual
+
+        Equal(engine.getcookedpat(), '')
+        engine.setpat('hello')
+        Equal(engine.getcookedpat(), 'hello')
+        engine.wordvar.set(True)
+        Equal(engine.getcookedpat(), r'\bhello\b')
+        engine.wordvar.set(False)
+
+        engine.setpat('\s')
+        Equal(engine.getcookedpat(), r'\\s')
+        engine.revar.set(True)
+        Equal(engine.getcookedpat(), '\s')
+
+    def test_getprog(self):
+        engine = self.engine
+        Equal = self.assertEqual
+
+        engine.setpat('Hello')
+        temppat = engine.getprog()
+        Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
+        engine.casevar.set(1)
+        temppat = engine.getprog()
+        Equal(temppat.pattern, re.compile('Hello').pattern, 0)
+
+        engine.setpat('')
+        Equal(engine.getprog(), None)
+        engine.setpat('+')
+        engine.revar.set(1)
+        Equal(engine.getprog(), None)
+        self.assertEqual(Mbox.showerror.message,
+                          'Error: nothing to repeat\nPattern: +')
+
+    def test_report_error(self):
+        showerror = Mbox.showerror
+        Equal = self.assertEqual
+        pat = '[a-z'
+        msg = 'unexpected end of regular expression'
+
+        Equal(self.engine.report_error(pat, msg), None)
+        Equal(showerror.title, 'Regular expression error')
+        expected_message = ("Error: " + msg + "\nPattern: [a-z")
+        Equal(showerror.message, expected_message)
+
+        Equal(self.engine.report_error(pat, msg, 5), None)
+        Equal(showerror.title, 'Regular expression error')
+        expected_message += "\nOffset: 5"
+        Equal(showerror.message, expected_message)
+
+
+class SearchTest(unittest.TestCase):
+    # Test that search_text makes right call to right method.
+
+    @classmethod
+    def setUpClass(cls):
+##        requires('gui')
+##        cls.root = Tk()
+##        cls.text = Text(master=cls.root)
+        cls.text = mockText()
+        test_text = (
+            'First line\n'
+            'Line with target\n'
+            'Last line\n')
+        cls.text.insert('1.0', test_text)
+        cls.pat = re.compile('target')
+
+        cls.engine = se.SearchEngine(None)
+        cls.engine.search_forward = lambda *args: ('f', args)
+        cls.engine.search_backward = lambda *args: ('b', args)
+
+##    @classmethod
+##    def tearDownClass(cls):
+##        cls.root.destroy()
+
+    def test_search(self):
+        Equal = self.assertEqual
+        engine = self.engine
+        search = engine.search_text
+        text = self.text
+        pat = self.pat
+
+        engine.patvar.set(None)
+        #engine.revar.set(pat)
+        Equal(search(text), None)
+
+        def mark(s):
+            # no selection, cursor after 'Hello'
+            if s == 'insert': return '1.5'
+            raise TclError
+        text.index = mark
+        Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
+        engine.wrapvar.set(False)
+        Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
+        engine.wrapvar.set(True)
+        engine.backvar.set(True)
+        Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
+        engine.backvar.set(False)
+
+        def sel(s):
+            if s == 'sel.first': return '2.10'
+            if s == 'sel.last': return '2.16'
+            raise TclError
+        text.index = sel
+        Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
+        Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
+        engine.backvar.set(True)
+        Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
+        Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
+
+
+class ForwardBackwardTest(unittest.TestCase):
+    # Test that search_forward method finds the target.
+##    @classmethod
+##    def tearDownClass(cls):
+##        cls.root.destroy()
+
+    @classmethod
+    def setUpClass(cls):
+        cls.engine = se.SearchEngine(None)
+##        requires('gui')
+##        cls.root = Tk()
+##        cls.text = Text(master=cls.root)
+        cls.text = mockText()
+        # search_backward calls index('end-1c')
+        cls.text.index = lambda index: '4.0'
+        test_text = (
+            'First line\n'
+            'Line with target\n'
+            'Last line\n')
+        cls.text.insert('1.0', test_text)
+        cls.pat = re.compile('target')
+        cls.res = (2, (10, 16))  # line, slice indexes of 'target'
+        cls.failpat = re.compile('xyz')  # not in text
+        cls.emptypat = re.compile('\w*')  # empty match possible
+
+    def make_search(self, func):
+        def search(pat, line, col, wrap, ok=0):
+            res = func(self.text, pat, line, col, wrap, ok)
+            # res is (line, matchobject) or None
+            return (res[0], res[1].span()) if res else res
+        return search
+
+    def test_search_forward(self):
+        # search for non-empty match
+        Equal = self.assertEqual
+        forward = self.make_search(self.engine.search_forward)
+        pat = self.pat
+        Equal(forward(pat, 1, 0, True), self.res)
+        Equal(forward(pat, 3, 0, True), self.res)  # wrap
+        Equal(forward(pat, 3, 0, False), None)  # no wrap
+        Equal(forward(pat, 2, 10, False), self.res)
+
+        Equal(forward(self.failpat, 1, 0, True), None)
+        Equal(forward(self.emptypat, 2,  9, True, ok=True), (2, (9, 9)))
+        #Equal(forward(self.emptypat, 2, 9, True), self.res)
+        # While the initial empty match is correctly ignored, skipping
+        # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
+        Equal(forward(self.emptypat, 2, 10, True), self.res)
+
+    def test_search_backward(self):
+        # search for non-empty match
+        Equal = self.assertEqual
+        backward = self.make_search(self.engine.search_backward)
+        pat = self.pat
+        Equal(backward(pat, 3, 5, True), self.res)
+        Equal(backward(pat, 2, 0, True), self.res)  # wrap
+        Equal(backward(pat, 2, 0, False), None)  # no wrap
+        Equal(backward(pat, 2, 16, False), self.res)
+
+        Equal(backward(self.failpat, 3, 9, True), None)
+        Equal(backward(self.emptypat, 2,  10, True, ok=True), (2, (9,9)))
+        # Accepted because 9 < 10, not because ok=True.
+        # It is not clear that ok=True is useful going back - tjr
+        Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2, exit=2)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -177,6 +177,11 @@
   possible, since "localhost" goes through a DNS lookup under recent Windows
   versions.
 
+IDLE
+----
+
+- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster.
+
 Documentation
 -------------
 

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list