mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 10:26:02 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			477 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			477 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| """
 | |
| GUI framework and application for use with Python unit testing framework.
 | |
| Execute tests written using the framework provided by the 'unittest' module.
 | |
| 
 | |
| Updated for unittest test discovery by Mark Roddy and Python 3
 | |
| support by Brian Curtin.
 | |
| 
 | |
| Based on the original by Steve Purcell, from:
 | |
| 
 | |
|   http://pyunit.sourceforge.net/
 | |
| 
 | |
| Copyright (c) 1999, 2000, 2001 Steve Purcell
 | |
| This module is free software, and you may redistribute it and/or modify
 | |
| it under the same terms as Python itself, so long as this copyright message
 | |
| and disclaimer are retained in their original form.
 | |
| 
 | |
| IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
 | |
| SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
 | |
| THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
 | |
| DAMAGE.
 | |
| 
 | |
| THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
 | |
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 | |
| PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
 | |
| AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 | |
| SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 | |
| """
 | |
| 
 | |
| __author__ = "Steve Purcell (stephen_purcell@yahoo.com)"
 | |
| 
 | |
| import sys
 | |
| import traceback
 | |
| import unittest
 | |
| 
 | |
| import tkinter as tk
 | |
| from tkinter import messagebox
 | |
| from tkinter import filedialog
 | |
| from tkinter import simpledialog
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| ##############################################################################
 | |
| # GUI framework classes
 | |
| ##############################################################################
 | |
| 
 | |
| class BaseGUITestRunner(object):
 | |
|     """Subclass this class to create a GUI TestRunner that uses a specific
 | |
|     windowing toolkit. The class takes care of running tests in the correct
 | |
|     manner, and making callbacks to the derived class to obtain information
 | |
|     or signal that events have occurred.
 | |
|     """
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         self.currentResult = None
 | |
|         self.running = 0
 | |
|         self.__rollbackImporter = RollbackImporter()
 | |
|         self.test_suite = None
 | |
| 
 | |
|         #test discovery variables
 | |
|         self.directory_to_read = ''
 | |
|         self.top_level_dir = ''
 | |
|         self.test_file_glob_pattern = 'test*.py'
 | |
| 
 | |
|         self.initGUI(*args, **kwargs)
 | |
| 
 | |
|     def errorDialog(self, title, message):
 | |
|         "Override to display an error arising from GUI usage"
 | |
|         pass
 | |
| 
 | |
|     def getDirectoryToDiscover(self):
 | |
|         "Override to prompt user for directory to perform test discovery"
 | |
|         pass
 | |
| 
 | |
|     def runClicked(self):
 | |
|         "To be called in response to user choosing to run a test"
 | |
|         if self.running: return
 | |
|         if not self.test_suite:
 | |
|             self.errorDialog("Test Discovery", "You discover some tests first!")
 | |
|             return
 | |
|         self.currentResult = GUITestResult(self)
 | |
|         self.totalTests = self.test_suite.countTestCases()
 | |
|         self.running = 1
 | |
|         self.notifyRunning()
 | |
|         self.test_suite.run(self.currentResult)
 | |
|         self.running = 0
 | |
|         self.notifyStopped()
 | |
| 
 | |
|     def stopClicked(self):
 | |
|         "To be called in response to user stopping the running of a test"
 | |
|         if self.currentResult:
 | |
|             self.currentResult.stop()
 | |
| 
 | |
|     def discoverClicked(self):
 | |
|         self.__rollbackImporter.rollbackImports()
 | |
|         directory = self.getDirectoryToDiscover()
 | |
|         if not directory:
 | |
|             return
 | |
|         self.directory_to_read = directory
 | |
|         try:
 | |
|             # Explicitly use 'None' value if no top level directory is
 | |
|             # specified (indicated by empty string) as discover() explicitly
 | |
|             # checks for a 'None' to determine if no tld has been specified
 | |
