Sat, 17 Dec 2011 19:13:19 +0100
Extended the unit test module to allow to rerun only failed tests of the last.
# -*- coding: utf-8 -*- # Copyright (c) 2002 - 2011 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the UI to the pyunit package. """ import unittest import sys import time import re import os from PyQt4.QtCore import pyqtSignal, QEvent, Qt, pyqtSlot from PyQt4.QtGui import QWidget, QColor, QDialog, QApplication, QDialogButtonBox, \ QMainWindow, QListWidgetItem from E5Gui.E5Application import e5App from E5Gui.E5Completers import E5FileCompleter from E5Gui import E5MessageBox, E5FileDialog from .Ui_UnittestDialog import Ui_UnittestDialog from .Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog from DebugClients.Python3.coverage import coverage import UI.PixmapCache import Utilities import Preferences class UnittestDialog(QWidget, Ui_UnittestDialog): """ Class implementing the UI to the pyunit package. @signal unittestFile(str, int, int) emitted to show the source of a unittest file @signal unittestStopped() emitted after a unit test was run """ unittestFile = pyqtSignal(str, int, int) unittestStopped = pyqtSignal() def __init__(self, prog=None, dbs=None, ui=None, fromEric=False, parent=None, name=None): """ Constructor @param prog filename of the program to open @param dbs reference to the debug server object. It is an indication whether we were called from within the eric5 IDE @param ui reference to the UI object @param fromEric flag indicating an instantiation from within the eric IDE (boolean) @param parent parent widget of this dialog (QWidget) @param name name of this dialog (string) """ super().__init__(parent) if name: self.setObjectName(name) self.setupUi(self) self.startButton = self.buttonBox.addButton( self.trUtf8("Start"), QDialogButtonBox.ActionRole) self.startButton.setToolTip(self.trUtf8("Start the selected testsuite")) self.startButton.setWhatsThis(self.trUtf8( """<b>Start Test</b>""" """<p>This button starts the selected testsuite.</p>""")) self.startFailedButton = self.buttonBox.addButton( self.trUtf8("Rerun Failed"), QDialogButtonBox.ActionRole) self.startFailedButton.setToolTip( self.trUtf8("Reruns failed tests of the selected testsuite")) self.startFailedButton.setWhatsThis(self.trUtf8( """<b>Rerun Failed</b>""" """<p>This button reruns all failed tests of the selected testsuite.</p>""")) self.stopButton = self.buttonBox.addButton( self.trUtf8("Stop"), QDialogButtonBox.ActionRole) self.stopButton.setToolTip(self.trUtf8("Stop the running unittest")) self.stopButton.setWhatsThis(self.trUtf8( """<b>Stop Test</b>""" """<p>This button stops a running unittest.</p>""")) self.stopButton.setEnabled(False) self.startButton.setDefault(True) self.startFailedButton.setEnabled(False) self.dbs = dbs self.__fromEric = fromEric self.setWindowFlags( self.windowFlags() | Qt.WindowFlags(Qt.WindowContextHelpButtonHint)) self.setWindowIcon(UI.PixmapCache.getIcon("eric.png")) self.setWindowTitle(self.trUtf8("Unittest")) if dbs: self.ui = ui else: self.localCheckBox.hide() self.__setProgressColor("green") self.progressLed.setDarkFactor(150) self.progressLed.off() self.testSuiteCompleter = E5FileCompleter(self.testsuiteComboBox) self.fileHistory = [] self.testNameHistory = [] self.running = False self.savedModulelist = None self.savedSysPath = sys.path if prog: self.insertProg(prog) self.rxPatterns = [ self.trUtf8("^Failure: "), self.trUtf8("^Error: "), ] self.__failedTests = [] # now connect the debug server signals if called from the eric5 IDE if self.dbs: self.dbs.utPrepared.connect(self.__UTPrepared) self.dbs.utFinished.connect(self.__setStoppedMode) self.dbs.utStartTest.connect(self.testStarted) self.dbs.utStopTest.connect(self.testFinished) self.dbs.utTestFailed.connect(self.testFailed) self.dbs.utTestErrored.connect(self.testErrored) self.dbs.utTestSkipped.connect(self.testSkipped) self.dbs.utTestFailedExpected.connect(self.testFailedExpected) self.dbs.utTestSucceededUnexpected.connect(self.testSucceededUnexpected) def keyPressEvent(self, evt): """ Protected slot to handle key press events. @param evt key press event to handle (QKeyEvent) """ if evt.key() == Qt.Key_Escape and self.__fromEric: self.close() def __setProgressColor(self, color): """ Private methode to set the color of the progress color label. @param color colour to be shown (string) """ self.progressLed.setColor(QColor(color)) def insertProg(self, prog): """ Public slot to insert the filename prog into the testsuiteComboBox object. @param prog filename to be inserted (string) """ # prepend the selected file to the testsuite combobox if prog is None: prog = "" if prog in self.fileHistory: self.fileHistory.remove(prog) self.fileHistory.insert(0, prog) self.testsuiteComboBox.clear() self.testsuiteComboBox.addItems(self.fileHistory) def insertTestName(self, testName): """ Public slot to insert a test name into the testComboBox object. @param testName name of the test to be inserted (string) """ # prepend the selected file to the testsuite combobox if testName is None: testName = "" if testName in self.testNameHistory: self.testNameHistory.remove(testName) self.testNameHistory.insert(0, testName) self.testComboBox.clear() self.testComboBox.addItems(self.testNameHistory) @pyqtSlot() def on_fileDialogButton_clicked(self): """ Private slot to open a file dialog. """ if self.dbs: py2Extensions = \ ' '.join(["*{0}".format(ext) for ext in self.dbs.getExtensions('Python2')]) py3Extensions = \ ' '.join(["*{0}".format(ext) for ext in self.dbs.getExtensions('Python3')]) filter = self.trUtf8( "Python3 Files ({1});;Python2 Files ({0});;All Files (*)")\ .format(py2Extensions, py3Extensions) else: filter = self.trUtf8("Python Files (*.py);;All Files (*)") prog = E5FileDialog.getOpenFileName( self, "", self.testsuiteComboBox.currentText(), filter) if not prog: return self.insertProg(Utilities.toNativeSeparators(prog)) @pyqtSlot(str) def on_testsuiteComboBox_editTextChanged(self, txt): """ Private slot to handle changes of the test file name. @param txt name of the test file (string) """ if self.dbs: exts = self.dbs.getExtensions("Python2") flags = Utilities.extractFlagsFromFile(txt) if txt.endswith(exts) or \ ("FileType" in flags and flags["FileType"] in ["Python", "Python2"]): self.coverageCheckBox.setChecked(False) self.coverageCheckBox.setEnabled(False) self.localCheckBox.setChecked(False) self.localCheckBox.setEnabled(False) return self.coverageCheckBox.setEnabled(True) self.localCheckBox.setEnabled(True) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.startButton: self.on_startButton_clicked() elif button == self.stopButton: self.on_stopButton_clicked() elif button == self.startFailedButton: self.on_startButton_clicked(failedOnly=True) @pyqtSlot() def on_startButton_clicked(self, failedOnly=False): """ Public slot to start the test. @keyparam failedOnly flag indicating to run only failed tests (boolean) """ if self.running: return prog = self.testsuiteComboBox.currentText() if not prog: E5MessageBox.critical(self, self.trUtf8("Unittest"), self.trUtf8("You must enter a test suite file.")) return # prepend the selected file to the testsuite combobox self.insertProg(prog) self.sbLabel.setText(self.trUtf8("Preparing Testsuite")) QApplication.processEvents() testFunctionName = self.testComboBox.currentText() if testFunctionName: self.insertTestName(testFunctionName) else: testFunctionName = "suite" # build the module name from the filename without extension self.testName = os.path.splitext(os.path.basename(prog))[0] if self.dbs and not self.localCheckBox.isChecked(): # we are cooperating with the eric5 IDE project = e5App().getObject("Project") if project.isOpen() and project.isProjectSource(prog): mainScript = project.getMainScript(True) clientType = self.project.getProjectLanguage() else: mainScript = os.path.abspath(prog) flags = Utilities.extractFlagsFromFile(mainScript) if mainScript.endswith( tuple(Preferences.getPython("PythonExtensions"))) or \ ("FileType" in flags and flags["FileType"] in ["Python", "Python2"]): clientType = "Python2" else: clientType = "" if failedOnly and self.__failedTests: failed = [t.split(".", 1)[1] for t in self.__failedTests] else: failed = [] self.__failedTests = [] self.dbs.remoteUTPrepare(prog, self.testName, testFunctionName, failed, self.coverageCheckBox.isChecked(), mainScript, self.coverageEraseCheckBox.isChecked(), clientType=clientType) else: # we are running as an application or in local mode sys.path = [os.path.dirname(os.path.abspath(prog))] + self.savedSysPath # clean up list of imported modules to force a reimport upon running the test if self.savedModulelist: for modname in list(sys.modules.keys()): if modname not in self.savedModulelist: # delete it del(sys.modules[modname]) self.savedModulelist = sys.modules.copy() # now try to generate the testsuite try: module = __import__(self.testName) try: if failedOnly and self.__failedTests: test = unittest.defaultTestLoader.loadTestsFromNames( [t.split(".", 1)[1] for t in self.__failedTests], module) else: test = unittest.defaultTestLoader.loadTestsFromName( testFunctionName, module) except AttributeError: test = unittest.defaultTestLoader.loadTestsFromModule(module) except: exc_type, exc_value, exc_tb = sys.exc_info() E5MessageBox.critical(self, self.trUtf8("Unittest"), self.trUtf8("<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>") .format(self.testName, str(exc_type), str(exc_value))) return # now set up the coverage stuff if self.coverageCheckBox.isChecked(): if self.dbs: # we are cooperating with the eric5 IDE project = e5App().getObject("Project") if project.isOpen() and project.isProjectSource(prog): mainScript = project.getMainScript(True) else: mainScript = os.path.abspath(prog) else: mainScript = os.path.abspath(prog) cover = coverage( data_file="{0}.coverage".format(os.path.splitext(mainScript)[0])) cover.use_cache(True) if self.coverageEraseCheckBox.isChecked(): cover.erase() else: cover = None self.testResult = QtTestResult(self) self.totalTests = test.countTestCases() self.__failedTests = [] self.__setRunningMode() if cover: cover.start() test.run(self.testResult) if cover: cover.stop() cover.save() self.__setStoppedMode() sys.path = self.savedSysPath def __UTPrepared(self, nrTests, exc_type, exc_value): """ Private slot to handle the utPrepared signal. If the unittest suite was loaded successfully, we ask the client to run the test suite. @param nrTests number of tests contained in the test suite (integer) @param exc_type type of exception occured during preparation (string) @param exc_value value of exception occured during preparation (string) """ if nrTests == 0: E5MessageBox.critical(self, self.trUtf8("Unittest"), self.trUtf8("<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>") .format(self.testName, exc_type, exc_value)) return self.totalTests = nrTests self.__setRunningMode() self.dbs.remoteUTRun() @pyqtSlot() def on_stopButton_clicked(self): """ Private slot to stop the test. """ if self.dbs and not self.localCheckBox.isChecked(): self.dbs.remoteUTStop() elif self.testResult: self.testResult.stop() def on_errorsListWidget_currentTextChanged(self, text): """ Private slot to handle the highlighted signal. @param txt current text (string) """ if text: for pattern in self.rxPatterns: text = re.sub(pattern, "", text) itm = self.testsListWidget.findItems(text, Qt.MatchFlags(Qt.MatchExactly))[0] self.testsListWidget.setCurrentItem(itm) self.testsListWidget.scrollToItem(itm) def __setRunningMode(self): """ Private method to set the GUI in running mode. """ self.running = True # reset counters and error infos self.runCount = 0 self.failCount = 0 self.errorCount = 0 self.skippedCount = 0 self.expectedFailureCount = 0 self.unexpectedSuccessCount = 0 self.remainingCount = self.totalTests # reset the GUI self.progressCounterRunCount.setText(str(self.runCount)) self.progressCounterRemCount.setText(str(self.remainingCount)) self.progressCounterFailureCount.setText(str(self.failCount)) self.progressCounterErrorCount.setText(str(self.errorCount)) self.progressCounterSkippedCount.setText(str(self.skippedCount)) self.progressCounterExpectedFailureCount.setText(str(self.expectedFailureCount)) self.progressCounterUnexpectedSuccessCount.setText( str(self.unexpectedSuccessCount)) self.errorsListWidget.clear() self.testsListWidget.clear() self.progressProgressBar.setRange(0, self.totalTests) self.__setProgressColor("green") self.progressProgressBar.reset() self.stopButton.setEnabled(True) self.startButton.setEnabled(False) self.stopButton.setDefault(True) self.sbLabel.setText(self.trUtf8("Running")) self.progressLed.on() QApplication.processEvents() self.startTime = time.time() def __setStoppedMode(self): """ Private method to set the GUI in stopped mode. """ self.stopTime = time.time() self.timeTaken = float(self.stopTime - self.startTime) self.running = False self.startButton.setEnabled(True) self.startFailedButton.setEnabled(bool(self.__failedTests)) self.stopButton.setEnabled(False) if self.__failedTests: self.startFailedButton.setDefault(True) self.startButton.setDefault(False) else: self.startFailedButton.setDefault(False) self.startButton.setDefault(True) if self.runCount == 1: self.sbLabel.setText(self.trUtf8("Ran {0} test in {1:.3f}s") .format(self.runCount, self.timeTaken)) else: self.sbLabel.setText(self.trUtf8("Ran {0} tests in {1:.3f}s") .format(self.runCount, self.timeTaken)) self.progressLed.off() self.unittestStopped.emit() def testFailed(self, test, exc, id): """ Public method called if a test fails. @param test name of the test (string) @param exc string representation of the exception (string) @param id id of the test (string) """ self.failCount += 1 self.progressCounterFailureCount.setText(str(self.failCount)) itm = QListWidgetItem(self.trUtf8("Failure: {0}").format(test)) itm.setData(Qt.UserRole, (test, exc)) self.errorsListWidget.insertItem(0, itm) self.__failedTests.append(id) def testErrored(self, test, exc, id): """ Public method called if a test errors. @param test name of the test (string) @param exc string representation of the exception (string) @param id id of the test (string) """ self.errorCount += 1 self.progressCounterErrorCount.setText(str(self.errorCount)) itm = QListWidgetItem(self.trUtf8("Error: {0}").format(test)) itm.setData(Qt.UserRole, (test, exc)) self.errorsListWidget.insertItem(0, itm) self.__failedTests.append(id) def testSkipped(self, test, reason, id): """ Public method called if a test was skipped. @param test name of the test (string) @param reason reason for skipping the test (string) @param id id of the test (string) """ self.skippedCount += 1 self.progressCounterSkippedCount.setText(str(self.skippedCount)) itm = QListWidgetItem(self.trUtf8(" Skipped: {0}").format(reason)) itm.setForeground(Qt.blue) self.testsListWidget.insertItem(1, itm) def testFailedExpected(self, test, exc, id): """ Public method called if a test fails expectedly. @param test name of the test (string) @param exc string representation of the exception (string) @param id id of the test (string) """ self.expectedFailureCount += 1 self.progressCounterExpectedFailureCount.setText(str(self.expectedFailureCount)) itm = QListWidgetItem(self.trUtf8(" Expected Failure")) itm.setForeground(Qt.blue) self.testsListWidget.insertItem(1, itm) def testSucceededUnexpected(self, test, id): """ Public method called if a test succeeds unexpectedly. @param test name of the test (string) @param id id of the test (string) """ self.unexpectedSuccessCount += 1 self.progressCounterUnexpectedSuccessCount.setText( str(self.unexpectedSuccessCount)) itm = QListWidgetItem(self.trUtf8(" Unexpected Success")) itm.setForeground(Qt.red) self.testsListWidget.insertItem(1, itm) def testStarted(self, test, doc): """ Public method called if a test is about to be run. @param test name of the started test (string) @param doc documentation of the started test (string) """ if doc: self.testsListWidget.insertItem(0, " {0}".format(doc)) self.testsListWidget.insertItem(0, test) if self.dbs is None or self.localCheckBox.isChecked(): QApplication.processEvents() def testFinished(self): """ Public method called if a test has finished. <b>Note</b>: It is also called if it has already failed or errored. """ # update the counters self.remainingCount -= 1 self.runCount += 1 self.progressCounterRunCount.setText(str(self.runCount)) self.progressCounterRemCount.setText(str(self.remainingCount)) # update the progressbar if self.errorCount: self.__setProgressColor("red") elif self.failCount: self.__setProgressColor("orange") self.progressProgressBar.setValue(self.runCount) def on_errorsListWidget_itemDoubleClicked(self, lbitem): """ Private slot called by doubleclicking an errorlist entry. It will popup a dialog showing the stacktrace. If called from eric, an additional button is displayed to show the python source in an eric source viewer (in erics main window. @param lbitem the listbox item that was double clicked """ self.errListIndex = self.errorsListWidget.row(lbitem) text = lbitem.text() self.on_errorsListWidget_currentTextChanged(text) # get the error info test, tracebackText = lbitem.data(Qt.UserRole) # now build the dialog self.dlg = QDialog() ui = Ui_UnittestStacktraceDialog() ui.setupUi(self.dlg) self.dlg.traceback = ui.traceback ui.showButton = ui.buttonBox.addButton( self.trUtf8("Show Source"), QDialogButtonBox.ActionRole) ui.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.dlg.setWindowTitle(text) ui.testLabel.setText(test) ui.traceback.setPlainText(tracebackText) # one more button if called from eric if self.dbs: ui.showButton.clicked[()].connect(self.__showSource) else: ui.showButton.hide() # and now fire it up self.dlg.show() self.dlg.exec_() def __showSource(self): """ Private slot to show the source of a traceback in an eric5 editor. """ if not self.dbs: return # get the error info tracebackLines = self.dlg.traceback.toPlainText().splitlines() # find the last entry matching the pattern for index in range(len(tracebackLines) - 1, -1, -1): fmatch = re.search(r'File "(.*?)", line (\d*?),.*', tracebackLines[index]) if fmatch: break if fmatch: fn, ln = fmatch.group(1, 2) self.unittestFile.emit(fn, int(ln), 1) def hasFailedTests(self): """ Public method to check, if there are failed tests from the last run. @return flag indicating the presence of failed tests (boolean) """ return bool(self.__failedTests) class QtTestResult(unittest.TestResult): """ A TestResult derivative to work with a graphical GUI. For more details see pyunit.py of the standard python distribution. """ def __init__(self, parent): """ Constructor @param parent The parent widget. """ super().__init__() self.parent = parent def addFailure(self, test, err): """ Method called if a test failed. @param test reference to the test object @param err error traceback """ super().addFailure(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testFailed(str(test), tracebackLines, test.id()) def addError(self, test, err): """ Method called if a test errored. @param test reference to the test object @param err error traceback """ super().addError(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testErrored(str(test), tracebackLines, test.id()) def addSkip(self, test, reason): """ Method called if a test was skipped. @param test reference to the test object @param reason reason for skipping the test (string) """ super().addSkip(test, reason) self.parent.testSkipped(str(test), reason, test.id()) def addExpectedFailure(self, test, err): """ Method called if a test failed expected. @param test reference to the test object @param err error traceback """ super().addExpectedFailure(test, err) tracebackLines = self._exc_info_to_string(err, test) self.parent.testFailedExpected(str(test), tracebackLines, test.id()) def addUnexpectedSuccess(self, test): """ Method called if a test succeeded expectedly. @param test reference to the test object """ super().addUnexpectedSuccess(test) self.parent.testSucceededUnexpected(str(test), test.id()) def startTest(self, test): """ Method called at the start of a test. @param test Reference to the test object """ super().startTest(test) self.parent.testStarted(str(test), test.shortDescription()) def stopTest(self, test): """ Method called at the end of a test. @param test Reference to the test object """ super().stopTest(test) self.parent.testFinished() class UnittestWindow(QMainWindow): """ Main window class for the standalone dialog. """ def __init__(self, prog=None, parent=None): """ Constructor @param prog filename of the program to open @param parent reference to the parent widget (QWidget) """ super().__init__(parent) self.cw = UnittestDialog(prog=prog, parent=self) self.cw.installEventFilter(self) size = self.cw.size() self.setCentralWidget(self.cw) self.resize(size) def eventFilter(self, obj, event): """ Public method to filter events. @param obj reference to the object the event is meant for (QObject) @param event reference to the event object (QEvent) @return flag indicating, whether the event was handled (boolean) """ if event.type() == QEvent.Close: QApplication.exit() return True return False