PyUnit/UnittestDialog.py

Sun, 24 Mar 2019 19:18:56 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 24 Mar 2019 19:18:56 +0100
changeset 6899
8c4cf9c405c7
parent 6897
701256697721
child 6900
060a30488316
permissions
-rw-r--r--

UnittestDialog: started implementing pure discovery support.

# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the UI to the pyunit package.
"""

from __future__ import unicode_literals

import unittest
import sys
import time
import re
import os

from PyQt5.QtCore import pyqtSignal, QEvent, Qt, pyqtSlot
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import QWidget, QDialog, QApplication, QDialogButtonBox, \
    QListWidgetItem, QComboBox, QTreeWidgetItem

from E5Gui.E5Application import e5App
from E5Gui import E5MessageBox
from E5Gui.E5MainWindow import E5MainWindow
from E5Gui.E5PathPicker import E5PathPickerModes

from .Ui_UnittestDialog import Ui_UnittestDialog

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, parent=None, name=None):
        """
        Constructor
        
        @param prog filename of the program to open
        @type str
        @param dbs reference to the debug server object. It is an indication
            whether we were called from within the eric6 IDE.
        @type DebugServer
        @param ui reference to the UI object
        @type UserInterface
        @param parent parent widget of this dialog
        @type QWidget
        @param name name of this dialog
        @type str
        """
        super(UnittestDialog, self).__init__(parent)
        if name:
            self.setObjectName(name)
        self.setupUi(self)
        
        self.testsuitePicker.setMode(E5PathPickerModes.OpenFileMode)
        self.testsuitePicker.setInsertPolicy(QComboBox.InsertAtTop)
        self.testsuitePicker.setSizeAdjustPolicy(
            QComboBox.AdjustToMinimumContentsLength)
        
        self.discoveryPicker.setMode(E5PathPickerModes.DirectoryMode)
        self.discoveryPicker.setInsertPolicy(QComboBox.InsertAtTop)
        self.discoveryPicker.setSizeAdjustPolicy(
            QComboBox.AdjustToMinimumContentsLength)
        
        # TODO: add a "Discover" button enabled upon selection of 'auto-discovery'
        self.discoverButton = self.buttonBox.addButton(
            self.tr("Discover"), QDialogButtonBox.ActionRole)
        self.discoverButton.setToolTip(self.tr(
            "Discover tests"))
        self.discoverButton.setWhatsThis(self.tr(
            """<b>Discover</b>"""
            """<p>This button starts a discovery of available tests.</p>"""))
        self.startButton = self.buttonBox.addButton(
            self.tr("Start"), QDialogButtonBox.ActionRole)
        self.startButton.setToolTip(self.tr(
            "Start the selected testsuite"))
        self.startButton.setWhatsThis(self.tr(
            """<b>Start Test</b>"""
            """<p>This button starts the selected testsuite.</p>"""))
        self.startFailedButton = self.buttonBox.addButton(
            self.tr("Rerun Failed"), QDialogButtonBox.ActionRole)
        self.startFailedButton.setToolTip(
            self.tr("Reruns failed tests of the selected testsuite"))
        self.startFailedButton.setWhatsThis(self.tr(
            """<b>Rerun Failed</b>"""
            """<p>This button reruns all failed tests of the selected"""
            """ testsuite.</p>"""))
        self.stopButton = self.buttonBox.addButton(
            self.tr("Stop"), QDialogButtonBox.ActionRole)
        self.stopButton.setToolTip(self.tr("Stop the running unittest"))
        self.stopButton.setWhatsThis(self.tr(
            """<b>Stop Test</b>"""
            """<p>This button stops a running unittest.</p>"""))
        self.discoverButton.setEnabled(False)
        self.stopButton.setEnabled(False)
        self.startButton.setDefault(True)
        self.startFailedButton.setEnabled(False)
        
        self.__dbs = dbs
        self.__forProject = False
        
        self.setWindowFlags(
            self.windowFlags() | Qt.WindowFlags(
                Qt.WindowContextHelpButtonHint))
        self.setWindowIcon(UI.PixmapCache.getIcon("eric.png"))
        self.setWindowTitle(self.tr("Unittest"))
        if dbs:
            self.ui = ui
            
            # virtual environment manager is only used in the integrated
            # variant
            self.__venvManager = e5App().getObject("VirtualEnvManager")
            self.__populateVenvComboBox()
            self.__venvManager.virtualEnvironmentAdded.connect(
                self.__populateVenvComboBox)
            self.__venvManager.virtualEnvironmentRemoved.connect(
                self.__populateVenvComboBox)
            self.__venvManager.virtualEnvironmentChanged.connect(
                self.__populateVenvComboBox)
        else:
            self.__venvManager = None
        self.venvComboBox.setVisible(bool(self.__venvManager))
        self.venvLabel.setVisible(bool(self.__venvManager))
        
        self.__setProgressColor("green")
        self.progressLed.setDarkFactor(150)
        self.progressLed.off()
        
        self.discoverHistory = []
        self.fileHistory = []
        self.testNameHistory = []
        self.running = False
        self.savedModulelist = None
        self.savedSysPath = sys.path
        self.savedCwd = os.getcwd()
        if prog:
            self.insertProg(prog)
        
        self.rxPatterns = [
            self.tr("^Failure: "),
            self.tr("^Error: "),
            # These are for untranslated/partially translated situations
            "^Failure: ",
            "^Error: ",
        ]
        
        self.__failedTests = []
        
        # now connect the debug server signals if called from the eric6 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)
        
        self.__editors = []
    
    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.__dbs:
            self.close()
    
    def __populateVenvComboBox(self):
        """
        Private method to (re-)populate the virtual environments selector.
        """
        currentText = self.venvComboBox.currentText()
        self.venvComboBox.clear()
        self.venvComboBox.addItem("")
        self.venvComboBox.addItems(
            sorted(self.__venvManager.getVirtualenvNames()))
        index = self.venvComboBox.findText(currentText)
        if index < 0:
            index = 0
        self.venvComboBox.setCurrentIndex(index)
    
    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 setProjectMode(self, forProject):
        """
        Public method to set the project mode of the dialog.
        
        @param forProject flag indicating to run for the open project
        @type bool
        """
        self.__forProject = forProject
        if forProject:
            project = e5App().getObject("Project")
            if project.isOpen():
                self.insertDiscovery(project.getProjectPath())
            else:
                self.insertDiscovery("")
        else:
            self.insertDiscovery("")
    
    def insertDiscovery(self, start):
        """
        Public slot to insert the discovery start directory into the
        discoveryPicker object.
        
        @param start start directory name to be inserted
        @type str
        """
        # prepend the given directory to the discovery picker
        if start is None:
            start = ""
        if start in self.discoverHistory:
            self.discoverHistory.remove(start)
        self.discoverHistory.insert(0, start)
        self.discoveryPicker.clear()
        self.discoveryPicker.addItems(self.discoverHistory)
    
    def insertProg(self, prog):
        """
        Public slot to insert the filename prog into the testsuitePicker
        object.
        
        @param prog filename to be inserted (string)
        """
        # prepend the selected file to the testsuite picker
        if prog is None:
            prog = ""
        if prog in self.fileHistory:
            self.fileHistory.remove(prog)
        self.fileHistory.insert(0, prog)
        self.testsuitePicker.clear()
        self.testsuitePicker.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_testsuitePicker_aboutToShowPathPickerDialog(self):
        """
        Private slot called before the test suite selection dialog is shown.
        """
        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')])
            fileFilter = self.tr(
                "Python3 Files ({1});;Python2 Files ({0});;All Files (*)")\
                .format(py2Extensions, py3Extensions)
        else:
            fileFilter = self.tr("Python Files (*.py);;All Files (*)")
        self.testsuitePicker.setFilters(fileFilter)
        
        defaultDirectory = Preferences.getMultiProject("Workspace")
        if not defaultDirectory:
            defaultDirectory = os.path.expanduser("~")
        if self.__dbs:
            project = e5App().getObject("Project")
            if self.__forProject and project.isOpen():
                defaultDirectory = project.getProjectPath()
        self.testsuitePicker.setDefaultDirectory(defaultDirectory)
    
    @pyqtSlot(str)
    def on_testsuitePicker_pathSelected(self, suite):
        """
        Private slot called after a test suite has been selected.
        
        @param suite file name of the test suite
        @type str
        """
        self.insertProg(suite)
    
    @pyqtSlot(str)
    def on_testsuitePicker_editTextChanged(self, path):
        """
        Private slot handling changes of the test suite path.
        
        @param path path of the test suite file
        @type str
        """
        self.startFailedButton.setEnabled(False)
    
    @pyqtSlot(bool)
    def on_discoverCheckBox_toggled(self, checked):
        """
        Private slot handling state changes of the 'discover' checkbox.
        
        @param checked state of the checkbox
        @type bool
        """
        self.discoverButton.setEnabled(checked)
        if not bool(self.discoveryPicker.currentText()):
            if self.__forProject:
                project = e5App().getObject("Project")
                if project.isOpen():
                    self.insertDiscovery(project.getProjectPath())
                    return
            
            self.insertDiscovery(Preferences.getMultiProject("Workspace"))
    
    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.discoverButton:
            self.__discover()
        elif button == self.startButton:
            self.startTests()
        elif button == self.stopButton:
            self.__stopTests()
        elif button == self.startFailedButton:
            self.startTests(failedOnly=True)
    
    @pyqtSlot()
    def __discover(self):
        """
        Private slot to discover unit test but don't run them.
        """
        if self.running:
            return
        
        self.discoveryList.clear()
        
        discoveryStart = self.discoveryPicker.currentText()
        self.sbLabel.setText(self.tr("Discovering Tests"))
        QApplication.processEvents()
        
        self.testName = self.tr("Unittest with auto-discovery")
        if self.__dbs:
            # TODO: implement this later
            pass
        else:
            # we are running as an application
            if not discoveryStart:
                E5MessageBox.critical(
                    self,
                    self.tr("Unittest"),
                    self.tr("You must enter a start directory for"
                            " auto-discovery."))
                return
            
            # 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 discover the testsuite
            os.chdir(discoveryStart)
            try:
                test = unittest.defaultTestLoader.discover(discoveryStart)
                if test:
                    testsList = self.__assembleTestCasesList(test)
                    self.__populateDiscoveryResults(testsList)
                    self.sbLabel.setText(
                        self.tr("Discovered %n Test(s)", "", len(testsList)))
                    self.tabWidget.setCurrentIndex(0)
            except Exception:
                exc_type, exc_value, exc_tb = sys.exc_info()
                E5MessageBox.critical(
                    self,
                    self.tr("Unittest"),
                    self.tr(
                        "<p>Unable to discover tests.<br/>"
                        "{0}<br/>{1}</p>")
                    .format(str(exc_type), str(exc_value)))
    
    def __assembleTestCasesList(self, suite):
        """
        Private method to assemble a list of test cases included in a test
        suite.
        
        @param suite test suite to be inspected
        @type unittest.TestSuite
        @return list of tuples containing the test case ID and short
            description
        @rtype list of tuples of (str, str)
        """
        testCases = []
        for test in suite:
            if isinstance(test, unittest.TestSuite):
                testCases.extend(self.__assembleTestCasesList(test))
            else:
                testCases.append((test.id(), test.shortDescription()))
        return testCases
    
    def __findDiscoveryItem(self, modulePath):
        """
        Private method to find an item given the module path.
        
        @param modulePath path of the module in dotted notation
        @type str
        @return reference to the item or None
        @rtype QTreeWidgetItem or None
        """
        itm = self.discoveryList.topLevelItem(0)
        while itm is not None:
            if itm.data(0, Qt.UserRole) == modulePath:
                return itm
            itm = self.discoveryList.itemBelow(itm)
        
        return None
    
    def __populateDiscoveryResults(self, tests):
        """
        Private method to populate the test discovery results list.
        
        @param tests list of tuples containing the discovery results
        @type list of tuples of (str, str)
        """
        for test, _testDescription in tests:
            testPath = test.split(".")
            pitm = None
            for index in range(1, len(testPath) + 1):
                modulePath = ".".join(testPath[:index])
                itm = self.__findDiscoveryItem(modulePath)
                if itm is not None:
                    pitm = itm
                else:
                    if pitm is None:
                        itm = QTreeWidgetItem(self.discoveryList,
                                              [testPath[index - 1]])
                    else:
                        itm = QTreeWidgetItem(pitm,
                                              [testPath[index - 1]])
                        pitm.setExpanded(True)
                    itm.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
                    itm.setCheckState(0, Qt.Unchecked)
                    itm.setData(0, Qt.UserRole, modulePath)
                    pitm = itm
    
    @pyqtSlot()
    def startTests(self, failedOnly=False):
        """
        Public slot to start the test.
        
        @keyparam failedOnly flag indicating to run only failed tests (boolean)
        """
        if self.running:
            return
        
        discover = self.discoverCheckBox.isChecked()
        if discover:
            discoveryStart = self.discoveryPicker.currentText()
            testFileName = ""
            testName = ""
        else:
            discoveryStart = ""
            testFileName = self.testsuitePicker.currentText()
            testName = self.testComboBox.currentText()
            if testName:
                self.insertTestName(testName)
            if testFileName and not testName:
                testName = "suite"
        
        if not discover and not testFileName and not testName:
            E5MessageBox.critical(
                self,
                self.tr("Unittest"),
                self.tr("You must select auto-discovery or enter a test suite"
                        " file or a dotted test name."))
            return
        
        # prepend the selected file to the testsuite combobox
        self.insertProg(testFileName)
        self.sbLabel.setText(self.tr("Preparing Testsuite"))
        QApplication.processEvents()
        
        if discover:
            self.testName = self.tr("Unittest with auto-discovery")
        else:
            # build the module name from the filename without extension
            if testFileName:
                self.testName = os.path.splitext(
                    os.path.basename(testFileName))[0]
            elif testName:
                self.testName = testName
            else:
                self.testName = self.tr("<Unnamed Test>")
        
        if self.__dbs:
            venvName = self.venvComboBox.currentText()
            
            # we are cooperating with the eric6 IDE
            project = e5App().getObject("Project")
            if self.__forProject:
                mainScript = os.path.abspath(project.getMainScript(True))
                clientType = project.getProjectLanguage()
                if mainScript:
                    workdir = os.path.dirname(mainScript)
                else:
                    workdir = project.getProjectPath()
                sysPath = [workdir]
                coverageFile = os.path.splitext(mainScript)[0]
                if discover and not discoveryStart:
                    discoveryStart = workdir
            else:
                if discover:
                    if not discoveryStart:
                        E5MessageBox.critical(
                            self,
                            self.tr("Unittest"),
                            self.tr("You must enter a start directory for"
                                    " auto-discovery."))
                        return
                    
                    coverageFile = os.path.join(discoveryStart, "unittest")
                    workdir = ""
                    clientType = \
                        self.__venvManager.getVirtualenvVariant(venvName)
                    if not clientType:
                        # assume Python 3
                        clientType = "Python3"
                elif testFileName:
                    mainScript = os.path.abspath(testFileName)
                    flags = Utilities.extractFlagsFromFile(mainScript)
                    workdir = os.path.dirname(mainScript)
                    if mainScript.endswith(
                        tuple(Preferences.getPython("PythonExtensions"))) or \
                       ("FileType" in flags and
                            flags["FileType"] in ["Python", "Python2"]):
                        clientType = "Python2"
                    else:
                        # if it is not Python2 it must be Python3!
                        clientType = "Python3"
                    coverageFile = os.path.splitext(mainScript)[0]
                else:
                    coverageFile = os.path.abspath("unittest")
                    workdir = ""
                    clientType = \
                        self.__venvManager.getVirtualenvVariant(venvName)
                    if not clientType:
                        # assume Python 3
                        clientType = "Python3"
                sysPath = []
            if failedOnly and self.__failedTests:
                failed = self.__failedTests[:]
                if discover:
                    workdir = discoveryStart
                    discover = False
            else:
                failed = []
            self.__failedTests = []
            self.__dbs.remoteUTPrepare(
                testFileName, self.testName, testName, failed,
                self.coverageCheckBox.isChecked(), coverageFile,
                self.coverageEraseCheckBox.isChecked(), clientType=clientType,
                forProject=self.__forProject, workdir=workdir,
                venvName=venvName, syspath=sysPath,
                discover=discover, discoveryStart=discoveryStart)
        else:
            # we are running as an application
            if discover and not discoveryStart:
                E5MessageBox.critical(
                    self,
                    self.tr("Unittest"),
                    self.tr("You must enter a start directory for"
                            " auto-discovery."))
                return
            
            if testFileName:
                sys.path = [os.path.dirname(os.path.abspath(testFileName))] + \
                    self.savedSysPath
            elif discoveryStart:
                sys.path = [os.path.abspath(discoveryStart)] + \
                    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()
            
            os.chdir(self.savedCwd)
            
            # now try to generate the testsuite
            try:
                if failedOnly and self.__failedTests:
                    failed = self.__failedTests[:]
                    if discover:
                        os.chdir(discoveryStart)
                        discover = False
                else:
                    failed = []
                if discover:
                    test = unittest.defaultTestLoader.discover(discoveryStart)
                else:
                    if testFileName:
                        module = __import__(self.testName)
                    else:
                        module = None
                    if failedOnly and self.__failedTests:
                        if module:
                            failed = [t.split(".", 1)[1]
                                      for t in self.__failedTests]
                        else:
                            failed = self.__failedTests[:]
                        test = unittest.defaultTestLoader.loadTestsFromNames(
                            failed, module)
                    else:
                        test = unittest.defaultTestLoader.loadTestsFromName(
                            testName, module)
            except Exception:
                exc_type, exc_value, exc_tb = sys.exc_info()
                E5MessageBox.critical(
                    self,
                    self.tr("Unittest"),
                    self.tr(
                        "<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 discover:
                    covname = os.path.join(discoveryStart, "unittest")
                elif testFileName:
                    covname = \
                        os.path.splitext(os.path.abspath(testFileName))[0]
                else:
                    covname = "unittest"
                
                from DebugClients.Python.coverage import coverage
                cover = coverage(data_file="{0}.coverage".format(covname))
                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.tr("Unittest"),
                self.tr(
                    "<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 __stopTests(self):
        """
        Private slot to stop the test.
        """
        if self.__dbs:
            self.__dbs.remoteUTStop()
        elif self.testResult:
            self.testResult.stop()
    
    def on_errorsListWidget_currentTextChanged(self, text):
        """
        Private slot to handle the highlighted signal.
        
        @param text current text (string)
        """
        if text:
            for pattern in self.rxPatterns:
                text = re.sub(pattern, "", text)
            
            foundItems = self.testsListWidget.findItems(
                text, Qt.MatchFlags(Qt.MatchExactly))
            if len(foundItems) > 0:
                itm = foundItems[0]
                self.testsListWidget.setCurrentItem(itm)
                self.testsListWidget.scrollToItem(itm)
    
    def __setRunningMode(self):
        """
        Private method to set the GUI in running mode.
        """
        self.running = True
        self.tabWidget.setCurrentIndex(1)
        
        # 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.startFailedButton.setEnabled(False)
        self.stopButton.setDefault(True)
        
        self.sbLabel.setText(self.tr("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
        
        failedAvailable = bool(self.__failedTests)
        self.startButton.setEnabled(True)
        self.startFailedButton.setEnabled(failedAvailable)
        self.stopButton.setEnabled(False)
        if failedAvailable:
            self.startFailedButton.setDefault(True)
            self.startButton.setDefault(False)
        else:
            self.startFailedButton.setDefault(False)
            self.startButton.setDefault(True)
        self.sbLabel.setText(
            self.tr("Ran %n test(s) in {0:.3f}s", "", self.runCount)
            .format(self.timeTaken))
        self.progressLed.off()
        
        self.unittestStopped.emit()
    
    def testFailed(self, test, exc, testId):
        """
        Public method called if a test fails.
        
        @param test name of the test (string)
        @param exc string representation of the exception (string)
        @param testId id of the test (string)
        """
        self.failCount += 1
        self.progressCounterFailureCount.setText(str(self.failCount))
        itm = QListWidgetItem(self.tr("Failure: {0}").format(test))
        itm.setData(Qt.UserRole, (test, exc))
        self.errorsListWidget.insertItem(0, itm)
        self.__failedTests.append(testId)
    
    def testErrored(self, test, exc, testId):
        """
        Public method called if a test errors.
        
        @param test name of the test (string)
        @param exc string representation of the exception (string)
        @param testId id of the test (string)
        """
        self.errorCount += 1
        self.progressCounterErrorCount.setText(str(self.errorCount))
        itm = QListWidgetItem(self.tr("Error: {0}").format(test))
        itm.setData(Qt.UserRole, (test, exc))
        self.errorsListWidget.insertItem(0, itm)
        self.__failedTests.append(testId)
    
    def testSkipped(self, test, reason, testId):
        """
        Public method called if a test was skipped.
        
        @param test name of the test (string)
        @param reason reason for skipping the test (string)
        @param testId id of the test (string)
        """
        self.skippedCount += 1
        self.progressCounterSkippedCount.setText(str(self.skippedCount))
        itm = QListWidgetItem(self.tr("    Skipped: {0}").format(reason))
        itm.setForeground(Qt.blue)
        self.testsListWidget.insertItem(1, itm)
    
    def testFailedExpected(self, test, exc, testId):
        """
        Public method called if a test fails expectedly.
        
        @param test name of the test (string)
        @param exc string representation of the exception (string)
        @param testId id of the test (string)
        """
        self.expectedFailureCount += 1
        self.progressCounterExpectedFailureCount.setText(
            str(self.expectedFailureCount))
        itm = QListWidgetItem(self.tr("    Expected Failure"))
        itm.setForeground(Qt.blue)
        self.testsListWidget.insertItem(1, itm)
    
    def testSucceededUnexpected(self, test, testId):
        """
        Public method called if a test succeeds unexpectedly.
        
        @param test name of the test (string)
        @param testId id of the test (string)
        """
        self.unexpectedSuccessCount += 1
        self.progressCounterUnexpectedSuccessCount.setText(
            str(self.unexpectedSuccessCount))
        itm = QListWidgetItem(self.tr("    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:
            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
        from .Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog
        self.dlg = QDialog(self)
        ui = Ui_UnittestStacktraceDialog()
        ui.setupUi(self.dlg)
        self.dlg.traceback = ui.traceback
        
        ui.showButton = ui.buttonBox.addButton(
            self.tr("Show Source"), QDialogButtonBox.ActionRole)
        ui.showButton.clicked.connect(self.__showSource)
        
        ui.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
        
        self.dlg.setWindowTitle(text)
        ui.testLabel.setText(test)
        ui.traceback.setPlainText(tracebackText)
        
        # 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 eric6 editor.
        """
        # 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)
            if self.__dbs:
                # running as part of eric IDE
                self.unittestFile.emit(fn, int(ln), 1)
            else:
                self.__openEditor(fn, int(ln))
    
    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)
    
    def __openEditor(self, filename, linenumber):
        """
        Private method to open an editor window for the given file.
        
        Note: This method opens an editor window when the unittest dialog
        is called as a standalone application.
        
        @param filename path of the file to be opened
        @type str
        @param linenumber line number to place the cursor at
        @type int
        """
        from QScintilla.MiniEditor import MiniEditor
        editor = MiniEditor(filename, "Python3", self)
        editor.gotoLine(linenumber)
        editor.show()
        
        self.__editors.append(editor)
    
    def closeEvent(self, event):
        """
        Protected method to handle the close event.
        
        @param event close event
        @type QCloseEvent
        """
        event.accept()
        
        for editor in self.__editors:
            try:
                editor.close()
            except Exception:
                # ignore all exceptions
                pass


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(QtTestResult, self).__init__()
        self.parent = parent
    
    def addFailure(self, test, err):
        """
        Public method called if a test failed.
        
        @param test reference to the test object
        @param err error traceback
        """
        super(QtTestResult, self).addFailure(test, err)
        tracebackLines = self._exc_info_to_string(err, test)
        self.parent.testFailed(str(test), tracebackLines, test.id())
    
    def addError(self, test, err):
        """
        Public method called if a test errored.
        
        @param test reference to the test object
        @param err error traceback
        """
        super(QtTestResult, self).addError(test, err)
        tracebackLines = self._exc_info_to_string(err, test)
        self.parent.testErrored(str(test), tracebackLines, test.id())
    
    def addSkip(self, test, reason):
        """
        Public method called if a test was skipped.
        
        @param test reference to the test object
        @param reason reason for skipping the test (string)
        """
        super(QtTestResult, self).addSkip(test, reason)
        self.parent.testSkipped(str(test), reason, test.id())
    
    def addExpectedFailure(self, test, err):
        """
        Public method called if a test failed expected.
        
        @param test reference to the test object
        @param err error traceback
        """
        super(QtTestResult, self).addExpectedFailure(test, err)
        tracebackLines = self._exc_info_to_string(err, test)
        self.parent.testFailedExpected(str(test), tracebackLines, test.id())
    
    def addUnexpectedSuccess(self, test):
        """
        Public method called if a test succeeded expectedly.
        
        @param test reference to the test object
        """
        super(QtTestResult, self).addUnexpectedSuccess(test)
        self.parent.testSucceededUnexpected(str(test), test.id())
    
    def startTest(self, test):
        """
        Public method called at the start of a test.
        
        @param test Reference to the test object
        """
        super(QtTestResult, self).startTest(test)
        self.parent.testStarted(str(test), test.shortDescription())

    def stopTest(self, test):
        """
        Public method called at the end of a test.
        
        @param test Reference to the test object
        """
        super(QtTestResult, self).stopTest(test)
        self.parent.testFinished()


class UnittestWindow(E5MainWindow):
    """
    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(UnittestWindow, self).__init__(parent)
        self.cw = UnittestDialog(prog, parent=self)
        self.cw.installEventFilter(self)
        size = self.cw.size()
        self.setCentralWidget(self.cw)
        self.resize(size)
        
        self.setStyle(Preferences.getUI("Style"),
                      Preferences.getUI("StyleSheet"))
        
        self.cw.buttonBox.accepted.connect(self.close)
        self.cw.buttonBox.rejected.connect(self.close)
    
    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