PyUnit/UnittestDialog.py

changeset 0
de9c2efb9d02
child 12
1d8dd9706f46
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the UI to the pyunit package.
8 """
9
10 import unittest
11 import sys
12 import traceback
13 import time
14 import re
15 import os
16
17 from PyQt4.QtCore import *
18 from PyQt4.QtGui import *
19
20 from E4Gui.E4Application import e4App
21 from E4Gui.E4Completers import E4FileCompleter
22
23 from Ui_UnittestDialog import Ui_UnittestDialog
24 from Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog
25
26 from DebugClients.Python.coverage import coverage
27
28 import UI.PixmapCache
29
30 import Utilities
31
32 class UnittestDialog(QWidget, Ui_UnittestDialog):
33 """
34 Class implementing the UI to the pyunit package.
35
36 @signal unittestFile(string,int,int) emitted to show the source of a unittest file
37 """
38 def __init__(self,prog = None,dbs = None,ui = None,parent = None,name = None):
39 """
40 Constructor
41
42 @param prog filename of the program to open
43 @param dbs reference to the debug server object. It is an indication
44 whether we were called from within the eric4 IDE
45 @param ui reference to the UI object
46 @param parent parent widget of this dialog (QWidget)
47 @param name name of this dialog (string)
48 """
49 QWidget.__init__(self,parent)
50 if name:
51 self.setObjectName(name)
52 self.setupUi(self)
53
54 self.startButton = self.buttonBox.addButton(\
55 self.trUtf8("Start"), QDialogButtonBox.ActionRole)
56 self.startButton.setToolTip(self.trUtf8("Start the selected testsuite"))
57 self.startButton.setWhatsThis(self.trUtf8(\
58 """<b>Start Test</b>"""
59 """<p>This button starts the selected testsuite.</p>"""))
60 self.stopButton = self.buttonBox.addButton(\
61 self.trUtf8("Stop"), QDialogButtonBox.ActionRole)
62 self.stopButton.setToolTip(self.trUtf8("Stop the running unittest"))
63 self.stopButton.setWhatsThis(self.trUtf8(\
64 """<b>Stop Test</b>"""
65 """<p>This button stops a running unittest.</p>"""))
66 self.stopButton.setEnabled(False)
67 self.startButton.setDefault(True)
68
69 self.dbs = dbs
70
71 self.setWindowFlags(\
72 self.windowFlags() | Qt.WindowFlags(Qt.WindowContextHelpButtonHint))
73 self.setWindowIcon(UI.PixmapCache.getIcon("eric.png"))
74 self.setWindowTitle(self.trUtf8("Unittest"))
75 if dbs:
76 self.ui = ui
77 else:
78 self.localCheckBox.hide()
79 self.__setProgressColor("green")
80 self.progressLed.setDarkFactor(150)
81 self.progressLed.off()
82
83 self.testSuiteCompleter = E4FileCompleter(self.testsuiteComboBox)
84
85 self.fileHistory = []
86 self.testNameHistory = []
87 self.running = False
88 self.savedModulelist = None
89 self.savedSysPath = sys.path
90 if prog:
91 self.insertProg(prog)
92
93 self.rx1 = self.trUtf8("^Failure: ")
94 self.rx2 = self.trUtf8("^Error: ")
95
96 # now connect the debug server signals if called from the eric4 IDE
97 if self.dbs:
98 self.connect(self.dbs, SIGNAL('utPrepared'),
99 self.__UTPrepared)
100 self.connect(self.dbs, SIGNAL('utFinished'),
101 self.__setStoppedMode)
102 self.connect(self.dbs, SIGNAL('utStartTest'),
103 self.testStarted)
104 self.connect(self.dbs, SIGNAL('utStopTest'),
105 self.testFinished)
106 self.connect(self.dbs, SIGNAL('utTestFailed'),
107 self.testFailed)
108 self.connect(self.dbs, SIGNAL('utTestErrored'),
109 self.testErrored)
110
111 def __setProgressColor(self, color):
112 """
113 Private methode to set the color of the progress color label.
114
115 @param color colour to be shown (string)
116 """
117 self.progressLed.setColor(QColor(color))
118
119 def insertProg(self, prog):
120 """
121 Public slot to insert the filename prog into the testsuiteComboBox object.
122
123 @param prog filename to be inserted (string)
124 """
125 # prepend the selected file to the testsuite combobox
126 if prog is None:
127 prog = ""
128 if prog in self.fileHistory:
129 self.fileHistory.remove(prog)
130 self.fileHistory.insert(0, prog)
131 self.testsuiteComboBox.clear()
132 self.testsuiteComboBox.addItems(self.fileHistory)
133
134 def insertTestName(self, testName):
135 """
136 Public slot to insert a test name into the testComboBox object.
137
138 @param testName name of the test to be inserted (string)
139 """
140 # prepend the selected file to the testsuite combobox
141 if testName is None:
142 testName = ""
143 if testName in self.testNameHistory:
144 self.testNameHistory.remove(testName)
145 self.testNameHistory.insert(0, testName)
146 self.testComboBox.clear()
147 self.testComboBox.addItems(self.testNameHistory)
148
149 @pyqtSlot()
150 def on_fileDialogButton_clicked(self):
151 """
152 Private slot to open a file dialog.
153 """
154 if self.dbs:
155 pyExtensions = \
156 ' '.join(["*%s" % ext for ext in self.dbs.getExtensions('Python')])
157 py3Extensions = \
158 ' '.join(["*%s" % ext for ext in self.dbs.getExtensions('Python3')])
159 filter = self.trUtf8("Python Files ({0});;Python3 Files ({1});;All Files (*)")\
160 .format(pyExtensions, py3Extensions)
161 else:
162 filter = self.trUtf8("Python Files (*.py);;All Files (*)")
163 prog = QFileDialog.getOpenFileName(\
164 self,
165 "",
166 self.testsuiteComboBox.currentText(),
167 filter)
168
169 if not prog:
170 return
171
172 self.insertProg(Utilities.toNativeSeparators(prog))
173
174 @pyqtSlot(str)
175 def on_testsuiteComboBox_editTextChanged(self, txt):
176 """
177 Private slot to handle changes of the test file name.
178
179 @param txt name of the test file (string)
180 """
181 if self.dbs:
182 exts = self.dbs.getExtensions("Python3")
183 if txt.endswith(exts):
184 self.coverageCheckBox.setChecked(False)
185 self.coverageCheckBox.setEnabled(False)
186 self.localCheckBox.setChecked(False)
187 self.localCheckBox.setEnabled(False)
188 return
189
190 self.coverageCheckBox.setEnabled(True)
191 self.localCheckBox.setEnabled(True)
192
193 def on_buttonBox_clicked(self, button):
194 """
195 Private slot called by a button of the button box clicked.
196
197 @param button button that was clicked (QAbstractButton)
198 """
199 if button == self.startButton:
200 self.on_startButton_clicked()
201 elif button == self.stopButton:
202 self.on_stopButton_clicked()
203
204 @pyqtSlot()
205 def on_startButton_clicked(self):
206 """
207 Public slot to start the test.
208 """
209 if self.running:
210 return
211
212 prog = self.testsuiteComboBox.currentText()
213 if not prog:
214 QMessageBox.critical(self,
215 self.trUtf8("Unittest"),
216 self.trUtf8("You must enter a test suite file."))
217 return
218
219 # prepend the selected file to the testsuite combobox
220 self.insertProg(prog)
221 self.sbLabel.setText(self.trUtf8("Preparing Testsuite"))
222 QApplication.processEvents()
223
224 testFunctionName = self.testComboBox.currentText() or "suite"
225
226 # build the module name from the filename without extension
227 self.testName = os.path.splitext(os.path.basename(prog))[0]
228
229 if self.dbs and not self.localCheckBox.isChecked():
230 # we are cooperating with the eric4 IDE
231 project = e4App().getObject("Project")
232 if project.isOpen() and project.isProjectSource(prog):
233 mainScript = project.getMainScript(True)
234 else:
235 mainScript = os.path.abspath(prog)
236 self.dbs.remoteUTPrepare(prog, self.testName, testFunctionName,
237 self.coverageCheckBox.isChecked(), mainScript,
238 self.coverageEraseCheckBox.isChecked())
239 else:
240 # we are running as an application or in local mode
241 sys.path = [os.path.dirname(os.path.abspath(prog))] + self.savedSysPath
242
243 # clean up list of imported modules to force a reimport upon running the test
244 if self.savedModulelist:
245 for modname in sys.modules:
246 if modname not in self.savedModulelist:
247 # delete it
248 del(sys.modules[modname])
249 self.savedModulelist = sys.modules.copy()
250
251 # now try to generate the testsuite
252 try:
253 module = __import__(self.testName)
254 try:
255 test = unittest.defaultTestLoader.loadTestsFromName(\
256 testFunctionName, module)
257 except AttributeError:
258 test = unittest.defaultTestLoader.loadTestsFromModule(module)
259 except:
260 exc_type, exc_value, exc_tb = sys.exc_info()
261 QMessageBox.critical(self,
262 self.trUtf8("Unittest"),
263 self.trUtf8("<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>")
264 .format(self.testName, unicode(exc_type), unicode(exc_value)))
265 return
266
267 # now set up the coverage stuff
268 if self.coverageCheckBox.isChecked():
269 if self.dbs:
270 # we are cooperating with the eric4 IDE
271 project = e4App().getObject("Project")
272 if project.isOpen() and project.isProjectSource(prog):
273 mainScript = project.getMainScript(True)
274 else:
275 mainScript = os.path.abspath(prog)
276 else:
277 mainScript = os.path.abspath(prog)
278 cover = coverage(
279 data_file = "%s.coverage" % os.path.splitext(mainScript)[0])
280 cover.use_cache(True)
281 if self.coverageEraseCheckBox.isChecked():
282 cover.erase()
283 else:
284 cover = None
285
286 self.testResult = QtTestResult(self)
287 self.totalTests = test.countTestCases()
288 self.__setRunningMode()
289 if cover:
290 cover.start()
291 test.run(self.testResult)
292 if cover:
293 cover.stop()
294 cover.save()
295 self.__setStoppedMode()
296 sys.path = self.savedSysPath
297
298 def __UTPrepared(self, nrTests, exc_type, exc_value):
299 """
300 Private slot to handle the utPrepared signal.
301
302 If the unittest suite was loaded successfully, we ask the
303 client to run the test suite.
304
305 @param nrTests number of tests contained in the test suite (integer)
306 @param exc_type type of exception occured during preparation (string)
307 @param exc_value value of exception occured during preparation (string)
308 """
309 if nrTests == 0:
310 QMessageBox.critical(self,
311 self.trUtf8("Unittest"),
312 self.trUtf8("<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>")
313 .format(self.testName, exc_type, exc_value))
314 return
315
316 self.totalTests = nrTests
317 self.__setRunningMode()
318 self.dbs.remoteUTRun()
319
320 @pyqtSlot()
321 def on_stopButton_clicked(self):
322 """
323 Private slot to stop the test.
324 """
325 if self.dbs and not self.localCheckBox.isChecked():
326 self.dbs.remoteUTStop()
327 elif self.testResult:
328 self.testResult.stop()
329
330 def on_errorsListWidget_currentTextChanged(self, text):
331 """
332 Private slot to handle the highlighted signal.
333
334 @param txt current text (string)
335 """
336 if text:
337 text = re.sub(self.rx1, "", text)
338 text = re.sub(self.rx2, "", text)
339 itm = self.testsListWidget.findItems(text, Qt.MatchFlags(Qt.MatchExactly))[0]
340 self.testsListWidget.setCurrentItem(itm)
341 self.testsListWidget.scrollToItem(itm)
342
343 def __setRunningMode(self):
344 """
345 Private method to set the GUI in running mode.
346 """
347 self.running = True
348
349 # reset counters and error infos
350 self.runCount = 0
351 self.failCount = 0
352 self.errorCount = 0
353 self.remainingCount = self.totalTests
354 self.errorInfo = []
355
356 # reset the GUI
357 self.progressCounterRunCount.setText(str(self.runCount))
358 self.progressCounterFailureCount.setText(str(self.failCount))
359 self.progressCounterErrorCount.setText(str(self.errorCount))
360 self.progressCounterRemCount.setText(str(self.remainingCount))
361 self.errorsListWidget.clear()
362 self.testsListWidget.clear()
363 self.progressProgressBar.setRange(0, self.totalTests)
364 self.__setProgressColor("green")
365 self.progressProgressBar.reset()
366 self.stopButton.setEnabled(True)
367 self.startButton.setEnabled(False)
368 self.stopButton.setDefault(True)
369 self.sbLabel.setText(self.trUtf8("Running"))
370 self.progressLed.on()
371 QApplication.processEvents()
372
373 self.startTime = time.time()
374
375 def __setStoppedMode(self):
376 """
377 Private method to set the GUI in stopped mode.
378 """
379 self.stopTime = time.time()
380 self.timeTaken = float(self.stopTime - self.startTime)
381 self.running = False
382
383 self.startButton.setEnabled(True)
384 self.stopButton.setEnabled(False)
385 self.startButton.setDefault(True)
386 if self.runCount == 1:
387 self.sbLabel.setText(self.trUtf8("Ran {0} test in {1:.3f}s")
388 .format(self.runCount, self.timeTaken))
389 else:
390 self.sbLabel.setText(self.trUtf8("Ran {0} tests in {1:.3f}s")
391 .format(self.runCount, self.timeTaken))
392 self.progressLed.off()
393
394 def testFailed(self, test, exc):
395 """
396 Public method called if a test fails.
397
398 @param test name of the failed test (string)
399 @param exc string representation of the exception (list of strings)
400 """
401 self.failCount += 1
402 self.progressCounterFailureCount.setText(str(self.failCount))
403 self.errorsListWidget.insertItem(0, self.trUtf8("Failure: {0}").format(test))
404 self.errorInfo.insert(0, (test, exc))
405
406 def testErrored(self, test, exc):
407 """
408 Public method called if a test errors.
409
410 @param test name of the failed test (string)
411 @param exc string representation of the exception (list of strings)
412 """
413 self.errorCount += 1
414 self.progressCounterErrorCount.setText(str(self.errorCount))
415 self.errorsListWidget.insertItem(0, self.trUtf8("Error: {0}").format(test))
416 self.errorInfo.insert(0, (test, exc))
417
418 def testStarted(self, test, doc):
419 """
420 Public method called if a test is about to be run.
421
422 @param test name of the started test (string)
423 @param doc documentation of the started test (string)
424 """
425 if doc:
426 self.testsListWidget.insertItem(0, " %s" % doc)
427 self.testsListWidget.insertItem(0, test)
428 if self.dbs is None or self.localCheckBox.isChecked():
429 QApplication.processEvents()
430
431 def testFinished(self):
432 """
433 Public method called if a test has finished.
434
435 <b>Note</b>: It is also called if it has already failed or errored.
436 """
437 # update the counters
438 self.remainingCount -= 1
439 self.runCount += 1
440 self.progressCounterRunCount.setText(str(self.runCount))
441 self.progressCounterRemCount.setText(str(self.remainingCount))
442
443 # update the progressbar
444 if self.errorCount:
445 self.__setProgressColor("red")
446 elif self.failCount:
447 self.__setProgressColor("orange")
448 self.progressProgressBar.setValue(self.runCount)
449
450 def on_errorsListWidget_itemDoubleClicked(self, lbitem):
451 """
452 Private slot called by doubleclicking an errorlist entry.
453
454 It will popup a dialog showing the stacktrace.
455 If called from eric, an additional button is displayed
456 to show the python source in an eric source viewer (in
457 erics main window.
458
459 @param lbitem the listbox item that was double clicked
460 """
461 self.errListIndex = self.errorsListWidget.row(lbitem)
462 text = lbitem.text()
463
464 # get the error info
465 test, tracebackLines = self.errorInfo[self.errListIndex]
466 tracebackText = "".join(tracebackLines)
467
468 # now build the dialog
469 self.dlg = QDialog()
470 ui = Ui_UnittestStacktraceDialog()
471 ui.setupUi(self.dlg)
472
473 ui.showButton = ui.buttonBox.addButton(\
474 self.trUtf8("Show Source"), QDialogButtonBox.ActionRole)
475 ui.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
476
477 self.dlg.setWindowTitle(text)
478 ui.testLabel.setText(test)
479 ui.traceback.setPlainText(tracebackText)
480
481 # one more button if called from eric
482 if self.dbs:
483 self.dlg.connect(ui.showButton, SIGNAL("clicked()"),
484 self.__showSource)
485 else:
486 ui.showButton.hide()
487
488 # and now fire it up
489 self.dlg.show()
490 self.dlg.exec_()
491
492 def __showSource(self):
493 """
494 Private slot to show the source of a traceback in an eric4 editor.
495 """
496 if not self.dbs:
497 return
498
499 # get the error info
500 test, tracebackLines = self.errorInfo[self.errListIndex]
501 # find the last entry matching the pattern
502 for index in range(len(tracebackLines) - 1, -1, -1):
503 fmatch = re.search(r'File "(.*?)", line (\d*?),.*', tracebackLines[index])
504 if fmatch:
505 break
506 if fmatch:
507 fn, ln = fmatch.group(1, 2)
508 self.emit(SIGNAL('unittestFile'), fn, int(ln), 1)
509
510 class QtTestResult(unittest.TestResult):
511 """
512 A TestResult derivative to work with a graphical GUI.
513
514 For more details see pyunit.py of the standard python distribution.
515 """
516 def __init__(self, parent):
517 """
518 Constructor
519
520 @param parent The parent widget.
521 """
522 unittest.TestResult.__init__(self)
523 self.parent = parent
524
525 def addFailure(self, test, err):
526 """
527 Method called if a test failed.
528
529 @param test Reference to the test object
530 @param err The error traceback
531 """
532 unittest.TestResult.addFailure(self, test, err)
533 tracebackLines = apply(traceback.format_exception, err + (10,))
534 self.parent.testFailed(unicode(test), tracebackLines)
535
536 def addError(self, test, err):
537 """
538 Method called if a test errored.
539
540 @param test Reference to the test object
541 @param err The error traceback
542 """
543 unittest.TestResult.addError(self, test, err)
544 tracebackLines = apply(traceback.format_exception, err + (10,))
545 self.parent.testErrored(unicode(test), tracebackLines)
546
547 def startTest(self, test):
548 """
549 Method called at the start of a test.
550
551 @param test Reference to the test object
552 """
553 unittest.TestResult.startTest(self, test)
554 self.parent.testStarted(unicode(test), test.shortDescription())
555
556 def stopTest(self, test):
557 """
558 Method called at the end of a test.
559
560 @param test Reference to the test object
561 """
562 unittest.TestResult.stopTest(self, test)
563 self.parent.testFinished()
564
565 class UnittestWindow(QMainWindow):
566 """
567 Main window class for the standalone dialog.
568 """
569 def __init__(self, prog = None, parent = None):
570 """
571 Constructor
572
573 @param prog filename of the program to open
574 @param parent reference to the parent widget (QWidget)
575 """
576 QMainWindow.__init__(self, parent)
577 self.cw = UnittestDialog(prog = prog, parent = self)
578 self.cw.installEventFilter(self)
579 size = self.cw.size()
580 self.setCentralWidget(self.cw)
581 self.resize(size)
582
583 def eventFilter(self, obj, event):
584 """
585 Public method to filter events.
586
587 @param obj reference to the object the event is meant for (QObject)
588 @param event reference to the event object (QEvent)
589 @return flag indicating, whether the event was handled (boolean)
590 """
591 if event.type() == QEvent.Close:
592 QApplication.exit()
593 return True
594
595 return False

eric ide

mercurial