|             top_level_dir = self.top_level_dir or None
 | |
|             tests = unittest.defaultTestLoader.discover(directory, self.test_file_glob_pattern, top_level_dir)
 | |
|             self.test_suite = tests
 | |
|         except:
 | |
|             exc_type, exc_value, exc_tb = sys.exc_info()
 | |
|             traceback.print_exception(*sys.exc_info())
 | |
|             self.errorDialog("Unable to run test '%s'" % directory,
 | |
|                              "Error loading specified test: %s, %s" % (exc_type, exc_value))
 | |
|             return
 | |
|         self.notifyTestsDiscovered(self.test_suite)
 | |
| 
 | |
|     # Required callbacks
 | |
| 
 | |
|     def notifyTestsDiscovered(self, test_suite):
 | |
|         "Override to display information about the suite of discovered tests"
 | |
|         pass
 | |
| 
 | |
|     def notifyRunning(self):
 | |
|         "Override to set GUI in 'running' mode, enabling 'stop' button etc."
 | |
|         pass
 | |
| 
 | |
|     def notifyStopped(self):
 | |
|         "Override to set GUI in 'stopped' mode, enabling 'run' button etc."
 | |
|         pass
 | |
| 
 | |
|     def notifyTestFailed(self, test, err):
 | |
|         "Override to indicate that a test has just failed"
 | |
|         pass
 | |
| 
 | |
|     def notifyTestErrored(self, test, err):
 | |
|         "Override to indicate that a test has just errored"
 | |
|         pass
 | |
| 
 | |
|     def notifyTestSkipped(self, test, reason):
 | |
|         "Override to indicate that test was skipped"
 | |
|         pass
 | |
| 
 | |
|     def notifyTestFailedExpectedly(self, test, err):
 | |
|         "Override to indicate that test has just failed expectedly"
 | |
|         pass
 | |
| 
 | |
|     def notifyTestStarted(self, test):
 | |
|         "Override to indicate that a test is about to run"
 | |
|         pass
 | |
| 
 | |
|     def notifyTestFinished(self, test):
 | |
|         """Override to indicate that a test has finished (it may already have
 | |
|            failed or errored)"""
 | |
|         pass
 | |
| 
 | |
| 
 | |
| class GUITestResult(unittest.TestResult):
 | |
|     """A TestResult that makes callbacks to its associated GUI TestRunner.
 | |
|     Used by BaseGUITestRunner. Need not be created directly.
 | |
|     """
 | |
|     def __init__(self, callback):
 | |
|         unittest.TestResult.__init__(self)
 | |
|         self.callback = callback
 | |
| 
 | |
|     def addError(self, test, err):
 | |
|         unittest.TestResult.addError(self, test, err)
 | |
|         self.callback.notifyTestErrored(test, err)
 | |
| 
 | |
|     def addFailure(self, test, err):
 | |
|         unittest.TestResult.addFailure(self, test, err)
 | |
|         self.callback.notifyTestFailed(test, err)
 | |
| 
 | |
|     def addSkip(self, test, reason):
 | |
|         super(GUITestResult,self).addSkip(test, reason)
 | |
|         self.callback.notifyTestSkipped(test, reason)
 | |
| 
 | |
|     def addExpectedFailure(self, test, err):
 | |
|         super(GUITestResult,self).addExpectedFailure(test, err)
 | |
|         self.callback.notifyTestFailedExpectedly(test, err)
 | |
| 
 | |
|     def stopTest(self, test):
 | |
|         unittest.TestResult.stopTest(self, test)
 | |
|         self.callback.notifyTestFinished(test)
 | |
| 
 | |
|     def startTest(self, test):
 | |
|         unittest.TestResult.startTest(self, test)
 | |
|         self.callback.notifyTestStarted(test)
 | |
| 
 | |
| 
 | |
| class RollbackImporter:
 | |
|     """This tricky little class is used to make sure that modules under test
 | |
|     will be reloaded the next time they are imported.
 | |
|     """
 | |
