ยปCore Development>Code coverage>Tools/unittestgui/unittestgui.py

Python code coverage for Tools/unittestgui/unittestgui.py

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