FreeCAD

Форк
0
/
unittestgui.py 
401 строка · 14.7 Кб
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
Further information is available in the bundled documentation, and from
7

8
  http://pyunit.sourceforge.net/
9

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.
14

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
18
DAMAGE.
19

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.
25
"""
26

27
__author__ = "Steve Purcell (stephen_purcell@yahoo.com)"
28
__version__ = "$Revision: 2.0 $"[11:-2]
29

30
import unittest
31
import sys
32
import tkinter as tk
33
from tkinter import messagebox as tkMessageBox
34
import traceback
35

36
import string
37

38

39
##############################################################################
40
# GUI framework classes
41
##############################################################################
42

43

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.
49
    """
50

51
    def __init__(self, *args, **kwargs):
52
        self.currentResult = None
53
        self.running = 0
54
        self.__rollbackImporter = None
55
        self.initGUI(*args, **kwargs)
56

57
    def getSelectedTestName(self):
58
        "Override to return the name of the test selected to be run"
59
        pass
60

61
    def errorDialog(self, title, message):
62
        "Override to display an error arising from GUI usage"
63
        pass
64

65
    def runClicked(self):
66
        "To be called in response to user choosing to run a test"
67
        if self.running:
68
            return
69
        testName = self.getSelectedTestName()
70
        if not testName:
71
            self.errorDialog("Test name entry", "You must enter a test name")
72
            return
73
        if self.__rollbackImporter:
74
            self.__rollbackImporter.rollbackImports()
75
        self.__rollbackImporter = RollbackImporter()
76
        try:
77
            test = unittest.defaultTestLoader.loadTestsFromName(testName)
78
        except Exception:
79
            exc_type, exc_value, exc_tb = sys.exc_info()
80
            traceback.print_exception(*sys.exc_info())
81
            self.errorDialog(
82
                "Unable to run test '%s'" % testName,
83
                "Error loading specified test: %s, %s" % (exc_type, exc_value),
84
            )
85
            return
86
        self.currentResult = GUITestResult(self)
87
        self.totalTests = test.countTestCases()
88
        self.running = 1
89
        self.notifyRunning()
90
        test.run(self.currentResult)
91
        self.running = 0
92
        self.notifyStopped()
93

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

99
    # Required callbacks
100

101
    def notifyRunning(self):
102
        "Override to set GUI in 'running' mode, enabling 'stop' button etc."
103
        pass
104

105
    def notifyStopped(self):
106
        "Override to set GUI in 'stopped' mode, enabling 'run' button etc."
107
        pass
108

109
    def notifyTestFailed(self, test, err):
110
        "Override to indicate that a test has just failed"
111
        pass
112

113
    def notifyTestErrored(self, test, err):
114
        "Override to indicate that a test has just errored"
115
        pass
116

117
    def notifyTestStarted(self, test):
118
        "Override to indicate that a test is about to run"
119
        pass
120

121
    def notifyTestFinished(self, test):
122
        """Override to indicate that a test has finished (it may already have
123
        failed or errored)"""
124
        pass
125

126

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.
130
    """
131

132
    def __init__(self, callback):
133
        unittest.TestResult.__init__(self)
134
        self.callback = callback
135

136
    def addError(self, test, err):
137
        unittest.TestResult.addError(self, test, err)
138
        self.callback.notifyTestErrored(test, err)
139

140
    def addFailure(self, test, err):
141
        unittest.TestResult.addFailure(self, test, err)
142
        self.callback.notifyTestFailed(test, err)
143

144
    def stopTest(self, test):
145
        unittest.TestResult.stopTest(self, test)
146
        self.callback.notifyTestFinished(test)
147

148
    def startTest(self, test):
149
        unittest.TestResult.startTest(self, test)
150
        self.callback.notifyTestStarted(test)
151

152

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.
156
    """
157

158
    def __init__(self):
159
        self.previousModules = sys.modules.copy()
160

161
    def rollbackImports(self):
162
        for modname in sys.modules.keys():
163
            if modname not in self.previousModules:
164
                # Force reload when modname next imported
165
                del sys.modules[modname]
166

167

168
##############################################################################
169
# Tkinter GUI
170
##############################################################################
171