|     def __init__(self):
 | |
|         self.previousModules = sys.modules.copy()
 | |
| 
 | |
|     def rollbackImports(self):
 | |
|         for modname in sys.modules.copy().keys():
 | |
|             if not modname in self.previousModules:
 | |
|                 # Force reload when modname next imported
 | |
|                 del(sys.modules[modname])
 | |
| 
 | |
| 
 | |
| ##############################################################################
 | |
| # Tkinter GUI
 | |
| ##############################################################################
 | |
| 
 | |
| class DiscoverSettingsDialog(simpledialog.Dialog):
 | |
|     """
 | |
|     Dialog box for prompting test discovery settings
 | |
|     """
 | |
| 
 | |
|     def __init__(self, master, top_level_dir, test_file_glob_pattern, *args, **kwargs):
 | |
|         self.top_level_dir = top_level_dir
 | |
|         self.dirVar = tk.StringVar()
 | |
|         self.dirVar.set(top_level_dir)
 | |
| 
 | |
|         self.test_file_glob_pattern = test_file_glob_pattern
 | |
|         self.testPatternVar = tk.StringVar()
 | |
|         self.testPatternVar.set(test_file_glob_pattern)
 | |
| 
 | |
|         simpledialog.Dialog.__init__(self, master, title="Discover Settings",
 | |
|                                      *args, **kwargs)
 | |
| 
 | |
|     def body(self, master):
 | |
|         tk.Label(master, text="Top Level Directory").grid(row=0)
 | |
|         self.e1 = tk.Entry(master, textvariable=self.dirVar)
 | |
|         self.e1.grid(row = 0, column=1)
 | |
|         tk.Button(master, text="...",
 | |
|                   command=lambda: self.selectDirClicked(master)).grid(row=0,column=3)
 | |
| 
 | |
|         tk.Label(master, text="Test File Pattern").grid(row=1)
 | |
|         self.e2 = tk.Entry(master, textvariable = self.testPatternVar)
 | |
|         self.e2.grid(row = 1, column=1)
 | |
|         return None
 | |
| 
 | |
|     def selectDirClicked(self, master):
 | |
|         dir_path = filedialog.askdirectory(parent=master)
 | |
|         if dir_path:
 | |
|             self.dirVar.set(dir_path)
 | |
| 
 | |
|     def apply(self):
 | |
|         self.top_level_dir = self.dirVar.get()
 | |
|         self.test_file_glob_pattern = self.testPatternVar.get()
 | |
| 
 | |
| class TkTestRunner(BaseGUITestRunner):
 | |
|     """An implementation of BaseGUITestRunner using Tkinter.
 | |
|     """
 | |
|     def initGUI(self, root, initialTestName):
 | |
|         """Set up the GUI inside the given root window. The test name entry
 | |
|         field will be pre-filled with the given initialTestName.
 | |
|         """
 | |
|         self.root = root
 | |
| 
 | |
|         self.statusVar = tk.StringVar()
 | |
|         self.statusVar.set("Idle")
 | |
| 
 | |
|         #tk vars for tracking counts of test result types
 | |
|         self.runCountVar = tk.IntVar()
 | |
|         self.failCountVar = tk.IntVar()
 | |
|         self.errorCountVar = tk.IntVar()
 | |
|         self.skipCountVar = tk.IntVar()
 | |
|         self.expectFailCountVar = tk.IntVar()
 | |
|         self.remainingCountVar = tk.IntVar()
 | |
| 
 | |
|         self.top = tk.Frame()
 | |
|         self.top.pack(fill=tk.BOTH, expand=1)
 | |
|         self.createWidgets()
 | |
| 
 | |
|     def getDirectoryToDiscover(self):
 | |
|         return filedialog.askdirectory()
 | |
| 
 | |
|     def settingsClicked(self):
 | |
