(root)/
Python-3.11.7/
Tools/
unittestgui/
unittestgui.py
       1  #!/usr/bin/env python3
       2  """
       3  GUI framework and application for use with Python unit testing framework.
       4  Execute tests written using the framework provided by the 'unittest' module.
       5  
       6  Updated for unittest test discovery by Mark Roddy and Python 3
       7  support by Brian Curtin.
       8  
       9  Based on the original by Steve Purcell, from:
      10  
      11    http://pyunit.sourceforge.net/
      12  
      13  Copyright (c) 1999, 2000, 2001 Steve Purcell
      14  This module is free software, and you may redistribute it and/or modify
      15  it under the same terms as Python itself, so long as this copyright message
      16  and disclaimer are retained in their original form.
      17  
      18  IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
      19  SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
      20  THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
      21  DAMAGE.
      22  
      23  THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
      24  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
      25  PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
      26  AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
      27  SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
      28  """
      29  
      30  __author__ = "Steve Purcell (stephen_purcell@yahoo.com)"
      31  
      32  import sys
      33  import traceback
      34  import unittest
      35  
      36  import tkinter as tk
      37  from tkinter import messagebox
      38  from tkinter import filedialog
      39  from tkinter import simpledialog
      40  
      41  
      42  
      43  
      44  ##############################################################################
      45  # GUI framework classes
      46  ##############################################################################
      47  
      48  class ESC[4;38;5;81mBaseGUITestRunner(ESC[4;38;5;149mobject):
      49      """Subclass this class to create a GUI TestRunner that uses a specific
      50      windowing toolkit. The class takes care of running tests in the correct
      51      manner, and making callbacks to the derived class to obtain information
      52      or signal that events have occurred.
      53      """
      54      def __init__(self, *args, **kwargs):
      55          self.currentResult = None
      56          self.running = 0
      57          self.__rollbackImporter = RollbackImporter()
      58          self.test_suite = None
      59  
      60          #test discovery variables
      61          self.directory_to_read = ''
      62          self.top_level_dir = ''
      63          self.test_file_glob_pattern = 'test*.py'
      64  
      65          self.initGUI(*args, **kwargs)
      66  
      67      def errorDialog(self, title, message):
      68          "Override to display an error arising from GUI usage"
      69          pass
      70  
      71      def getDirectoryToDiscover(self):
      72          "Override to prompt user for directory to perform test discovery"
      73          pass
      74  
      75      def runClicked(self):
      76          "To be called in response to user choosing to run a test"
      77          if self.running: return
      78          if not self.test_suite:
      79              self.errorDialog("Test Discovery", "You discover some tests first!")
      80              return
      81          self.currentResult = GUITestResult(self)
      82          self.totalTests = self.test_suite.countTestCases()
      83          self.running = 1
      84          self.notifyRunning()
      85          self.test_suite.run(self.currentResult)
      86          self.running = 0
      87          self.notifyStopped()
      88  
      89      def stopClicked(self):
      90          "To be called in response to user stopping the running of a test"
      91          if self.currentResult:
      92              self.currentResult.stop()
      93  
      94      def discoverClicked(self):
      95          self.__rollbackImporter.rollbackImports()
      96          directory = self.getDirectoryToDiscover()
      97          if not directory:
      98              return
      99          self.directory_to_read = directory
     100          try:
     101              # Explicitly use 'None' value if no top level directory is
     102              # specified (indicated by empty string) as discover() explicitly
     103              # checks for a 'None' to determine if no tld has been specified
     104              top_level_dir = self.top_level_dir or None
     105              tests = unittest.defaultTestLoader.discover(directory, self.test_file_glob_pattern, top_level_dir)
     106              self.test_suite = tests
     107          except:
     108              exc_type, exc_value, exc_tb = sys.exc_info()
     109              traceback.print_exception(*sys.exc_info())
     110              self.errorDialog("Unable to run test '%s'" % directory,
     111                               "Error loading specified test: %s, %s" % (exc_type, exc_value))
     112              return
     113          self.notifyTestsDiscovered(self.test_suite)
     114  
     115      # Required callbacks
     116  
     117      def notifyTestsDiscovered(self, test_suite):
     118          "Override to display information about the suite of discovered tests"
     119          pass
     120  
     121      def notifyRunning(self):
     122          "Override to set GUI in 'running' mode, enabling 'stop' button etc."
     123          pass
     124  
     125      def notifyStopped(self):
     126          "Override to set GUI in 'stopped' mode, enabling 'run' button etc."
     127          pass
     128  
     129      def notifyTestFailed(self, test, err):
     130          "Override to indicate that a test has just failed"
     131          pass
     132  
     133      def notifyTestErrored(self, test, err):
     134          "Override to indicate that a test has just errored"
     135          pass
     136  
     137      def notifyTestSkipped(self, test, reason):
     138          "Override to indicate that test was skipped"
     139          pass
     140  
     141      def notifyTestFailedExpectedly(self, test, err):
     142          "Override to indicate that test has just failed expectedly"
     143          pass
     144  
     145      def notifyTestStarted(self, test):
     146          "Override to indicate that a test is about to run"
     147          pass
     148  
     149      def notifyTestFinished(self, test):
     150          """Override to indicate that a test has finished (it may already have
     151             failed or errored)"""
     152          pass
     153  
     154  
     155  class ESC[4;38;5;81mGUITestResult(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestResult):
     156      """A TestResult that makes callbacks to its associated GUI TestRunner.
     157      Used by BaseGUITestRunner. Need not be created directly.
     158      """
     159      def __init__(self, callback):
     160          unittest.TestResult.__init__(self)
     161          self.callback = callback
     162  
     163      def addError(self, test, err):
     164          unittest.TestResult.addError(self, test, err)
     165          self.callback.notifyTestErrored(test, err)
     166  
     167      def addFailure(self, test, err):
     168          unittest.TestResult.addFailure(self, test, err)
     169          self.callback.notifyTestFailed(test, err)
     170  
     171      def addSkip(self, test, reason):
     172          super(GUITestResult,self).addSkip(test, reason)
     173          self.callback.notifyTestSkipped(test, reason)
     174  
     175      def addExpectedFailure(self, test, err):
     176          super(GUITestResult,self).addExpectedFailure(test, err)
     177          self.callback.notifyTestFailedExpectedly(test, err)
     178  
     179      def stopTest(self, test):
     180          unittest.TestResult.stopTest(self, test)
     181          self.callback.notifyTestFinished(test)
     182  
     183      def startTest(self, test):
     184          unittest.TestResult.startTest(self, test)
     185          self.callback.notifyTestStarted(test)
     186  
     187  
     188  class ESC[4;38;5;81mRollbackImporter:
     189      """This tricky little class is used to make sure that modules under test
     190      will be reloaded the next time they are imported.
     191      """
     192      def __init__(self):
     193          self.previousModules = sys.modules.copy()
     194  
     195      def rollbackImports(self):
     196          for modname in sys.modules.copy().keys():
     197              if not modname in self.previousModules:
     198                  # Force reload when modname next imported
     199                  del(sys.modules[modname])
     200  
     201  
     202  ##############################################################################
     203  # Tkinter GUI
     204  ##############################################################################
     205  
     206  class ESC[4;38;5;81mDiscoverSettingsDialog(ESC[4;38;5;149msimpledialogESC[4;38;5;149m.ESC[4;38;5;149mDialog):
     207      """
     208      Dialog box for prompting test discovery settings
     209      """
     210  
     211      def __init__(self, master, top_level_dir, test_file_glob_pattern, *args, **kwargs):
     212          self.top_level_dir = top_level_dir
     213          self.dirVar = tk.StringVar()
     214          self.dirVar.set(top_level_dir)
     215  
     216          self.test_file_glob_pattern = test_file_glob_pattern
     217          self.testPatternVar = tk.StringVar()
     218          self.testPatternVar.set(test_file_glob_pattern)
     219  
     220          simpledialog.Dialog.__init__(self, master, title="Discover Settings",
     221                                       *args, **kwargs)
     222  
     223      def body(self, master):
     224          tk.Label(master, text="Top Level Directory").grid(row=0)
     225          self.e1 = tk.Entry(master, textvariable=self.dirVar)
     226          self.e1.grid(row = 0, column=1)
     227          tk.Button(master, text="...",
     228                    command=lambda: self.selectDirClicked(master)).grid(row=0,column=3)
     229  
     230          tk.Label(master, text="Test File Pattern").grid(row=1)
     231          self.e2 = tk.Entry(master, textvariable = self.testPatternVar)
     232          self.e2.grid(row = 1, column=1)
     233          return None
     234  
     235      def selectDirClicked(self, master):
     236          dir_path = filedialog.askdirectory(parent=master)
     237          if dir_path:
     238              self.dirVar.set(dir_path)
     239  
     240      def apply(self):
     241          self.top_level_dir = self.dirVar.get()
     242          self.test_file_glob_pattern = self.testPatternVar.get()
     243  
     244  class ESC[4;38;5;81mTkTestRunner(ESC[4;38;5;149mBaseGUITestRunner):
     245      """An implementation of BaseGUITestRunner using Tkinter.
     246      """
     247      def initGUI(self, root, initialTestName):
     248          """Set up the GUI inside the given root window. The test name entry
     249          field will be pre-filled with the given initialTestName.
     250          """
     251          self.root = root
     252  
     253          self.statusVar = tk.StringVar()
     254          self.statusVar.set("Idle")
     255  
     256          #tk vars for tracking counts of test result types
     257          self.runCountVar = tk.IntVar()
     258          self.failCountVar = tk.IntVar()
     259          self.errorCountVar = tk.IntVar()
     260          self.skipCountVar = tk.IntVar()
     261          self.expectFailCountVar = tk.IntVar()
     262          self.remainingCountVar = tk.IntVar()
     263  
     264          self.top = tk.Frame()
     265          self.top.pack(fill=tk.BOTH, expand=1)
     266          self.createWidgets()
     267  
     268      def getDirectoryToDiscover(self):
     269          return filedialog.askdirectory()
     270  
     271      def settingsClicked(self):
     272          d = DiscoverSettingsDialog(self.top, self.top_level_dir, self.test_file_glob_pattern)
     273          self.top_level_dir = d.top_level_dir
     274          self.test_file_glob_pattern = d.test_file_glob_pattern
     275  
     276      def notifyTestsDiscovered(self, test_suite):
     277          discovered = test_suite.countTestCases()
     278          self.runCountVar.set(0)
     279          self.failCountVar.set(0)
     280          self.errorCountVar.set(0)
     281          self.remainingCountVar.set(discovered)
     282          self.progressBar.setProgressFraction(0.0)
     283          self.errorListbox.delete(0, tk.END)
     284          self.statusVar.set("Discovering tests from %s. Found: %s" %
     285              (self.directory_to_read, discovered))
     286          self.stopGoButton['state'] = tk.NORMAL
     287  
     288      def createWidgets(self):
     289          """Creates and packs the various widgets.
     290  
     291          Why is it that GUI code always ends up looking a mess, despite all the
     292          best intentions to keep it tidy? Answers on a postcard, please.
     293          """
     294          # Status bar
     295          statusFrame = tk.Frame(self.top, relief=tk.SUNKEN, borderwidth=2)
     296          statusFrame.pack(anchor=tk.SW, fill=tk.X, side=tk.BOTTOM)
     297          tk.Label(statusFrame, width=1, textvariable=self.statusVar).pack(side=tk.TOP, fill=tk.X)
     298  
     299          # Area to enter name of test to run
     300          leftFrame = tk.Frame(self.top, borderwidth=3)
     301          leftFrame.pack(fill=tk.BOTH, side=tk.LEFT, anchor=tk.NW, expand=1)
     302          suiteNameFrame = tk.Frame(leftFrame, borderwidth=3)
     303          suiteNameFrame.pack(fill=tk.X)
     304  
     305          # Progress bar
     306          progressFrame = tk.Frame(leftFrame, relief=tk.GROOVE, borderwidth=2)
     307          progressFrame.pack(fill=tk.X, expand=0, anchor=tk.NW)
     308          tk.Label(progressFrame, text="Progress:").pack(anchor=tk.W)
     309          self.progressBar = ProgressBar(progressFrame, relief=tk.SUNKEN,
     310                                         borderwidth=2)
     311          self.progressBar.pack(fill=tk.X, expand=1)
     312  
     313  
     314          # Area with buttons to start/stop tests and quit
     315          buttonFrame = tk.Frame(self.top, borderwidth=3)
     316          buttonFrame.pack(side=tk.LEFT, anchor=tk.NW, fill=tk.Y)
     317  
     318          tk.Button(buttonFrame, text="Discover Tests",
     319                    command=self.discoverClicked).pack(fill=tk.X)
     320  
     321  
     322          self.stopGoButton = tk.Button(buttonFrame, text="Start",
     323                                        command=self.runClicked, state=tk.DISABLED)
     324          self.stopGoButton.pack(fill=tk.X)
     325  
     326          tk.Button(buttonFrame, text="Close",
     327                    command=self.top.quit).pack(side=tk.BOTTOM, fill=tk.X)
     328          tk.Button(buttonFrame, text="Settings",
     329                    command=self.settingsClicked).pack(side=tk.BOTTOM, fill=tk.X)
     330  
     331          # Area with labels reporting results
     332          for label, var in (('Run:', self.runCountVar),
     333                             ('Failures:', self.failCountVar),
     334                             ('Errors:', self.errorCountVar),
     335                             ('Skipped:', self.skipCountVar),
     336                             ('Expected Failures:', self.expectFailCountVar),
     337                             ('Remaining:', self.remainingCountVar),
     338                             ):
     339              tk.Label(progressFrame, text=label).pack(side=tk.LEFT)
     340              tk.Label(progressFrame, textvariable=var,
     341                       foreground="blue").pack(side=tk.LEFT, fill=tk.X,
     342                                               expand=1, anchor=tk.W)
     343  
     344          # List box showing errors and failures
     345          tk.Label(leftFrame, text="Failures and errors:").pack(anchor=tk.W)
     346          listFrame = tk.Frame(leftFrame, relief=tk.SUNKEN, borderwidth=2)
     347          listFrame.pack(fill=tk.BOTH, anchor=tk.NW, expand=1)
     348          self.errorListbox = tk.Listbox(listFrame, foreground='red',
     349                                         selectmode=tk.SINGLE,
     350                                         selectborderwidth=0)
     351          self.errorListbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=1,
     352                                 anchor=tk.NW)
     353          listScroll = tk.Scrollbar(listFrame, command=self.errorListbox.yview)
     354          listScroll.pack(side=tk.LEFT, fill=tk.Y, anchor=tk.N)
     355          self.errorListbox.bind("<Double-1>",
     356                                 lambda e, self=self: self.showSelectedError())
     357          self.errorListbox.configure(yscrollcommand=listScroll.set)
     358  
     359      def errorDialog(self, title, message):
     360          messagebox.showerror(parent=self.root, title=title,
     361                               message=message)
     362  
     363      def notifyRunning(self):
     364          self.runCountVar.set(0)
     365          self.failCountVar.set(0)
     366          self.errorCountVar.set(0)
     367          self.remainingCountVar.set(self.totalTests)
     368          self.errorInfo = []
     369          while self.errorListbox.size():
     370              self.errorListbox.delete(0)
     371          #Stopping seems not to work, so simply disable the start button
     372          #self.stopGoButton.config(command=self.stopClicked, text="Stop")
     373          self.stopGoButton.config(state=tk.DISABLED)
     374          self.progressBar.setProgressFraction(0.0)
     375          self.top.update_idletasks()
     376  
     377      def notifyStopped(self):
     378          self.stopGoButton.config(state=tk.DISABLED)
     379          #self.stopGoButton.config(command=self.runClicked, text="Start")
     380          self.statusVar.set("Idle")
     381  
     382      def notifyTestStarted(self, test):
     383          self.statusVar.set(str(test))
     384          self.top.update_idletasks()
     385  
     386      def notifyTestFailed(self, test, err):
     387          self.failCountVar.set(1 + self.failCountVar.get())
     388          self.errorListbox.insert(tk.END, "Failure: %s" % test)
     389          self.errorInfo.append((test,err))
     390  
     391      def notifyTestErrored(self, test, err):
     392          self.errorCountVar.set(1 + self.errorCountVar.get())
     393          self.errorListbox.insert(tk.END, "Error: %s" % test)
     394          self.errorInfo.append((test,err))
     395  
     396      def notifyTestSkipped(self, test, reason):
     397          super(TkTestRunner, self).notifyTestSkipped(test, reason)
     398          self.skipCountVar.set(1 + self.skipCountVar.get())
     399  
     400      def notifyTestFailedExpectedly(self, test, err):
     401          super(TkTestRunner, self).notifyTestFailedExpectedly(test, err)
     402          self.expectFailCountVar.set(1 + self.expectFailCountVar.get())
     403  
     404  
     405      def notifyTestFinished(self, test):
     406          self.remainingCountVar.set(self.remainingCountVar.get() - 1)
     407          self.runCountVar.set(1 + self.runCountVar.get())
     408          fractionDone = float(self.runCountVar.get())/float(self.totalTests)
     409          fillColor = len(self.errorInfo) and "red" or "green"
     410          self.progressBar.setProgressFraction(fractionDone, fillColor)
     411  
     412      def showSelectedError(self):
     413          selection = self.errorListbox.curselection()
     414          if not selection: return
     415          selected = int(selection[0])
     416          txt = self.errorListbox.get(selected)
     417          window = tk.Toplevel(self.root)
     418          window.title(txt)
     419          window.protocol('WM_DELETE_WINDOW', window.quit)
     420          test, error = self.errorInfo[selected]
     421          tk.Label(window, text=str(test),
     422                   foreground="red", justify=tk.LEFT).pack(anchor=tk.W)
     423          tracebackLines =  traceback.format_exception(*error)
     424          tracebackText = "".join(tracebackLines)
     425          tk.Label(window, text=tracebackText, justify=tk.LEFT).pack()
     426          tk.Button(window, text="Close",
     427                    command=window.quit).pack(side=tk.BOTTOM)
     428          window.bind('<Key-Return>', lambda e, w=window: w.quit())
     429          window.mainloop()
     430          window.destroy()
     431  
     432  
     433  class ESC[4;38;5;81mProgressBar(ESC[4;38;5;149mtkESC[4;38;5;149m.ESC[4;38;5;149mFrame):
     434      """A simple progress bar that shows a percentage progress in
     435      the given colour."""
     436  
     437      def __init__(self, *args, **kwargs):
     438          tk.Frame.__init__(self, *args, **kwargs)
     439          self.canvas = tk.Canvas(self, height='20', width='60',
     440                                  background='white', borderwidth=3)
     441          self.canvas.pack(fill=tk.X, expand=1)
     442          self.rect = self.text = None
     443          self.canvas.bind('<Configure>', self.paint)
     444          self.setProgressFraction(0.0)
     445  
     446      def setProgressFraction(self, fraction, color='blue'):
     447          self.fraction = fraction
     448          self.color = color
     449          self.paint()
     450          self.canvas.update_idletasks()
     451  
     452      def paint(self, *args):
     453          totalWidth = self.canvas.winfo_width()
     454          width = int(self.fraction * float(totalWidth))
     455          height = self.canvas.winfo_height()
     456          if self.rect is not None: self.canvas.delete(self.rect)
     457          if self.text is not None: self.canvas.delete(self.text)
     458          self.rect = self.canvas.create_rectangle(0, 0, width, height,
     459                                                   fill=self.color)
     460          percentString = "%3.0f%%" % (100.0 * self.fraction)
     461          self.text = self.canvas.create_text(totalWidth/2, height/2,
     462                                              anchor=tk.CENTER,
     463                                              text=percentString)
     464  
     465  def main(initialTestName=""):
     466      root = tk.Tk()
     467      root.title("PyUnit")
     468      runner = TkTestRunner(root, initialTestName)
     469      root.protocol('WM_DELETE_WINDOW', root.quit)
     470      root.mainloop()
     471  
     472  
     473  if __name__ == '__main__':
     474      if len(sys.argv) == 2:
     475          main(sys.argv[1])
     476      else:
     477          main()