172
_ABOUT_TEXT = """\
173
PyUnit unit testing framework.
174

175
For more information, visit
176
http://pyunit.sourceforge.net/
177

178
Copyright (c) 2000 Steve Purcell
179
<stephen_purcell@yahoo.com>
180
"""
181
_HELP_TEXT = """\
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.
184

185
Double click on an error in the listbox to see more information about it, \
186
including the stack trace.
187

188
For more information, visit
189
http://pyunit.sourceforge.net/
190
or see the bundled documentation
191
"""
192

193

194
class TkTestRunner(BaseGUITestRunner):
195
    """An implementation of BaseGUITestRunner using Tkinter."""
196

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.
200
        """
201
        self.root = root
202
        # Set up values that will be tied to widgets
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)
213
        self.createWidgets()
214

215
    def createWidgets(self):
216
        """Creates and packs the various widgets.
217

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.
220
        """
221
        # Status bar
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)
225

226
        # Area to enter name of test to run
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)
234
        e.focus_set()
235
        e.bind("<Key-Return>", lambda e, self=self: self.runClicked())
236

237
        # Progress bar
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)
243

244
        # Area with buttons to start/stop tests and quit
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
252
        )
253
        tk.Button(buttonFrame, text="Help", command=self.showHelpDialog).pack(
254
            side=tk.BOTTOM, fill=tk.X
255
        )
256

257
        # Area with labels reporting results
258
        for label, var in (
259
            ("Run:", self.runCountVar),
260
            ("Failures:", self.failCountVar),
261
            ("Errors:", self.errorCountVar),
262
            ("Remaining:", self.remainingCountVar),
263
        ):
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
267
            )
268

269
        # List box showing errors and failures
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
275
        )
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)
281

282
    def getSelectedTestName(self):
283
        return self.suiteNameVar.get()
284

285
    def errorDialog(self, title, message):
286
        tkMessageBox.showerror(parent=self.root, title=title, message=message)
287

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)
293
        self.errorInfo = []
294
        while self.errorListbox.size():
295
            self.errorListbox.delete(0)
296
        # Stopping seems not to work, so simply disable the start button
297
        # self.stopGoButton.config(command=self.stopClicked, text="Stop")
298
        self.stopGoButton.config(state=tk.DISABLED)
299
        self.progressBar.setProgressFraction(0.0)
300
        self.top.update_idletasks()
301

302
    def notifyStopped(self):
303
        self.stopGoButton.config(state=tk.ACTIVE)
304
        # self.stopGoButton.config(command=self.runClicked, text="Start")
305
        self.statusVar.set("Idle")
306

307
    def notifyTestStarted(self, test):
308
        self.statusVar.set(str(test))
309
        self.top.update_idletasks()
310

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

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

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

328
    def showAboutDialog(self):
329
        tkMessageBox.showinfo(parent=self.root, title="About PyUnit", message=_ABOUT_TEXT)
330

331
    def showHelpDialog(self):
332
        tkMessageBox.showinfo(parent=self.root, title="PyUnit help", message=_HELP_TEXT)
333

334
    def showSelectedError(self):
335
        selection = self.errorListbox.curselection()
336
        if not selection:
337
            return
338
        selected = int(selection[0])
339
        txt = self.errorListbox.get(selected)
340
        window = tk.Toplevel(self.root)
341
        window.title(txt)
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())
350
        window.mainloop()
351
        window.destroy()
352

353

354
class ProgressBar(tk.Frame):
355
    """A simple progress bar that shows a percentage progress in
356
    the given colour."""
357

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

366
    def setProgressFraction(self, fraction, color="blue"):
367
        self.fraction = fraction
368
        self.color = color
369
        self.paint()
370
        self.canvas.update_idletasks()
371

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
384
        )
385

386

387
def main(initialTestName=""):
388
    root = tk.Tk()
389
    root.title("PyUnit")
390
    runner = TkTestRunner(root, initialTestName)
391
    root.protocol("WM_DELETE_WINDOW", root.quit)
392
    root.mainloop()
393

394

395
if __name__ == "__main__":
396
    import sys
397

398
    if len(sys.argv) == 2:
399
        main(sys.argv[1])
400
    else:
401
        main()
402

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.