|         d = DiscoverSettingsDialog(self.top, self.top_level_dir, self.test_file_glob_pattern)
 | |
|         self.top_level_dir = d.top_level_dir
 | |
|         self.test_file_glob_pattern = d.test_file_glob_pattern
 | |
| 
 | |
|     def notifyTestsDiscovered(self, test_suite):
 | |
|         discovered = test_suite.countTestCases()
 | |
|         self.runCountVar.set(0)
 | |
|         self.failCountVar.set(0)
 | |
|         self.errorCountVar.set(0)
 | |
|         self.remainingCountVar.set(discovered)
 | |
|         self.progressBar.setProgressFraction(0.0)
 | |
|         self.errorListbox.delete(0, tk.END)
 | |
|         self.statusVar.set("Discovering tests from %s. Found: %s" %
 | |
|             (self.directory_to_read, discovered))
 | |
|         self.stopGoButton['state'] = tk.NORMAL
 | |
| 
 | |
|     def createWidgets(self):
 | |
|         """Creates and packs the various widgets.
 | |
| 
 | |
|         Why is it that GUI code always ends up looking a mess, despite all the
 | |
|         best intentions to keep it tidy? Answers on a postcard, please.
 | |
|         """
 | |
|         # Status bar
 | |
|         statusFrame = tk.Frame(self.top, relief=tk.SUNKEN, borderwidth=2)
 | |
|         statusFrame.pack(anchor=tk.SW, fill=tk.X, side=tk.BOTTOM)
 | |
|         tk.Label(statusFrame, width=1, textvariable=self.statusVar).pack(side=tk.TOP, fill=tk.X)
 | |
| 
 | |
|         # Area to enter name of test to run
 | |
|         leftFrame = tk.Frame(self.top, borderwidth=3)
 | |
|         leftFrame.pack(fill=tk.BOTH, side=tk.LEFT, anchor=tk.NW, expand=1)
 | |
|         suiteNameFrame = tk.Frame(leftFrame, borderwidth=3)
 | |
|         suiteNameFrame.pack(fill=tk.X)
 | |
| 
 | |
|         # Progress bar
 | |
|         progressFrame = tk.Frame(leftFrame, relief=tk.GROOVE, borderwidth=2)
 | |
|         progressFrame.pack(fill=tk.X, expand=0, anchor=tk.NW)
 | |
|         tk.Label(progressFrame, text="Progress:").pack(anchor=tk.W)
 | |
|         self.progressBar = ProgressBar(progressFrame, relief=tk.SUNKEN,
 | |
|                                        borderwidth=2)
 | |
|         self.progressBar.pack(fill=tk.X, expand=1)
 | |
| 
 | |
| 
 | |
|         # Area with buttons to start/stop tests and quit
 | |
|         buttonFrame = tk.Frame(self.top, borderwidth=3)
 | |
|         buttonFrame.pack(side=tk.LEFT, anchor=tk.NW, fill=tk.Y)
 | |
| 
 | |
|         tk.Button(buttonFrame, text="Discover Tests",
 | |
|                   command=self.discoverClicked).pack(fill=tk.X)
 | |
| 
 | |
| 
 | |
|         self.stopGoButton = tk.Button(buttonFrame, text="Start",
 | |
|                                       command=self.runClicked, state=tk.DISABLED)
 | |
|         self.stopGoButton.pack(fill=tk.X)
 | |
| 
 | |
|         tk.Button(buttonFrame, text="Close",
 | |
|                   command=self.top.quit).pack(side=tk.BOTTOM, fill=tk.X)
 | |
|         tk.Button(buttonFrame, text="Settings",
 | |
|                   command=self.settingsClicked).pack(side=tk.BOTTOM, fill=tk.X)
 | |
| 
 | |
|         # Area with labels reporting results
 | |
