[IronLute-CVS] ironlute/tk/tests generalTkTest.py,NONE,1.1 lineWrapperTest.py,NONE,1.1 tkTestBase.py
Status: Pre-Alpha
Brought to you by:
thejerf
From: Jeremy B. <th...@us...> - 2005-03-10 04:26:17
|
Update of /cvsroot/ironlute/ironlute/tk/tests In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv27941/tk/tests Added Files: generalTkTest.py lineWrapperTest.py tkTestBase.py Log Message: Welcome to Sourceforge, Iron Lute. --- NEW FILE: lineWrapperTest.py --- """Tk has a reasonably nice text widget, but it has one major flaw: It has this strange idea that if you have text that has the following lengths: 1 screen line 5 screen lines 1 screen line and you have the insertion cursor somewhere in the first line, to get to the bottom line, you need press down only twice. I hate this with a passion. It is lazy programming, not user focus. The good news is that "someday" this should be much more easily fixed; see http://www.talkaboutprogramming.com/group/comp.lang.tcl.announce/messages/3529.html (specifically, 'displaylines') which is supposedly implemented and accepted. As of this writing (May 2004), it's not in my actual copy of Tk or Tkinter, and with Gentoo I'm typically pretty up-to-date with these things. Therefore, I must assume that Iron Lute will be released long before everybody has updated to the Tk necessary to use 'displaylines'. Second, if the characters are not literally displayed on the screen at the time, Tk has no idea where they are. Thus, if you are on the top line of the screen and try to go up, or the bottom line and try to go down, you can't just add one line to the display coordinates and get the cursot position; IIRC the widget bails out and jumps to one of the endpoints. That means we get to try to compute the locations ourselves and pass Tk in the literal position that we want the cursor to be in (which it probably still doesn't understand but does correctly interpret at display time, which is good enough). That means we get to try to match the Tk line wrap algorithm, pixel for pixel (since even a single pixel may cause a line wrap). Oh, hooray; let's reverse-engineer a potentially platform-dependent, Tk version dependent, pixel-slinging algorithm with multiple fonts, God only knows what kerning, and lots of other tedious details. Anyhow, this is exactly the sort of thing that unit testing was made for; if a platform passes this testing suite then all is probably well. Also it looks pretty while it runs (since Tk requires actual screen updates to update its internal parameters.) """ # Next up: display our line breaks vs. tk line breaks in error # message, maybe w/ dlineinfo? import os import sys if __name__ != "__main__": f = __file__ d = os.path.normpath(os.path.join(os.getcwd(), os.path.dirname(f))) if d not in sys.path: sys.path.append(d) d = d[:d.rfind(os.sep)] if d not in sys.path: sys.path.append(d) d = d[:d.rfind(os.sep)] if d not in sys.path: sys.path.append(d) else: sys.path.append("..") sys.path.append("../..") import unittest import random import gui import outline import tk import tkTestBase from Tkinter import * # importing this directly results in some aliasing problems TextNodeWidget = tk.TextNodeWidget requestedLines = tk._requestedLines class LineWrapperTest(tkTestBase.TkTestBase): """This defines several tests to torture the line wrapping algorithm, both with extensive randomized testing and specific cases known to cause problems on various platforms.""" def setUp(self): tkTestBase.TkTestBase.setUp(self) self.hackWidgetForFullHeight() def getLines(self, text): """Wrap the requestedLines calls so we only have to pass the text in.""" return requestedLines(text, self.font, self.frame.height / self.frame.fontHeight, self.widget.width, debug = 1) def checkSomeText(self, text): """Given some text, validate Tk and requestedLines agree. This ought to be named "testSomeText", but then then Unit Test framework thinks this is a test.""" self.node.setData(text) self.il.update_idletasks() computedLines, computedWidth, lines = self.getLines(text) # Stupid TK won't give me the end, it just says None bbox = self.widget.bbox("1.end - 1char") tkLines = (bbox[1] / self.frame.fontHeight) + 1 if text: tkWidth = bbox[0] + self.font.measure(text[-1]) else: tkWidth = bbox[0] # Compute a comparision to display if this fails. compare = "We said:\n\n" compare += "|\n".join(lines) compare += "|\n" compare += "Tk said:\n\n" tkLineList = [self.widget.get("@0,%s" % (y * self.widget.fontHeight), "@65535,%s" % (y * self.widget.fontHeight)) for y in range(tkLines)] compare += "|\n".join(tkLineList) compare += "|\n\n" # I've added a 10-second wait upon failure so you have # a chance to see how Tk lays out the text. In rare cases, Tk actually # *cuts off* the last character, which you can see the text # window, and my wrapping algorithm is actually correct. # It only seems to happen if the last char is unicode # Thus, if Tk and my wrapping algorithm disagree by one line, # and my last line ends in unicode with no spaces in it, I # bypass the following to avoid what I consider a spurious # error. isUnicode = 0 try: lines[-1][-1].encode('ascii') except: isUnicode = 1 if tkLines == computedLines - 1 and \ (isUnicode or (len(lines[-1]) == 1 and len(lines[-1][0]) == 1)): print "aborted test case; see lineWrapperTest.py for explanation" return try: self.assert_(tkLines == computedLines, ("For text '%s', we computed a line count of " "%s, but Tk said %s (computed width %s, " "tk said %s).\n%s" % (text, computedLines, tkLines, computedWidth, tkWidth, compare)).encode('utf-8')) self.assert_(tkWidth == computedWidth, "For text '%s', we computed a line width of " "%s, but Tk said %s.\n%s" % (text, computedWidth, tkWidth, compare)) except: import time time.sleep(10) raise def testBasicLineWrapping(self): """Tk: Test line wrapping reverse engineering (basic)""" # Set up a node we can load text into self.checkSomeText('') self.checkSomeText(".") self.checkSomeText("This is a multi-word test.") self.node.setData("This is a multi-line test." * 4) self.checkSomeText("This is a multi-line test." * 4) def testSpacesTortureTest(self): """Tk: Test line wrapping w/ lots of spaces in the text""" # Unlike some text widgets, Tk won't collapse spaces at the # end of a line, it wraps them onto the next. Make sure # we correctly match that behavior. maxEms = self.maxEms() text = "M" * maxEms + " " * 10 self.checkSomeText(text) self.checkSomeText(text * 2) self.checkSomeText(text * 3 + "f") # Check large number of spaces in the middle text = "M" * (maxEms / 2) + " " * 60 + "M" * (maxEms / 2) self.checkSomeText(text) def testLineForceWrap(self): """Tk: Test line wrapping when word is longer then one line""" maxEms = self.maxEms() text = "M" * (maxEms + 1) self.checkSomeText(text) # Verify this was a forced wrap. self.assert_(self.getLines(text)[0] == 2) def testGeneralTortureTest(self): """Tk: Test Line Wrapping torture test""" # Basically, this throws everything we can think of # at the wrapper: Unicode, spaces, etc. dataChunks = ["MMMMMM ", "iiiiiiii ", "abcdef ", u" \u1234", u"ue\u1233\u1222", " M", "b b b b b b b b b", " " * 20, " kr ", "12345678 ", "90!@#$%^&*() ", "{}|:\"<>? "] widths = [self.font.measure(x) for x in dataChunks] maxWidth = max(widths) # Try to verify we never overflow the widget (with some # health leeway) maxUsableWidth = (self.widget.height / self.widget.fontHeight) \ * self.widget.width maxChoosable = maxUsableWidth / maxWidth # Fudge factor maxChoosable -= 20 for i in range(25): numberOfChoices = random.randint(10, maxChoosable) chunks = [] for i in range(numberOfChoices): chunks.append(random.choice(dataChunks)) self.checkSomeText(''.join(chunks)) def testUnicodeKerning(self): """Tk: Line wrapping: Unicode fonts aren't kerned.""" width = self.font.measure(u"\u1234") for i in range(10): self.assert_(self.font.measure(u"\u1234" * i) == i * width) if __name__ == "__main__": unittest.main() --- NEW FILE: tkTestBase.py --- """This provides a base class for GUI testing; for every test, it sets up a document, node, handle, il (Iron Lute frame instance), and font, and assigns those things to class attributes of that name. It also automatically shuts down the window at the end of the test. It is very useful for testing.""" import os import sys if __name__ != "__main__": f = __file__ if os.path.dirname(f) not in sys.path: sys.path.append(os.path.dirname(f)) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") sys.path.append("../..") import unittest import gui import outline import tk from Tkinter import * class TkTestBase(unittest.TestCase): def setUp(self): gui.activateGui() self.document = outline.quickMakeNodes(['']) self.node = self.document[0] self.rootHandle = self.document.getRootHandle() self.handle = self.rootHandle[0] self.il = gui.IronLute(self.document) self.frame = self.il.frame self.il.update_idletasks() self.widget = self.frame.handlesToWidgets[self.handle] self.widget.focus_set() self.assert_(self.frame.currentFocus is self.handle) self.font = self.widget.font self.assert_(isinstance(self.widget, tk.textnodewidget.TextNodeWidget)) def tearDown(self): self.il.closeCheck() del self.document del self.node del self.rootHandle del self.handle del self.il del self.frame del self.widget del self.font def hackWidgetForFullHeight(self): """This hacks the widget to take up the full height of the frame, useful for testing the line wrapping algorithms.""" self.widget.place(height = self.frame.height) self.widget.height = self.frame.height def clearDocument(self, document = None): """Clear out the current document, and reset member appropriately.""" for link in self.document.data.outgoing[:]: link.clear() self.document.addNewChild() self.node = self.document[0] self.handle = self.rootHandle[0] # Should this be necessary # FIXME: handle should not be necessary self.frame.syncWithDocument(self.handle) self.il.update_idletasks() self.widget = self.frame.handlesToWidgets[self.handle] self.widget.focus_set() self.assert_(self.frame.currentFocus is self.handle) # ON POSTING EVENTS: # Ideally, I'd like to be able to say "The user pressed down." # Perhaps at some point we'll work out a way to do that with # platform-specific libraries. Unfortunately, postEvent("<Down>") # doesn't work as we'd like; it looks like Tk eats it. If we want # to check a keypress, we must manually call the event handler # method. def postEvent(self, event): """Posts an event to the window.""" self.il.winfo_toplevel().event_generate(event) self.il.update_idletasks() def maxEms(self, widget = None, char = "M", indent = 0): """Returns the maximum number of some char without wrapping. widget defaults to self.widget. char defaults to a capital M, since this function is named after the typographical unit. Another char can be substituted if necessary.""" if widget is None: widget = self.widget em = self.font.measure(char) return (self.widget.width - tk.outlineframe.constants.INDENT_WIDTH * indent) / em def maxLines(self): """Returns the maximum number of unclipped lines you can fit in the current frame with the current font.""" return self.frame.height / self.frame.fontHeight --- NEW FILE: generalTkTest.py --- """This performs a wide variety of general testing on the Tk interface. It tests that nodes resize, scrollbars are always correct, pressing 'up' or 'down' on the top or bottom of the screen does the sane thing, or other things of that nature.""" import os import sys if __name__ != "__main__": f = __file__ if os.path.dirname(f) not in sys.path: sys.path.append(os.path.dirname(f)) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) f = os.path.normpath(os.path.dirname(f) + os.sep + os.pardir) if f not in sys.path: sys.path.append(f) else: sys.path.append("..") sys.path.append("../..") import time from Tkinter import * import unittest import tkTestBase import tk #from outlineframe import LineSizeCache class EventMock(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) class GeneralTkTest(tkTestBase.TkTestBase): def trestDownGeneral(self): """Tk: Pushing 'down' works?""" # This is one of the foundational elements of the GUI, and # there's a lot of cases. # By my count, we have the following (mostly) independent axes: # * On the bottom line of the screen or not # * there's a node below you or not # * you're actually on the bottom line of your node or not # * the node below you is indented/same level/dedented # * current node extends off top / doesn't # That's 2 * 2 * 2 * 3 * 2 possibilities, or 48 distinct # possibilities. Wow. # FIXME: Also need to handle holding shift for a new selection, # holding shift to extend a selection, and holding shift # to wrap back on a selection (cursor at 4, selection 4-8, # after shift-down selection should be 8-12) # FIXME: Actually implement nodeExtendsOffTop # Forgive the non-standard indenting, reason should be # obvious: for bottomLineOfScreen in (True, False): for nodeBelow in (True, False): for bottomLineOfNode in (True, False): for indentLevel in (-1, 0, 1): for nodeExtendsOffTop in (True, False): self.doTestDown(bottomLineOfScreen, nodeBelow, bottomLineOfNode, indentLevel, nodeExtendsOffTop) def doTestDown(self, bottomLineOfScreen, nodeBelow, bottomLineOfNode, indentLevel, nodeExtendsOffTop): """Do the actual testing for this test case.""" # Print out which case we're running: #print ["not bottom line of screen, ", "bottom line of screen, " # ][bottomLineOfScreen], #print ["no node below, ", "node below, "][nodeBelow], #print ["not bottom of node, ", "bottom of node, " # ][bottomLineOfNode], #print ("indent level %s, " % indentLevel), #print ["node not extend off top", "node extends off top" # ][nodeExtendsOffTop] self.clearDocument() # Start constructing the test case. maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * maxEms twoLines = oneLine + " " + oneLine maxLines = self.maxLines() indentWidth = tk.outlineframe.constants.INDENT_WIDTH # Make sure we have room to work with self.assert_(maxLines > 5) self.assert_(maxEms > 5) # FIXME: Ignoring nodeExtendsOffTop nodeToAddTo = self.document handle = self.rootHandle # We start out with one line on the screen. self.currentLines = 1 # If the indent = -1, we have to pull all these lines over # and redefine what "oneLine" and "twoLines" are if indentLevel == -1: handle = self.rootHandle[0] nodeToAddTo = handle.node maxEms = self.maxEms(indent = 1) oneLine = "M" * maxEms twoLines = oneLine + " " + oneLine # Start by padding the text node widget down to the # second-to-last line for i in range(maxLines - 3): nodeToAddTo.addNewChild(childArgs=(oneLine,)) self.frame.expandAll() # We now have two clear lines on the bottom. # What we do with them depends on "bottomLineOfNode" # and "bottomLineOfScreen" if bottomLineOfNode and bottomLineOfScreen: # A two-line widget that we're on the bottom of focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(maxEms + 4) elif bottomLineOfNode and not bottomLineOfScreen: # a one-line widget focusNode = nodeToAddTo.addNewChild(childArgs=(oneLine,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) elif not bottomLineOfNode and bottomLineOfScreen: # Another one-line widget to eat space, and then # a two-line widget that we are on the top line of nodeToAddTo.addNewChild(childArgs=(oneLine,)) focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) elif not bottomLineOfNode and not bottomLineOfScreen: # A two-line widget and we're one the first line focusNode = nodeToAddTo.addNewChild(childArgs=(twoLines,)) widget = self.frame.handlesToWidgets[handle[-1]] widget.focus_set() widget.setCursorPos(4) widget.update_idletasks() #print repr(widget.reflectedHandle.node), repr(focusNode) #print widget.reflectedHandle.node, ', ', focusNode self.assert_(focusNode is widget.reflectedHandle.node) originalX = tk.Bbox(widget.bbox(INSERT)).x originalTopHandle = self.frame.topHandle originalFocus = self.frame.currentFocus if nodeBelow: # Place a node below the current node with suitable # indentation. if indentLevel == -1: # Add to the original document to get a node # one to the left target = self.document.addNewChild(childArgs=(oneLine,)) elif indentLevel == 0: target = nodeToAddTo.addNewChild(childArgs=(oneLine,)) elif indentLevel == 1: target = focusNode.addNewChild(childArgs=(oneLine,)) self.frame.expandAll() self.frame.syncWithDocument() # We have now completely handled the construction for # everything except nodeExtendsOffTop # Make sure Tk is up-to-date, then actually run the Down # command self.frame.update_idletasks() noModifiers = EventMock(state = 0) widget.down(noModifiers) # Now, we start validating the results, based on the flags # If we were on the bottom and there was somewhere to go (a # node below the focused node, or another line in that node), # the outlineframe should have a different topHandle # Otherwise, it should be the same somewhereToGo = nodeBelow or not bottomLineOfNode if bottomLineOfScreen and somewhereToGo: self.assert_(self.frame.topHandle is not originalTopHandle) else: self.assert_(self.frame.topHandle is originalTopHandle) # If we were on the bottom line of the node and there was a # node below, see that the new focus is that new # node. Otherwise, the focus should be the same. if bottomLineOfNode and nodeBelow: self.assert_(self.frame.currentFocus.node is target) else: self.assert_(self.frame.currentFocus is originalFocus) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) # Assert basic functioning of the XLock newX = tk.Bbox(self.frame.handlesToWidgets[self.frame.currentFocus].bbox(INSERT)).x # indentLevel * INDENT_WIDTH + newX - oldX should be within an # Em of zero # This is the difference between the 'pure' x the cursor # should have, and the real one it has indentDifference = indentLevel * indentWidth + newX - originalX # FIXME: Is this a legitimate problem? #print indentDifference, em, indentLevel, newX, originalX #self.assert_(-em <= indentDifference <= em) def trestUpGeneral(self): """Tk: Pushing 'up' works?""" # Mirror image to 'down' for topLineOfScreen in (True, False): for nodeAbove in (True, False): for topLineOfNode in (True, False): for indentLevel in (-1, 0, 1): for nodeExtendsOffBottom in (True, False): self.doTestUp(topLineOfScreen, nodeAbove, topLineOfNode, indentLevel, nodeExtendsOffBottom) def doTestUp(self, topLineOfScreen, nodeAbove, topLineOfNode, indentLevel, nodeExtendsOffBottom): """Do the actual testing for this test case.""" # Print out which case we're running: #print ["not top line of screen, ", "top line of screen, " # ][topLineOfScreen], #print ["no node above, ", "node above, "][nodeAbove], #print ["not top of node, ", "top of node, " # ][topLineOfNode], #print ("indent level %s, " % indentLevel), #print ["node not extend off bottom", "node extends off bottom" # ][nodeExtendsOffBottom] # Bypass some tests: If there's no node above, skip all but # one indentLevel if not nodeAbove and indentLevel != 0: return self.clearDocument() # Start constructing the test case. indentWidth = tk.outlineframe.constants.INDENT_WIDTH maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * (maxEms - (indentWidth / em) - 1) twoLines = oneLine + " " + oneLine maxLines = self.maxLines() # Make sure we have room to work with self.assert_(maxLines > 5) self.assert_(maxEms > 5) # Business: # # * If nodeAbove, prepare and re-focus # * prepare for each of top(Node/Screen) target = self.handle self.node.setData(oneLine) # If there's a node above the target node, make it with the # proper indent if nodeAbove: if indentLevel == -1: # Make a child, that's our target target.node.addNewChild() target = target[0] elif indentLevel == 0: self.document.addNewChild() target = self.rootHandle[1] elif indentLevel == 1: # Complicated case target.node.addNewChild(childArgs=(oneLine,)) self.document.addNewChild() target = self.rootHandle[1] self.frame.expandAll() # We now have the node above set up; set up the target node # and screen position target.node.setData(twoLines) lineToSync = 0 if not topLineOfScreen: lineToSync -= 1 if not topLineOfNode: lineToSync += 1 self.frame.syncWithDocument(target, lineToSync) widget = self.frame.handlesToWidgets[target] if topLineOfNode: widget.setCursorPos(4) else: widget.setCursorPos(4 + maxEms) widget.update_idletasks() self.assert_(target is widget.reflectedHandle) originalX = tk.Bbox(widget.bbox(INSERT)).x originalTopHandle = self.frame.topHandle originalTopLine = self.frame.topLine originalFocus = self.frame.currentFocus self.frame.expandAll() self.frame.syncWithDocument() # We have now completely handled the construction for # everything except nodeExtendsOffTop # Make sure Tk is up-to-date, then actually run the Up # command self.frame.update_idletasks() noModifiers = EventMock(state = 0) #print "focus before: %s" % self.frame.currentFocus #print self.frame.handlesToWidgets #if not topLineOfNode: # time.sleep(5) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) widget.up(noModifiers) #if not topLineOfNode: # time.sleep(5) # Now, we start validating the results, based on the flags # If we had to scroll, the topHandle or topLine should be # different if topLineOfScreen and nodeAbove: #print self.frame.topHandle, originalTopHandle, \ # self.frame.topLine, originalTopLine self.assert_((self.frame.topHandle is not originalTopHandle) or (self.frame.topLine != originalTopLine)) else: self.assert_(self.frame.topHandle is originalTopHandle) # If we were on the bottom line of the node and there was a # node below, see that the new focus is that new # node. Otherwise, the focus should be the same. if topLineOfNode and nodeAbove: if indentLevel == 1: self.assert_(self.frame.currentFocus is self.rootHandle[0][0]) else: self.assert_(self.frame.currentFocus is self.rootHandle[0]) else: self.assert_(self.frame.currentFocus is originalFocus) #print self.frame.currentFocus, self.frame.handlesToWidgets #print target #print list(self.rootHandle.depthFirst(yieldMarkers = 1)) self.assert_(self.frame.currentFocus in self.frame.handlesToWidgets) # Assert basic functioning of the XLock newX = tk.Bbox(self.frame.handlesToWidgets[self.frame.currentFocus].bbox(INSERT)).x # indentLevel * INDENT_WIDTH + newX - oldX should be within an # Em of zero # This is the difference between the 'pure' x the cursor # should have, and the real one it has indentDifference = indentLevel * indentWidth + newX - originalX # FIXME: Is this a legitimate problem? #print indentDifference, em, indentLevel, newX, originalX #self.assert_(-em <= indentDifference <= em) def testScrollbar(self): """Tk: Scrollbar works""" # The algorithm the scrollbar uses is generally recursive # so it *should* suffice just to verify the root handle's # linecount # Here's how to manually calculate a line count: def realLineCount(frame, handle): import outlineframe cache = outlineframe.LineSizeCache(frame) count = 0 for handle, depth in frame.handleWalkerForward(handle): count += frame.lines.handle[handle] return count def compare(): claimed = self.frame.lines.child[self.rootHandle] computed = realLineCount(self.frame, self.rootHandle[0]) self.assert_(claimed == computed, "Claimed line count %s does not match " "computed line count %s." % (claimed, computed)) compare() indentWidth = tk.outlineframe.constants.INDENT_WIDTH maxEms = self.maxEms() em = self.font.measure("M") oneLine = "M" * (maxEms - (indentWidth / em) - 1) eightLines = (oneLine + " ") * 8 self.node.setData(eightLines) compare() if __name__ == "__main__": unittest.main() |