3
GUI framework and application for use with Python unit testing framework.
4
Execute tests written using the framework provided by the 'unittest' module.
6
Further information is available in the bundled documentation, and from
8
http://pyunit.sourceforge.net/
10
Copyright (c) 1999, 2000, 2001 Steve Purcell
11
This module is free software, and you may redistribute it and/or modify
12
it under the same terms as Python itself, so long as this copyright message
13
and disclaimer are retained in their original form.
15
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
16
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
17
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
20
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
21
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
23
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
24
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
27
__author__ = "Steve Purcell (stephen_purcell@yahoo.com)"
28
__version__ = "$Revision: 2.0 $"[11:-2]
33
from tkinter import messagebox as tkMessageBox
44
class BaseGUITestRunner:
45
"""Subclass this class to create a GUI TestRunner that uses a specific
46
windowing toolkit. The class takes care of running tests in the correct
47
manner, and making callbacks to the derived class to obtain information
48
or signal that events have occurred.
51
def __init__(self, *args, **kwargs):
52
self.currentResult = None
54
self.__rollbackImporter = None
55
self.initGUI(*args, **kwargs)
57
def getSelectedTestName(self):
58
"Override to return the name of the test selected to be run"
61
def errorDialog(self, title, message):
62
"Override to display an error arising from GUI usage"
66
"To be called in response to user choosing to run a test"
69
testName = self.getSelectedTestName()
71
self.errorDialog("Test name entry", "You must enter a test name")
73
if self.__rollbackImporter:
74
self.__rollbackImporter.rollbackImports()
75
self.__rollbackImporter = RollbackImporter()
77
test = unittest.defaultTestLoader.loadTestsFromName(testName)
79
exc_type, exc_value, exc_tb = sys.exc_info()
80
traceback.print_exception(*sys.exc_info())
82
"Unable to run test '%s'" % testName,
83
"Error loading specified test: %s, %s" % (exc_type, exc_value),
86
self.currentResult = GUITestResult(self)
87
self.totalTests = test.countTestCases()
90
test.run(self.currentResult)
94
def stopClicked(self):
95
"To be called in response to user stopping the running of a test"
96
if self.currentResult:
97
self.currentResult.stop()
101
def notifyRunning(self):
102
"Override to set GUI in 'running' mode, enabling 'stop' button etc."
105
def notifyStopped(self):
106
"Override to set GUI in 'stopped' mode, enabling 'run' button etc."
109
def notifyTestFailed(self, test, err):
110
"Override to indicate that a test has just failed"
113
def notifyTestErrored(self, test, err):
114
"Override to indicate that a test has just errored"
117
def notifyTestStarted(self, test):
118
"Override to indicate that a test is about to run"
121
def notifyTestFinished(self, test):
122
"""Override to indicate that a test has finished (it may already have
123
failed or errored)"""
127
class GUITestResult(unittest.TestResult):
128
"""A TestResult that makes callbacks to its associated GUI TestRunner.
129
Used by BaseGUITestRunner. Need not be created directly.
132
def __init__(self, callback):
133
unittest.TestResult.__init__(self)
134
self.callback = callback
136
def addError(self, test, err):
137
unittest.TestResult.addError(self, test, err)
138
self.callback.notifyTestErrored(test, err)
140
def addFailure(self, test, err):
141
unittest.TestResult.addFailure(self, test, err)
142
self.callback.notifyTestFailed(test, err)
144
def stopTest(self, test):
145
unittest.TestResult.stopTest(self, test)
146
self.callback.notifyTestFinished(test)
148
def startTest(self, test):
149
unittest.TestResult.startTest(self, test)
150
self.callback.notifyTestStarted(test)
153
class RollbackImporter:
154
"""This tricky little class is used to make sure that modules under test
155
will be reloaded the next time they are imported.
159
self.previousModules = sys.modules.copy()
161
def rollbackImports(self):
162
for modname in sys.modules.keys():
163
if modname not in self.previousModules:
165
del sys.modules[modname]
173
PyUnit unit testing framework.
175
For more information, visit
176
http://pyunit.sourceforge.net/
178
Copyright (c) 2000 Steve Purcell
179
<stephen_purcell@yahoo.com>
182
Enter the name of a callable object which, when called, will return a \
183
TestCase or TestSuite. Click 'start', and the test thus produced will be run.
185
Double click on an error in the listbox to see more information about it, \
186
including the stack trace.
188
For more information, visit
189
http://pyunit.sourceforge.net/
190
or see the bundled documentation
194
class TkTestRunner(BaseGUITestRunner):
195
"""An implementation of BaseGUITestRunner using Tkinter."""
197
def initGUI(self, root, initialTestName):
198
"""Set up the GUI inside the given root window. The test name entry
199
field will be pre-filled with the given initialTestName.
203
self.suiteNameVar = tk.StringVar()
204
self.suiteNameVar.set(initialTestName)
205
self.statusVar = tk.StringVar()
206
self.statusVar.set("Idle")
207
self.runCountVar = tk.IntVar()
208
self.failCountVar = tk.IntVar()
209
self.errorCountVar = tk.IntVar()
210
self.remainingCountVar = tk.IntVar()
211
self.top = tk.Frame()
212
self.top.pack(fill=tk.BOTH, expand=1)
215
def createWidgets(self):
216
"""Creates and packs the various widgets.
218
Why is it that GUI code always ends up looking a mess, despite all the
219
best intentions to keep it tidy? Answers on a postcard, please.
222
statusFrame = tk.Frame(self.top, relief=tk.SUNKEN, borderwidth=2)
223
statusFrame.pack(anchor=tk.SW, fill=tk.X, side=tk.BOTTOM)
224
tk.Label(statusFrame, textvariable=self.statusVar).pack(side=tk.LEFT)
227
leftFrame = tk.Frame(self.top, borderwidth=3)
228
leftFrame.pack(fill=tk.BOTH, side=tk.LEFT, anchor=tk.NW, expand=1)
229
suiteNameFrame = tk.Frame(leftFrame, borderwidth=3)
230
suiteNameFrame.pack(fill=tk.X)
231
tk.Label(suiteNameFrame, text="Enter test name:").pack(side=tk.LEFT)
232
e = tk.Entry(suiteNameFrame, textvariable=self.suiteNameVar, width=25)
233
e.pack(side=tk.LEFT, fill=tk.X, expand=1)
235
e.bind("<Key-Return>", lambda e, self=self: self.runClicked())
238
progressFrame = tk.Frame(leftFrame, relief=tk.GROOVE, borderwidth=2)
239
progressFrame.pack(fill=tk.X, expand=0, anchor=tk.NW)
240
tk.Label(progressFrame, text="Progress:").pack(anchor=tk.W)
241
self.progressBar = ProgressBar(progressFrame, relief=tk.SUNKEN, borderwidth=2)
242
self.progressBar.pack(fill=tk.X, expand=1)
245
buttonFrame = tk.Frame(self.top, borderwidth=3)
246
buttonFrame.pack(side=tk.LEFT, anchor=tk.NW, fill=tk.Y)
247
self.stopGoButton = tk.Button(buttonFrame, text="Start", command=self.runClicked)
248
self.stopGoButton.pack(fill=tk.X)
249
tk.Button(buttonFrame, text="Close", command=self.top.quit).pack(side=tk.BOTTOM, fill=tk.X)
250
tk.Button(buttonFrame, text="About", command=self.showAboutDialog).pack(
251
side=tk.BOTTOM, fill=tk.X
253
tk.Button(buttonFrame, text="Help", command=self.showHelpDialog).pack(
254
side=tk.BOTTOM, fill=tk.X
259
("Run:", self.runCountVar),
260
("Failures:", self.failCountVar),
261
("Errors:", self.errorCountVar),
262
("Remaining:", self.remainingCountVar),
264
tk.Label(progressFrame, text=label).pack(side=tk.LEFT)
265
tk.Label(progressFrame, textvariable=var, foreground="blue").pack(
266
side=tk.LEFT, fill=tk.X, expand=1, anchor=tk.W
270
tk.Label(leftFrame, text="Failures and errors:").pack(anchor=tk.W)
271
listFrame = tk.Frame(leftFrame, relief=tk.SUNKEN, borderwidth=2)
272
listFrame.pack(fill=tk.BOTH, anchor=tk.NW, expand=1)
273
self.errorListbox = tk.Listbox(
274
listFrame, foreground="red", selectmode=tk.SINGLE, selectborderwidth=0
276
self.errorListbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=1, anchor=tk.NW)
277
listScroll = tk.Scrollbar(listFrame, command=self.errorListbox.yview)
278
listScroll.pack(side=tk.LEFT, fill=tk.Y, anchor=tk.N)
279
self.errorListbox.bind("<Double-1>", lambda e, self=self: self.showSelectedError())
280
self.errorListbox.configure(yscrollcommand=listScroll.set)
282
def getSelectedTestName(self):
283
return self.suiteNameVar.get()
285
def errorDialog(self, title, message):
286
tkMessageBox.showerror(parent=self.root, title=title, message=message)
288
def notifyRunning(self):
289
self.runCountVar.set(0)
290
self.failCountVar.set(0)
291
self.errorCountVar.set(0)
292
self.remainingCountVar.set(self.totalTests)
294
while self.errorListbox.size():
295
self.errorListbox.delete(0)
298
self.stopGoButton.config(state=tk.DISABLED)
299
self.progressBar.setProgressFraction(0.0)
300
self.top.update_idletasks()
302
def notifyStopped(self):
303
self.stopGoButton.config(state=tk.ACTIVE)
305
self.statusVar.set("Idle")
307
def notifyTestStarted(self, test):
308
self.statusVar.set(str(test))
309
self.top.update_idletasks()
311
def notifyTestFailed(self, test, err):
312
self.failCountVar.set(1 + self.failCountVar.get())
313
self.errorListbox.insert(tk.END, "Failure: %s" % test)
314
self.errorInfo.append((test, err))
316
def notifyTestErrored(self, test, err):
317
self.errorCountVar.set(1 + self.errorCountVar.get())
318
self.errorListbox.insert(tk.END, "Error: %s" % test)
319
self.errorInfo.append((test, err))
321
def notifyTestFinished(self, test):
322
self.remainingCountVar.set(self.remainingCountVar.get() - 1)
323
self.runCountVar.set(1 + self.runCountVar.get())
324
fractionDone = float(self.runCountVar.get()) / float(self.totalTests)
325
fillColor = len(self.errorInfo) and "red" or "green"
326
self.progressBar.setProgressFraction(fractionDone, fillColor)
328
def showAboutDialog(self):
329
tkMessageBox.showinfo(parent=self.root, title="About PyUnit", message=_ABOUT_TEXT)
331
def showHelpDialog(self):
332
tkMessageBox.showinfo(parent=self.root, title="PyUnit help", message=_HELP_TEXT)
334
def showSelectedError(self):
335
selection = self.errorListbox.curselection()
338
selected = int(selection[0])
339
txt = self.errorListbox.get(selected)
340
window = tk.Toplevel(self.root)
342
window.protocol("WM_DELETE_WINDOW", window.quit)
343
test, error = self.errorInfo[selected]
344
tk.Label(window, text=str(test), foreground="red", justify=tk.LEFT).pack(anchor=tk.W)
345
tracebackLines = traceback.format_exception(*error + (10,))
346
tracebackText = string.join(tracebackLines, "")
347
tk.Label(window, text=tracebackText, justify=tk.LEFT).pack()
348
tk.Button(window, text="Close", command=window.quit).pack(side=tk.BOTTOM)
349
window.bind("<Key-Return>", lambda e, w=window: w.quit())
354
class ProgressBar(tk.Frame):
355
"""A simple progress bar that shows a percentage progress in
358
def __init__(self, *args, **kwargs):
359
tk.Frame.__init__(*(self,) + args, **kwargs)
360
self.canvas = tk.Canvas(self, height="20", width="60", background="white", borderwidth=3)
361
self.canvas.pack(fill=tk.X, expand=1)
362
self.rect = self.text = None
363
self.canvas.bind("<Configure>", self.paint)
364
self.setProgressFraction(0.0)
366
def setProgressFraction(self, fraction, color="blue"):
367
self.fraction = fraction
370
self.canvas.update_idletasks()
372
def paint(self, *args):
373
totalWidth = self.canvas.winfo_width()
374
width = int(self.fraction * float(totalWidth))
375
height = self.canvas.winfo_height()
376
if self.rect is not None:
377
self.canvas.delete(self.rect)
378
if self.text is not None:
379
self.canvas.delete(self.text)
380
self.rect = self.canvas.create_rectangle(0, 0, width, height, fill=self.color)
381
percentString = "%3.0f%%" % (100.0 * self.fraction)
382
self.text = self.canvas.create_text(
383
totalWidth / 2, height / 2, anchor=tk.CENTER, text=percentString
387
def main(initialTestName=""):
390
runner = TkTestRunner(root, initialTestName)
391
root.protocol("WM_DELETE_WINDOW", root.quit)
395
if __name__ == "__main__":
398
if len(sys.argv) == 2: