usrbinenv 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 c 1999 2000 2001 steve purcell this module is free software and you may redistribute it andor modify it under the same terms as python itself so long as this message and disclaimer are retained in their original form in no event shall the be liable to any party for direct indirect special incidental or consequential damages arising out of the use of this code even if the has been advised of the possibility of such damage the 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 gui framework classes 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 test discovery variables 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 required callbacks override to indicate that a test has finished it may already have failed or errored pass class guitestresultunittest testresult def initself callback unittest testresult initself self callback callback def adderrorself test err unittest testresult adderrorself test err self callback notifytesterroredtest err def addfailureself test err unittest testresult addfailureself test err self callback notifytestfailedtest err def addskipself test reason superguitestresult self addskiptest reason self callback notifytestskippedtest reason def addexpectedfailureself test err superguitestresult self addexpectedfailuretest err self callback notifytestfailedexpectedlytest err def stoptestself test unittest testresult stoptestself test self callback notifytestfinishedtest def starttestself test unittest testresult starttestself test self callback notifyteststartedtest class rollbackimporter def initself self previousmodules sys modules copy def rollbackimportsself for modname in sys modules copy keys if not modname in self previousmodules force reload when modname next imported delsys modulesmodname tkinter gui class discoversettingsdialogsimpledialog dialog def initself master topleveldir testfileglobpattern args kwargs self topleveldir topleveldir self dirvar tk stringvar self dirvar settopleveldir self testfileglobpattern testfileglobpattern self testpatternvar tk stringvar self testpatternvar settestfileglobpattern simpledialog dialog initself master titlediscover settings args kwargs def bodyself master tk labelmaster texttop level directory gridrow0 self e1 tk entrymaster textvariableself dirvar self e1 gridrow 0 column1 tk buttonmaster text commandlambda self selectdirclickedmaster gridrow0 column3 tk labelmaster texttest file pattern gridrow1 self e2 tk entrymaster textvariable self testpatternvar self e2 gridrow 1 column1 return none def selectdirclickedself master dirpath filedialog askdirectoryparentmaster if dirpath self dirvar setdirpath def applyself self topleveldir self dirvar get self testfileglobpattern self testpatternvar get class tktestrunnerbaseguitestrunner def initguiself root initialtestname self root root self statusvar tk stringvar self statusvar setidle 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 packfilltk both expand1 self createwidgets def getdirectorytodiscoverself return filedialog askdirectory def settingsclickedself d discoversettingsdialogself top self topleveldir self testfileglobpattern self topleveldir d topleveldir self testfileglobpattern d testfileglobpattern def notifytestsdiscoveredself testsuite discovered testsuite counttestcases self runcountvar set0 self failcountvar set0 self errorcountvar set0 self remainingcountvar setdiscovered self progressbar setprogressfraction0 0 self errorlistbox delete0 tk end self statusvar setdiscovering tests from s found s self directorytoread discovered self stopgobutton state tk normal def createwidgetsself status bar statusframe tk frameself top relieftk sunken borderwidth2 statusframe packanchortk sw filltk x sidetk bottom tk labelstatusframe width1 textvariableself statusvar packsidetk top filltk x area to enter name of test to run leftframe tk frameself top borderwidth3 leftframe packfilltk both sidetk left anchortk nw expand1 suitenameframe tk frameleftframe borderwidth3 suitenameframe packfilltk x progress bar progressframe tk frameleftframe relieftk groove borderwidth2 progressframe packfilltk x expand0 anchortk nw tk labelprogressframe textprogress packanchortk w self progressbar progressbarprogressframe relieftk sunken borderwidth2 self progressbar packfilltk x expand1 area with buttons to startstop tests and quit buttonframe tk frameself top borderwidth3 buttonframe packsidetk left anchortk nw filltk y tk buttonbuttonframe textdiscover tests commandself discoverclicked packfilltk x self stopgobutton tk buttonbuttonframe textstart commandself runclicked statetk disabled self stopgobutton packfilltk x tk buttonbuttonframe textclose commandself top quit packsidetk bottom filltk x tk buttonbuttonframe textsettings commandself settingsclicked packsidetk bottom filltk 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 labelprogressframe textlabel packsidetk left tk labelprogressframe textvariablevar foregroundblue packsidetk left filltk x expand1 anchortk w list box showing errors and failures tk labelleftframe textfailures and errors packanchortk w listframe tk frameleftframe relieftk sunken borderwidth2 listframe packfilltk both anchortk nw expand1 self errorlistbox tk listboxlistframe foreground red selectmodetk single selectborderwidth0 self errorlistbox packsidetk left filltk both expand1 anchortk nw listscroll tk scrollbarlistframe commandself errorlistbox yview listscroll packsidetk left filltk y anchortk n self errorlistbox binddouble1 lambda e selfself self showselectederror self errorlistbox configureyscrollcommandlistscroll set def errordialogself title message messagebox showerrorparentself root titletitle messagemessage def notifyrunningself self runcountvar set0 self failcountvar set0 self errorcountvar set0 self remainingcountvar setself totaltests self errorinfo while self errorlistbox size self errorlistbox delete0 stopping seems not to work so simply disable the start button self stopgobutton configcommandself stopclicked textstop self stopgobutton configstatetk disabled self progressbar setprogressfraction0 0 self top updateidletasks def notifystoppedself self stopgobutton configstatetk disabled self stopgobutton configcommandself runclicked textstart self statusvar setidle def notifyteststartedself test self statusvar setstrtest self top updateidletasks def notifytestfailedself test err self failcountvar set1 self failcountvar get self errorlistbox inserttk end failure s test self errorinfo appendtest err def notifytesterroredself test err self errorcountvar set1 self errorcountvar get self errorlistbox inserttk end error s test self errorinfo appendtest err def notifytestskippedself test reason supertktestrunner self notifytestskippedtest reason self skipcountvar set1 self skipcountvar get def notifytestfailedexpectedlyself test err supertktestrunner self notifytestfailedexpectedlytest err self expectfailcountvar set1 self expectfailcountvar get def notifytestfinishedself test self remainingcountvar setself remainingcountvar get 1 self runcountvar set1 self runcountvar get fractiondone floatself runcountvar getfloatself totaltests fillcolor lenself errorinfo and red or green self progressbar setprogressfractionfractiondone fillcolor def showselectederrorself selection self errorlistbox curselection if not selection return selected intselection0 txt self errorlistbox getselected window tk toplevelself root window titletxt window protocol wmdeletewindow window quit test error self errorinfoselected tk labelwindow textstrtest foregroundred justifytk left packanchortk w tracebacklines traceback formatexceptionerror tracebacktext jointracebacklines tk labelwindow texttracebacktext justifytk left pack tk buttonwindow textclose commandwindow quit packsidetk bottom window bind keyreturn lambda e wwindow w quit window mainloop window destroy class progressbartk frame 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 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 message and disclaimer are retained in their original form in no event shall the be liable to any party for direct indirect special incidental or consequential damages arising out of the use of this code even if the has been advised of the possibility of such damage the 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 gui framework classes 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 test discovery variables 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 required callbacks override to indicate that a test has finished it may already have failed or errored a testresult that makes callbacks to its associated gui testrunner used by baseguitestrunner need not be created directly this tricky little class is used to make sure that modules under test will be reloaded the next time they are imported force reload when modname next imported tkinter gui dialog box for prompting test discovery settings an implementation of baseguitestrunner using tkinter set up the gui inside the given root window the test name entry field will be pre filled with the given initialtestname tk vars for tracking counts of test result types 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 area to enter name of test to run progress bar area with buttons to start stop tests and quit area with labels reporting results list box showing errors and failures stopping seems not to work so simply disable the start button self stopgobutton config command self stopclicked text stop self stopgobutton config command self runclicked text start a simple progress bar that shows a percentage progress in the given colour
__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








class BaseGUITestRunner(object):
    
    def __init__(self, *args, **kwargs):
        self.currentResult = None
        self.running = 0
        self.__rollbackImporter = RollbackImporter()
        self.test_suite = None

        
        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:
            
            
            
            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)

    

    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):
        
        pass


class GUITestResult(unittest.TestResult):
    
    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:
    
    def __init__(self):
        self.previousModules = sys.modules.copy()

    def rollbackImports(self):
        for modname in sys.modules.copy().keys():
            if not modname in self.previousModules:
                
                del(sys.modules[modname])






class DiscoverSettingsDialog(simpledialog.Dialog):
    

    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):
    
    def initGUI(self, root, initialTestName):
        
        self.root = root

        self.statusVar = tk.StringVar()
        self.statusVar.set("Idle")

        
        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):
        
        
        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)

        
        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)

        
        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)


        
        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)

        
        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)

        
        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)
        
        
        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.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):
    

    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()