|         for label, var in (('Run:', self.runCountVar),
 | |
|                            ('Failures:', self.failCountVar),
 | |
|                            ('Errors:', self.errorCountVar),
 | |
|                            ('Skipped:', self.skipCountVar),
 | |
|                            ('Expected Failures:', self.expectFailCountVar),
 | |
|                            ('Remaining:', self.remainingCountVar),
 | |
|                            ):
 | |
|             tk.Label(progressFrame, text=label).pack(side=tk.LEFT)
 | |
|             tk.Label(progressFrame, textvariable=var,
 | |
|                      foreground="blue").pack(side=tk.LEFT, fill=tk.X,
 | |
|                                              expand=1, anchor=tk.W)
 | |
| 
 | |
|         # List box showing errors and failures
 | |
|         tk.Label(leftFrame, text="Failures and errors:").pack(anchor=tk.W)
 | |
|         listFrame = tk.Frame(leftFrame, relief=tk.SUNKEN, borderwidth=2)
 | |
|         listFrame.pack(fill=tk.BOTH, anchor=tk.NW, expand=1)
 | |
|         self.errorListbox = tk.Listbox(listFrame, foreground='red',
 | |
|                                        selectmode=tk.SINGLE,
 | |
|                                        selectborderwidth=0)
 | |
|         self.errorListbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=1,
 | |
|                                anchor=tk.NW)
 | |
|         listScroll = tk.Scrollbar(listFrame, command=self.errorListbox.yview)
 | |
|         listScroll.pack(side=tk.LEFT, fill=tk.Y, anchor=tk.N)
 | |
|         self.errorListbox.bind("<Double-1>",
 | |
|                                lambda e, self=self: self.showSelectedError())
 | |
|         self.errorListbox.configure(yscrollcommand=listScroll.set)
 | |
| 
 | |
|     def errorDialog(self, title, message):
 | |
|         messagebox.showerror(parent=self.root, title=title,
 | |
|                              message=message)
 | |
| 
 | |
|     def notifyRunning(self):
 | |
|         self.runCountVar.set(0)
 | |
|         self.failCountVar.set(0)
 | |
|         self.errorCountVar.set(0)
 | |
|         self.remainingCountVar.set(self.totalTests)
 | |
|         self.errorInfo = []
 | |
|         while self.errorListbox.size():
 | |
|             self.errorListbox.delete(0)
 | |
|         #Stopping seems not to work, so simply disable the start button
 | |
|         #self.stopGoButton.config(command=self.stopClicked, text="Stop")
 | |
|         self.stopGoButton.config(state=tk.DISABLED)
 | |
|         self.progressBar.setProgressFraction(0.0)
 | |
|         self.top.update_idletasks()
 | |
| 
 | |
|     def notifyStopped(self):
 | |
|         self.stopGoButton.config(state=tk.DISABLED)
 | |
|         #self.stopGoButton.config(command=self.runClicked, text="Start")
 | |
|         self.statusVar.set("Idle")
 | |
| 
 | |
|     def notifyTestStarted(self, test):
 | |
|         self.statusVar.set(str(test))
 | |
|         self.top.update_idletasks()
 | |
| 
 | |
|     def notifyTestFailed(self, test, err):
 | |
|         self.failCountVar.set(1 + self.failCountVar.get())
 | |
|         self.errorListbox.insert(tk.END, "Failure: %s" % test)
 | |
|         self.errorInfo.append((test,err))
 | |
| 
 | |
|     def notifyTestErrored(self, test, err):
 | |
|         self.errorCountVar.set(1 + self.errorCountVar.get())
 | |
|         self.errorListbox.insert(tk.END, "Error: %s" % test)
 | |
|         self.errorInfo.append((test,err))
 | |
| 
 | |
|     def notifyTestSkipped(self, test, reason):
 | |
|         super(TkTestRunner, self).notifyTestSkipped(test, reason)
 | |
|         self.skipCountVar.set(1 + self.skipCountVar.get())
 | |
| 
 | |
|     def notifyTestFailedExpectedly(self, test, err):
 | |
