PyUnit/UnittestDialog.py

changeset 0
de9c2efb9d02
child 12
1d8dd9706f46
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PyUnit/UnittestDialog.py	Mon Dec 28 16:03:33 2009 +0000
@@ -0,0 +1,595 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the UI to the pyunit package.
+"""
+
+import unittest
+import sys
+import traceback
+import time
+import re
+import os
+
+from PyQt4.QtCore import *
+from PyQt4.QtGui import *
+
+from E4Gui.E4Application import e4App
+from E4Gui.E4Completers import E4FileCompleter
+
+from Ui_UnittestDialog import Ui_UnittestDialog
+from Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog
+
+from DebugClients.Python.coverage import coverage
+
+import UI.PixmapCache
+
+import Utilities
+
+class UnittestDialog(QWidget, Ui_UnittestDialog):
+    """
+    Class implementing the UI to the pyunit package.
+    
+    @signal unittestFile(string,int,int) emitted to show the source of a unittest file
+    """
+    def __init__(self,prog = None,dbs = None,ui = None,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 eric4 IDE
+        @param ui reference to the UI object
+        @param parent parent widget of this dialog (QWidget)
+        @param name name of this dialog (string)
+        """
+        QWidget.__init__(self,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.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.dbs = dbs
+        
+        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 = E4FileCompleter(self.testsuiteComboBox)
+        
+        self.fileHistory = []
+        self.testNameHistory = []
+        self.running = False
+        self.savedModulelist = None
+        self.savedSysPath = sys.path
+        if prog:
+            self.insertProg(prog)
+        
+        self.rx1 = self.trUtf8("^Failure: ")
+        self.rx2 = self.trUtf8("^Error: ")
+        
+        # now connect the debug server signals if called from the eric4 IDE
+        if self.dbs:
+            self.connect(self.dbs, SIGNAL('utPrepared'),
+                self.__UTPrepared)
+            self.connect(self.dbs, SIGNAL('utFinished'),
+                self.__setStoppedMode)
+            self.connect(self.dbs, SIGNAL('utStartTest'),
+                self.testStarted)
+            self.connect(self.dbs, SIGNAL('utStopTest'),
+                self.testFinished)
+            self.connect(self.dbs, SIGNAL('utTestFailed'),
+                self.testFailed)
+            self.connect(self.dbs, SIGNAL('utTestErrored'),
+                self.testErrored)
+        
+    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:
+            pyExtensions = \
+                ' '.join(["*%s" % ext for ext in self.dbs.getExtensions('Python')])
+            py3Extensions = \
+                ' '.join(["*%s" % ext for ext in self.dbs.getExtensions('Python3')])
+            filter = self.trUtf8("Python Files ({0});;Python3 Files ({1});;All Files (*)")\
+                .format(pyExtensions, py3Extensions)
+        else:
+            filter = self.trUtf8("Python Files (*.py);;All Files (*)")
+        prog = QFileDialog.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("Python3")
+            if txt.endswith(exts):
+                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()
+        
+    @pyqtSlot()
+    def on_startButton_clicked(self):
+        """
+        Public slot to start the test.
+        """
+        if self.running:
+            return
+        
+        prog = self.testsuiteComboBox.currentText()
+        if not prog:
+            QMessageBox.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() or "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 eric4 IDE
+            project = e4App().getObject("Project")
+            if project.isOpen() and project.isProjectSource(prog):
+                mainScript = project.getMainScript(True)
+            else:
+                mainScript = os.path.abspath(prog)
+            self.dbs.remoteUTPrepare(prog, self.testName, testFunctionName,
+                self.coverageCheckBox.isChecked(), mainScript,
+                self.coverageEraseCheckBox.isChecked())
+        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 sys.modules:
+                    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:
+                    test = unittest.defaultTestLoader.loadTestsFromName(\
+                        testFunctionName, module)
+                except AttributeError:
+                    test = unittest.defaultTestLoader.loadTestsFromModule(module)
+            except:
+                exc_type, exc_value, exc_tb = sys.exc_info()
+                QMessageBox.critical(self, 
+                        self.trUtf8("Unittest"),
+                        self.trUtf8("<p>Unable to run test <b>{0}</b>.<br>{1}<br>{2}</p>")
+                            .format(self.testName, unicode(exc_type), unicode(exc_value)))
+                return
+                
+            # now set up the coverage stuff
+            if self.coverageCheckBox.isChecked():
+                if self.dbs:
+                    # we are cooperating with the eric4 IDE
+                    project = e4App().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 = "%s.coverage" % 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.__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:
+            QMessageBox.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:
+            text = re.sub(self.rx1, "", text)
+            text = re.sub(self.rx2, "", 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.remainingCount = self.totalTests
+        self.errorInfo = []
+
+        # reset the GUI
+        self.progressCounterRunCount.setText(str(self.runCount))
+        self.progressCounterFailureCount.setText(str(self.failCount))
+        self.progressCounterErrorCount.setText(str(self.errorCount))
+        self.progressCounterRemCount.setText(str(self.remainingCount))
+        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.stopButton.setEnabled(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()
+
+    def testFailed(self, test, exc):
+        """
+        Public method called if a test fails.
+        
+        @param test name of the failed test (string)
+        @param exc string representation of the exception (list of strings)
+        """
+        self.failCount += 1
+        self.progressCounterFailureCount.setText(str(self.failCount))
+        self.errorsListWidget.insertItem(0, self.trUtf8("Failure: {0}").format(test))
+        self.errorInfo.insert(0, (test, exc))
+        
+    def testErrored(self, test, exc):
+        """
+        Public method called if a test errors.
+        
+        @param test name of the failed test (string)
+        @param exc string representation of the exception (list of strings)
+        """
+        self.errorCount += 1
+        self.progressCounterErrorCount.setText(str(self.errorCount))
+        self.errorsListWidget.insertItem(0, self.trUtf8("Error: {0}").format(test))
+        self.errorInfo.insert(0, (test, exc))
+        
+    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, "    %s" % 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()
+
+        # get the error info
+        test, tracebackLines = self.errorInfo[self.errListIndex]
+        tracebackText = "".join(tracebackLines)
+
+        # now build the dialog
+        self.dlg = QDialog()
+        ui = Ui_UnittestStacktraceDialog()
+        ui.setupUi(self.dlg)
+        
+        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:
+            self.dlg.connect(ui.showButton, SIGNAL("clicked()"),
+                            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 eric4 editor.
+        """
+        if not self.dbs:
+            return
+            
+        # get the error info
+        test, tracebackLines = self.errorInfo[self.errListIndex]
+        # 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.emit(SIGNAL('unittestFile'), fn, int(ln), 1)
+
+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.
+        """
+        unittest.TestResult.__init__(self)
+        self.parent = parent
+        
+    def addFailure(self, test, err):
+        """
+        Method called if a test failed.
+        
+        @param test Reference to the test object
+        @param err The error traceback
+        """
+        unittest.TestResult.addFailure(self, test, err)
+        tracebackLines = apply(traceback.format_exception, err + (10,))
+        self.parent.testFailed(unicode(test), tracebackLines)
+        
+    def addError(self, test, err):
+        """
+        Method called if a test errored.
+        
+        @param test Reference to the test object
+        @param err The error traceback
+        """
+        unittest.TestResult.addError(self, test, err)
+        tracebackLines = apply(traceback.format_exception, err + (10,))
+        self.parent.testErrored(unicode(test), tracebackLines)
+        
+    def startTest(self, test):
+        """
+        Method called at the start of a test.
+        
+        @param test Reference to the test object
+        """
+        unittest.TestResult.startTest(self, test)
+        self.parent.testStarted(unicode(test), test.shortDescription())
+
+    def stopTest(self, test):
+        """
+        Method called at the end of a test.
+        
+        @param test Reference to the test object
+        """
+        unittest.TestResult.stopTest(self, 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)
+        """
+        QMainWindow.__init__(self, 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

eric ide

mercurial