diff -r 6f16fa5223cc Lib/idlelib/idle_test/mock_tk.py --- a/Lib/idlelib/idle_test/mock_tk.py Mon Jul 08 17:52:54 2013 +0200 +++ b/Lib/idlelib/idle_test/mock_tk.py Mon Jul 08 18:15:08 2013 -0400 @@ -1,4 +1,5 @@ """Classes that replace tkinter gui objects used by an object being tested. + A gui object is anything with a master or parent paramenter, which is typically required in spite of what the doc strings say. """ @@ -15,8 +16,10 @@ return self.value class Mbox_func: - """Generic mock for messagebox functions. All have same call signature. - Mbox instantiates once for each function. Tester uses attributes. + """Generic mock for messagebox functions, which all have the same signature. + + Instead of displaying a message box, the mock's call method saves the + arguments as instance attributes, which test functions can then examime. """ def __init__(self): self.result = None # The return for all show funcs @@ -30,6 +33,7 @@ class Mbox: """Mock for tkinter.messagebox with an Mbox_func for each function. + This module was 'tkMessageBox' in 2.x; hence the 'import as' in 3.x. Example usage in test_module.py for testing functios in module.py: --- @@ -49,9 +53,9 @@ def tearDownClass(cls): module.tkMessageBox = orig_mbox --- - When tkMessageBox functions are the only gui making calls in a method, - this replacement makes the method gui-free and unit-testable. - For 'ask' functions, set func.result return before calling method. + For 'ask' functions, set func.result return value before calling the method + that uses the message function. When tkMessageBox functions are the + only gui alls in a method, this replacement makes the method gui-free, """ askokcancel = Mbox_func() # True or False askquestion = Mbox_func() # 'yes' or 'no' @@ -61,3 +65,165 @@ showerror = Mbox_func() # None showinfo = Mbox_func() # None showwarning = Mbox_func() # None + +from _tkinter import TclError + +class Text: + """A semi-functional non-gui replacement for tkinter.Text text editors. + + The mock's data model is that a text is a list of \n-terminated lines. + The mock adds an empty string at the beginning of the list so that the + index of actual lines start at 1, as with Tk. The methods never see this. + Tk initializes files with a terminal \n that cannot be deleted. It is + invisible in the sense that one cannot move the cursor beyond it. + + This class is only tested (and valid) with strings of ascii chars. + For testing, we are not concerned with Tk Text's treatment of, + for instance, 0-width characters or character + accent. + """ + def __init__(self): + self.data = ['', '\n'] + + def _decode(self, position): # private helper + """Returns self.date line and column indexes for position. + + Position is row.col float, 'row.colx' string, or 'end', where row and + col are decimal ints and colx is integer, '0 lineend', or 'end'. + + Warning: return is contrained by current self.data; 1,0 if empty! + """ + + lastline = len(self.data) - 1 # same as number of text lines + position = str(position) # Tkinter allows floats to be passed in + if position.lower() == 'end': + # corresponds to last line, just before newline character + return lastline, len(self.data[lastline]) - 1 + + line, col = position.split('.') + line = int(line) + + # Out of bounds line same as 'end' + if line > lastline: + return lastline, len(self.data[lastline]) - 1 + # or first index if less than 1 + elif line < 1: + return 1, 0 + + linelength = len(self.data[line]) -1 + if col == '0 lineend' or col.lower() == 'end': + return line, linelength + return line, min(int(col), linelength) + + def insert(self, position, text): + if not text: # ''.splitlines() is [], not [''] + return + text = text.splitlines(True) + if text[-1][-1] == '\n': + text.append('') + line, col = self._decode(position) + before = self.data[line][:col] + after = self.data[line][col:] + self.data[line] = before + text[0] + self.data[line+1:line+1] = text[1:] + self.data[line+len(text)-1] += after + + def get(self, start, end=None): + startline, startcol = self._decode(start) + + if end is None: + if str(start).lower() == 'end': + return '' + return self.data[startline][startcol] + else: + endline, endcol = self._decode(end) + + if str(end).lower() == 'end': + endcol += 1 + + if startline == endline: + return self.data[startline][startcol:endcol] + else: + lines = [self.data[startline][startcol:]] + for i in range(startline+1, endline): + lines.append(self.data[i]) + lines.append(self.data[endline][:endcol]) + return ''.join(lines) + + def delete(self, start, end=None): + startline, startcol = self._decode(start) + if end is None: + # delete single character at start index + self.data[startline] = self.data[startline][:startcol] + \ + self.data[startline][startcol+1:] + return + else: + endline, endcol = self._decode(end) + if startline > endline: + return + elif startline == endline: + if startcol >= endcol: + return + else: + self.data[startline] = self.data[startline][:startcol] + \ + self.data[startline][endcol:] + elif endline - 1 == startline and endcol == 0: + del self.data[startline] + else: + self.data[startline] = self.data[startline][:startcol] + \ + self.data[endline][endcol:] + for i in range(startline+1, endline+1): + del self.data[startline+1] + if end.lower() == 'end': + self.data[-1] = '\n' + + + def index(self, position): + if position.lower() == 'end': + return str(len(self.data)) + '.' + '0' + elif position == 'sel.first': + return '1.0' + elif position == 'sel.last': + return 'end' + elif position == 'insert': + return '1.0' + else: + return + # TODO: support other index types (eg. line.column, line.end, ...) + + def compare(self, index1, op, index2): + line1, col1 = self._decode(index1) + line2, col2 = self._decode(index2) + if op == '<': + return line1 < line2 or line1 == line2 and col1 < col2 + elif op == '<=': + return line1 < line2 or line1 == line2 and col1 <= col2 + elif op == '>': + return line1 > line2 or line1 == line2 and col1 > col2 + elif op == '>=': + return line1 > line2 or line1 == line2 and col1 >= col2 + elif op == '==': + return line1 == line2 and col1 == col2 + elif op == '!=': + return line1 != line2 or col1 != col2 + else: + raise TclError('''bad comparison operator "%s":''' + '''must be <, <=, ==, >=, >, or !=''' % op) + + def tag_remove(self, tagName, index1, index2=None): + pass + + def mark_set(self, name, index): + pass + + def bind(sequence=None, func=None, add=None): + pass + + def undo_block_start(self, *args): + pass + + def undo_block_stop(self, *args): + pass + + def see(self, index): + pass + diff -r 6f16fa5223cc Lib/idlelib/idle_test/test_text.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_text.py Mon Jul 08 18:15:08 2013 -0400 @@ -0,0 +1,137 @@ +import unittest +from test.support import requires + +from _tkinter import TclError +import tkinter as tk + +class TextTest(object): + Text = None + def setUp(self): + self.text = self.Text() + + def test_init(self): + self.assertEqual(self.text.get('1.0'), '\n') + self.assertEqual(self.text.get('end'), '') + + def test_get(self): + self.text.insert('1.0','hello\nworld') + self.assertEqual(self.text.get('end'),'') + self.assertEqual(self.text.get('1.0'),'h') + self.assertEqual(self.text.get('1.0','1.1'),'h') + self.assertEqual(self.text.get('1.0','1.3'),'hel') + self.assertEqual(self.text.get('1.1','1.3'),'el') + self.assertEqual(self.text.get('1.0','1.0 lineend'),'hello') + self.assertEqual(self.text.get('1.0 lineend'),'\n') + self.assertEqual(self.text.get('1.0','end'),'hello\nworld\n') + self.assertEqual(self.text.get('1.1','2.3'),'ello\nwor') + + def test_insert(self): + self.text.insert('1.0', 'hello\nworld') + self.assertEqual(self.text.get('1.0','end'),'hello\nworld\n') + + self.text.insert('1.0', '') + self.assertEqual(self.text.get('1.0','end'),'hello\nworld\n') + + self.text.insert('1.0','*') + self.assertEqual(self.text.get('1.0','end'),'*hello\nworld\n') + + self.text.insert('1.0 lineend','*') + self.assertEqual(self.text.get('1.0','end'),'*hello*\nworld\n') + + self.text.insert('2.3','*') + self.assertEqual(self.text.get('1.0','end'),'*hello*\nwor*ld\n') + + self.text.insert('end','x') + self.assertEqual(self.text.get('1.0','end'),'*hello*\nwor*ldx\n') + + self.text.insert('1.4','x\n') + self.assertEqual(self.text.get('1.0','end'),'*helx\nlo*\nwor*ldx\n') + + def test_no_delete(self): + # if start >= end, there is no deletion + hw = 'hello\nworld' + hwn = hw+'\n' + self.text.insert('1.0', hw) + self.text.delete('2.0', '1.0') + self.assertEqual(self.text.get('1.0','end'), hwn) + self.text.delete('1.4', '1.0') + self.assertEqual(self.text.get('1.0','end'), hwn) + self.text.delete('1.4', '1.4') + self.assertEqual(self.text.get('1.0','end'), hwn) + + def test_delete(self): + hw = 'hello\nworld' + self.text.insert('1.0', hw) + self.text.delete('1.0','1.0 lineend') + self.assertEqual(self.text.get('1.0','end'),'\nworld\n') + + self.text.delete('1.0','end') + self.assertEqual(self.text.get('1.0','end'),'\n') + + self.text.insert('1.0','hello\nworld') + self.text.delete('1.0','2.0') + self.assertEqual(self.text.get('1.0','end'),'world\n') + + self.text.delete('1.0','end') + self.assertEqual(self.text.get('1.0','end'),'\n') + + self.text.insert('1.0','hello\nworld') + self.text.delete('1.2','2.3') + self.assertEqual(self.text.get('1.0','end'),'held\n') + + def test_index(self): + self.text.insert('1.0','hello\nworld') + self.assertEqual(self.text.index('end'),'3.0') + + def test_compare(self): + # need data so indexes not squished to 1,0 + self.text.insert('1.0', 'First\nSecond\nThird\n') + self.assertRaises(TclError, self.text.compare, '2.2', '=', '2.2') + for op, less1, less0, equal, greater0, greater1 in ( + ('<', True, True, False, False, False), + ('<=', True, True, True, False, False), + ('>', False, False, False, True, True), + ('>=', False, False, True, True, True), + ('==', False, False, True, False, False), + ('!=', True, True, False, True, True), + ): + self.assertEqual(self.text.compare('1.1', op, '2.2'), less1, op) + self.assertEqual(self.text.compare('2.1', op, '2.2'), less0, op) + self.assertEqual(self.text.compare('2.2', op, '2.2'), equal, op) + self.assertEqual(self.text.compare('2.3', op, '2.2'), greater0, op) + self.assertEqual(self.text.compare('3.3', op, '2.2'), greater1, op) + + +class MockTextTest(TextTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + from idlelib.idle_test.mock_tk import Text + cls.Text = Text + + def test__decode(self): + self.assertEqual(self.text._decode('end'),(1,0)) + self.text.insert('1.0','hello\nworld') + self.assertEqual(self.text._decode('1.0'), (1,0)) + self.assertEqual(self.text._decode('1.0 lineend'),(1,5)) + self.assertEqual(self.text._decode('end'),(2,5)) + self.assertEqual(self.text._decode('2.end'), (2,5)) + self.assertEqual(self.text._decode('2.2'),(2,2)) + + +class TkTextTest(TextTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + from tkinter import Tk, Text + cls.Text = Text + cls.root = Tk() + + @classmethod + def tearDownClass(cls): + cls.root.destroy() + +if __name__ == '__main__': + unittest.main(verbosity=2) + #unittest.main()