|         super(TkTestRunner, self).notifyTestFailedExpectedly(test, err)
 | |
|         self.expectFailCountVar.set(1 + self.expectFailCountVar.get())
 | |
| 
 | |
| 
 | |
|     def notifyTestFinished(self, test):
 | |
|         self.remainingCountVar.set(self.remainingCountVar.get() - 1)
 | |
|         self.runCountVar.set(1 + self.runCountVar.get())
 | |
|         fractionDone = float(self.runCountVar.get())/float(self.totalTests)
 | |
|         fillColor = len(self.errorInfo) and "red" or "green"
 | |
|         self.progressBar.setProgressFraction(fractionDone, fillColor)
 | |
| 
 | |
|     def showSelectedError(self):
 | |
|         selection = self.errorListbox.curselection()
 | |
|         if not selection: return
 | |
|         selected = int(selection[0])
 | |
|         txt = self.errorListbox.get(selected)
 | |
|         window = tk.Toplevel(self.root)
 | |
|         window.title(txt)
 | |
|         window.protocol('WM_DELETE_WINDOW', window.quit)
 | |
|         test, error = self.errorInfo[selected]
 | |
|         tk.Label(window, text=str(test),
 | |
|                  foreground="red", justify=tk.LEFT).pack(anchor=tk.W)
 | |
|         tracebackLines =  traceback.format_exception(*error)
 | |
|         tracebackText = "".join(tracebackLines)
 | |
|         tk.Label(window, text=tracebackText, justify=tk.LEFT).pack()
 | |
|         tk.Button(window, text="Close",
 | |
|                   command=window.quit).pack(side=tk.BOTTOM)
 | |
|         window.bind('<Key-Return>', lambda e, w=window: w.quit())
 | |
|         window.mainloop()
 | |
|         window.destroy()
 | |
| 
 | |
| 
 | |
| class ProgressBar(tk.Frame):
 | |
|     """A simple progress bar that shows a percentage progress in
 | |
|     the given colour."""
 | |
| 
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         tk.Frame.__init__(self, *args, **kwargs)
 | |
|         self.canvas = tk.Canvas(self, height='20', width='60',
 | |
|                                 background='white', borderwidth=3)
 | |
|         self.canvas.pack(fill=tk.X, expand=1)
 | |
|         self.rect = self.text = None
 | |
|         self.canvas.bind('<Configure>', self.paint)
 | |
|         self.setProgressFraction(0.0)
 | |
| 
 | |
|     def setProgressFraction(self, fraction, color='blue'):
 | |
|         self.fraction = fraction
 | |
|         self.color = color
 | |
|         self.paint()
 | |
|         self.canvas.update_idletasks()
 | |
| 
 | |
|     def paint(self, *args):
 | |
|         totalWidth = self.canvas.winfo_width()
 | |
|         width = int(self.fraction * float(totalWidth))
 | |
|         height = self.canvas.winfo_height()
 | |
|         if self.rect is not None: self.canvas.delete(self.rect)
 | |
|         if self.text is not None: self.canvas.delete(self.text)
 | |
|         self.rect = self.canvas.create_rectangle(0, 0, width, height,
 | |
|                                                  fill=self.color)
 | |
|         percentString = "%3.0f%%" % (100.0 * self.fraction)
 | |
|         self.text = self.canvas.create_text(totalWidth/2, height/2,
 | |
|                                             anchor=tk.CENTER,
 | |
|                                             text=percentString)
 | |
| 
 | |
| def main(initialTestName=""):
 | |
|     root = tk.Tk()
 | |
|     root.title("PyUnit")
 | |
|     runner = TkTestRunner(root, initialTestName)
 | |
|     root.protocol('WM_DELETE_WINDOW', root.quit)
 | |
|     root.mainloop()
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     if len(sys.argv) == 2:
 | |
|         main(sys.argv[1])
 | |
|     else:
 | |
|         main()
 | 
