unittestgui.py

Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 """
00003 GUI framework and application for use with Python unit testing framework.
00004 Execute tests written using the framework provided by the 'unittest' module.
00005 
00006 Further information is available in the bundled documentation, and from
00007 
00008   http://pyunit.sourceforge.net/
00009 
00010 Copyright (c) 1999, 2000, 2001 Steve Purcell
00011 This module is free software, and you may redistribute it and/or modify
00012 it under the same terms as Python itself, so long as this copyright message
00013 and disclaimer are retained in their original form.
00014 
00015 IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
00016 SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
00017 THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
00018 DAMAGE.
00019 
00020 THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
00021 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
00022 PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
00023 AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
00024 SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
00025 """
00026 
00027 __author__ = "Steve Purcell (stephen_purcell@yahoo.com)"
00028 __version__ = "$Revision: 2.0 $"[11:-2]
00029 
00030 import unittest
00031 import sys
00032 import Tkinter
00033 import tkMessageBox
00034 import traceback
00035 
00036 import string
00037 tk = Tkinter # Alternative to the messy 'from Tkinter import *' often seen
00038 
00039 
00040 ##############################################################################
00041 # GUI framework classes
00042 ##############################################################################
00043 
00044 class BaseGUITestRunner:
00045     """Subclass this class to create a GUI TestRunner that uses a specific
00046     windowing toolkit. The class takes care of running tests in the correct
00047     manner, and making callbacks to the derived class to obtain information
00048     or signal that events have occurred.
00049     """
00050     def __init__(self, *args, **kwargs):
00051         self.currentResult = None
00052         self.running = 0
00053         self.__rollbackImporter = None
00054         apply(self.initGUI, args, kwargs)
00055 
00056     def getSelectedTestName(self):
00057         "Override to return the name of the test selected to be run"
00058         pass
00059 
00060     def errorDialog(self, title, message):
00061         "Override to display an error arising from GUI usage"
00062         pass
00063 
00064     def runClicked(self):
00065         "To be called in response to user choosing to run a test"
00066         if self.running: return
00067         testName = self.getSelectedTestName()
00068         if not testName:
00069             self.errorDialog("Test name entry", "You must enter a test name")
00070             return
00071         if self.__rollbackImporter:
00072             self.__rollbackImporter.rollbackImports()
00073         self.__rollbackImporter = RollbackImporter()
00074         try:
00075             test = unittest.defaultTestLoader.loadTestsFromName(testName)
00076         except:
00077             exc_type, exc_value, exc_tb = sys.exc_info()
00078             apply(traceback.print_exception,sys.exc_info())
00079             self.errorDialog("Unable to run test '%s'" % testName,
00080                              "Error loading specified test: %s, %s" % \
00081                              (exc_type, exc_value))
00082             return
00083         self.currentResult = GUITestResult(self)
00084         self.totalTests = test.countTestCases()
00085         self.running = 1
00086         self.notifyRunning()
00087         test.run(self.currentResult)
00088         self.running = 0
00089         self.notifyStopped()
00090 
00091     def stopClicked(self):
00092         "To be called in response to user stopping the running of a test"
00093         if self.currentResult:
00094             self.currentResult.stop()
00095 
00096     # Required callbacks
00097 
00098     def notifyRunning(self):
00099         "Override to set GUI in 'running' mode, enabling 'stop' button etc."
00100         pass
00101 
00102     def notifyStopped(self):
00103         "Override to set GUI in 'stopped' mode, enabling 'run' button etc."
00104         pass
00105 
00106     def notifyTestFailed(self, test, err):
00107         "Override to indicate that a test has just failed"
00108         pass
00109 
00110     def notifyTestErrored(self, test, err):
00111         "Override to indicate that a test has just errored"
00112         pass
00113 
00114     def notifyTestStarted(self, test):
00115         "Override to indicate that a test is about to run"
00116         pass
00117 
00118     def notifyTestFinished(self, test):
00119         """Override to indicate that a test has finished (it may already have
00120            failed or errored)"""
00121         pass
00122 
00123 
00124 class GUITestResult(unittest.TestResult):
00125     """A TestResult that makes callbacks to its associated GUI TestRunner.
00126     Used by BaseGUITestRunner. Need not be created directly.
00127     """
00128     def __init__(self, callback):
00129         unittest.TestResult.__init__(self)
00130         self.callback = callback
00131 
00132     def addError(self, test, err):
00133         unittest.TestResult.addError(self, test, err)
00134         self.callback.notifyTestErrored(test, err)
00135 
00136     def addFailure(self, test, err):
00137         unittest.TestResult.addFailure(self, test, err)
00138         self.callback.notifyTestFailed(test, err)
00139 
00140     def stopTest(self, test):
00141         unittest.TestResult.stopTest(self, test)
00142         self.callback.notifyTestFinished(test)
00143 
00144     def startTest(self, test):
00145         unittest.TestResult.startTest(self, test)
00146         self.callback.notifyTestStarted(test)
00147 
00148 
00149 class RollbackImporter:
00150     """This tricky little class is used to make sure that modules under test
00151     will be reloaded the next time they are imported.
00152     """
00153     def __init__(self):
00154         self.previousModules = sys.modules.copy()
00155         
00156     def rollbackImports(self):
00157         for modname in sys.modules.keys():
00158             if not self.previousModules.has_key(modname):
00159                 # Force reload when modname next imported
00160                 del(sys.modules[modname])
00161 
00162 
00163 ##############################################################################
00164 # Tkinter GUI
00165 ##############################################################################
00166 
00167 _ABOUT_TEXT="""\
00168 PyUnit unit testing framework.
00169 
00170 For more information, visit
00171 http://pyunit.sourceforge.net/
00172 
00173 Copyright (c) 2000 Steve Purcell
00174 <stephen_purcell@yahoo.com>
00175 """
00176 _HELP_TEXT="""\
00177 Enter the name of a callable object which, when called, will return a \
00178 TestCase or TestSuite. Click 'start', and the test thus produced will be run.
00179 
00180 Double click on an error in the listbox to see more information about it,\
00181 including the stack trace.
00182 
00183 For more information, visit
00184 http://pyunit.sourceforge.net/
00185 or see the bundled documentation
00186 """
00187 
00188 class TkTestRunner(BaseGUITestRunner):
00189     """An implementation of BaseGUITestRunner using Tkinter.
00190     """
00191     def initGUI(self, root, initialTestName):
00192         """Set up the GUI inside the given root window. The test name entry
00193         field will be pre-filled with the given initialTestName.
00194         """
00195         self.root = root
00196         # Set up values that will be tied to widgets
00197         self.suiteNameVar = tk.StringVar()
00198         self.suiteNameVar.set(initialTestName)
00199         self.statusVar = tk.StringVar()
00200         self.statusVar.set("Idle")
00201         self.runCountVar = tk.IntVar()
00202         self.failCountVar = tk.IntVar()
00203         self.errorCountVar = tk.IntVar()
00204         self.remainingCountVar = tk.IntVar()
00205         self.top = tk.Frame()
00206         self.top.pack(fill=tk.BOTH, expand=1)
00207         self.createWidgets()
00208 
00209     def createWidgets(self):
00210         """Creates and packs the various widgets.
00211         
00212         Why is it that GUI code always ends up looking a mess, despite all the
00213         best intentions to keep it tidy? Answers on a postcard, please.
00214         """
00215         # Status bar
00216         statusFrame = tk.Frame(self.top, relief=tk.SUNKEN, borderwidth=2)
00217         statusFrame.pack(anchor=tk.SW, fill=tk.X, side=tk.BOTTOM)
00218         tk.Label(statusFrame, textvariable=self.statusVar).pack(side=tk.LEFT)
00219 
00220         # Area to enter name of test to run
00221         leftFrame = tk.Frame(self.top, borderwidth=3)
00222         leftFrame.pack(fill=tk.BOTH, side=tk.LEFT, anchor=tk.NW, expand=1)
00223         suiteNameFrame = tk.Frame(leftFrame, borderwidth=3)
00224         suiteNameFrame.pack(fill=tk.X)
00225         tk.Label(suiteNameFrame, text="Enter test name:").pack(side=tk.LEFT)
00226         e = tk.Entry(suiteNameFrame, textvariable=self.suiteNameVar, width=25)
00227         e.pack(side=tk.LEFT, fill=tk.X, expand=1)
00228         e.focus_set()
00229         e.bind('<Key-Return>', lambda e, self=self: self.runClicked())
00230 
00231         # Progress bar
00232         progressFrame = tk.Frame(leftFrame, relief=tk.GROOVE, borderwidth=2)
00233         progressFrame.pack(fill=tk.X, expand=0, anchor=tk.NW)
00234         tk.Label(progressFrame, text="Progress:").pack(anchor=tk.W)
00235         self.progressBar = ProgressBar(progressFrame, relief=tk.SUNKEN,
00236                                        borderwidth=2)
00237         self.progressBar.pack(fill=tk.X, expand=1)
00238 
00239         # Area with buttons to start/stop tests and quit
00240         buttonFrame = tk.Frame(self.top, borderwidth=3)
00241         buttonFrame.pack(side=tk.LEFT, anchor=tk.NW, fill=tk.Y)
00242         self.stopGoButton = tk.Button(buttonFrame, text="Start",
00243                                       command=self.runClicked)
00244         self.stopGoButton.pack(fill=tk.X)
00245         tk.Button(buttonFrame, text="Close",
00246                   command=self.top.quit).pack(side=tk.BOTTOM, fill=tk.X)
00247         tk.Button(buttonFrame, text="About",
00248                   command=self.showAboutDialog).pack(side=tk.BOTTOM, fill=tk.X)
00249         tk.Button(buttonFrame, text="Help",
00250                   command=self.showHelpDialog).pack(side=tk.BOTTOM, fill=tk.X)
00251 
00252         # Area with labels reporting results
00253         for label, var in (('Run:', self.runCountVar),
00254                            ('Failures:', self.failCountVar),
00255                            ('Errors:', self.errorCountVar),
00256                            ('Remaining:', self.remainingCountVar)):
00257             tk.Label(progressFrame, text=label).pack(side=tk.LEFT)
00258             tk.Label(progressFrame, textvariable=var,
00259                      foreground="blue").pack(side=tk.LEFT, fill=tk.X,
00260                                              expand=1, anchor=tk.W)
00261 
00262         # List box showing errors and failures
00263         tk.Label(leftFrame, text="Failures and errors:").pack(anchor=tk.W)
00264         listFrame = tk.Frame(leftFrame, relief=tk.SUNKEN, borderwidth=2)
00265         listFrame.pack(fill=tk.BOTH, anchor=tk.NW, expand=1)
00266         self.errorListbox = tk.Listbox(listFrame, foreground='red',
00267                                        selectmode=tk.SINGLE,
00268                                        selectborderwidth=0)
00269         self.errorListbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=1,
00270                                anchor=tk.NW)
00271         listScroll = tk.Scrollbar(listFrame, command=self.errorListbox.yview)
00272         listScroll.pack(side=tk.LEFT, fill=tk.Y, anchor=tk.N)
00273         self.errorListbox.bind("<Double-1>",
00274                                lambda e, self=self: self.showSelectedError())
00275         self.errorListbox.configure(yscrollcommand=listScroll.set)
00276 
00277 
00278     def getSelectedTestName(self):
00279         return self.suiteNameVar.get()
00280 
00281     def errorDialog(self, title, message):
00282         tkMessageBox.showerror(parent=self.root, title=title,
00283                                message=message)
00284 
00285     def notifyRunning(self):
00286         self.runCountVar.set(0)
00287         self.failCountVar.set(0)
00288         self.errorCountVar.set(0)
00289         self.remainingCountVar.set(self.totalTests)
00290         self.errorInfo = []
00291         while self.errorListbox.size():
00292             self.errorListbox.delete(0)
00293         #Stopping seems not to work, so simply disable the start button
00294         #self.stopGoButton.config(command=self.stopClicked, text="Stop")
00295         self.stopGoButton.config(state=tk.DISABLED)
00296         self.progressBar.setProgressFraction(0.0)
00297         self.top.update_idletasks()
00298 
00299     def notifyStopped(self):
00300         self.stopGoButton.config(state=tk.ACTIVE)
00301         #self.stopGoButton.config(command=self.runClicked, text="Start")
00302         self.statusVar.set("Idle")
00303 
00304     def notifyTestStarted(self, test):
00305         self.statusVar.set(str(test))
00306         self.top.update_idletasks()
00307 
00308     def notifyTestFailed(self, test, err):
00309         self.failCountVar.set(1 + self.failCountVar.get())
00310         self.errorListbox.insert(tk.END, "Failure: %s" % test)
00311         self.errorInfo.append((test,err))
00312 
00313     def notifyTestErrored(self, test, err):
00314         self.errorCountVar.set(1 + self.errorCountVar.get())
00315         self.errorListbox.insert(tk.END, "Error: %s" % test)
00316         self.errorInfo.append((test,err))
00317 
00318     def notifyTestFinished(self, test):
00319         self.remainingCountVar.set(self.remainingCountVar.get() - 1)
00320         self.runCountVar.set(1 + self.runCountVar.get())
00321         fractionDone = float(self.runCountVar.get())/float(self.totalTests)
00322         fillColor = len(self.errorInfo) and "red" or "green"
00323         self.progressBar.setProgressFraction(fractionDone, fillColor)
00324 
00325     def showAboutDialog(self):
00326         tkMessageBox.showinfo(parent=self.root, title="About PyUnit",
00327                               message=_ABOUT_TEXT)
00328 
00329     def showHelpDialog(self):
00330         tkMessageBox.showinfo(parent=self.root, title="PyUnit help",
00331                               message=_HELP_TEXT)
00332 
00333     def showSelectedError(self):
00334         selection = self.errorListbox.curselection()
00335         if not selection: return
00336         selected = int(selection[0])
00337         txt = self.errorListbox.get(selected)
00338         window = tk.Toplevel(self.root)
00339         window.title(txt)
00340         window.protocol('WM_DELETE_WINDOW', window.quit)
00341         test, error = self.errorInfo[selected]
00342         tk.Label(window, text=str(test),
00343                  foreground="red", justify=tk.LEFT).pack(anchor=tk.W)
00344         tracebackLines = apply(traceback.format_exception, error + (10,))
00345         tracebackText = string.join(tracebackLines,'')
00346         tk.Label(window, text=tracebackText, justify=tk.LEFT).pack()
00347         tk.Button(window, text="Close",
00348                   command=window.quit).pack(side=tk.BOTTOM)
00349         window.bind('<Key-Return>', lambda e, w=window: w.quit())
00350         window.mainloop()
00351         window.destroy()
00352 
00353 
00354 class ProgressBar(tk.Frame):
00355     """A simple progress bar that shows a percentage progress in
00356     the given colour."""
00357 
00358     def __init__(self, *args, **kwargs):
00359         apply(tk.Frame.__init__, (self,) + args, kwargs)
00360         self.canvas = tk.Canvas(self, height='20', width='60',
00361                                 background='white', borderwidth=3)
00362         self.canvas.pack(fill=tk.X, expand=1)
00363         self.rect = self.text = None
00364         self.canvas.bind('<Configure>', self.paint)
00365         self.setProgressFraction(0.0)
00366 
00367     def setProgressFraction(self, fraction, color='blue'):
00368         self.fraction = fraction
00369         self.color = color
00370         self.paint()
00371         self.canvas.update_idletasks()
00372         
00373     def paint(self, *args):
00374         totalWidth = self.canvas.winfo_width()
00375         width = int(self.fraction * float(totalWidth))
00376         height = self.canvas.winfo_height()
00377         if self.rect is not None: self.canvas.delete(self.rect)
00378         if self.text is not None: self.canvas.delete(self.text)
00379         self.rect = self.canvas.create_rectangle(0, 0, width, height,
00380                                                  fill=self.color)
00381         percentString = "%3.0f%%" % (100.0 * self.fraction)
00382         self.text = self.canvas.create_text(totalWidth/2, height/2,
00383                                             anchor=tk.CENTER,
00384                                             text=percentString)
00385 
00386 def main(initialTestName=""):
00387     root = tk.Tk()
00388     root.title("PyUnit")
00389     runner = TkTestRunner(root, initialTestName)
00390     root.protocol('WM_DELETE_WINDOW', root.quit)
00391     root.mainloop()
00392 
00393 
00394 if __name__ == '__main__':
00395     import sys
00396     if len(sys.argv) == 2:
00397         main(sys.argv[1])
00398     else:
00399         main()

Generated on Wed Nov 23 19:00:56 2011 for FreeCAD by  doxygen 1.6.1