eric7/Testing/TestingWidget.py

branch
unittest
changeset 9066
a219ade50f7c
parent 9065
39405e6eba20
child 9070
eab09a1ab8ce
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Testing/TestingWidget.py	Mon May 16 19:46:51 2022 +0200
@@ -0,0 +1,1024 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a widget to orchestrate unit test execution.
+"""
+
+import contextlib
+import enum
+import locale
+import os
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QCoreApplication
+from PyQt6.QtWidgets import (
+    QAbstractButton, QComboBox, QDialogButtonBox, QWidget
+)
+
+from EricWidgets import EricMessageBox
+from EricWidgets.EricApplication import ericApp
+from EricWidgets.EricMainWindow import EricMainWindow
+from EricWidgets.EricPathPicker import EricPathPickerModes
+
+from .Ui_TestingWidget import Ui_TestingWidget
+
+from .TestResultsTree import TestResultsModel, TestResultsTreeView
+from .Interfaces import Frameworks
+from .Interfaces.TestExecutorBase import (
+    TestConfig, TestResult, TestResultCategory
+)
+from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry
+
+import Preferences
+import UI.PixmapCache
+
+from Globals import (
+    recentNameTestDiscoverHistory, recentNameTestFileHistory,
+    recentNameTestNameHistory, recentNameTestFramework,
+    recentNameTestEnvironment
+)
+
+
+class TestingWidgetModes(enum.Enum):
+    """
+    Class defining the various modes of the testing widget.
+    """
+    IDLE = 0            # idle, no test were run yet
+    RUNNING = 1         # test run being performed
+    STOPPED = 2         # test run finished
+
+
+# TODO: add a "Show Coverage" function using PyCoverageDialog
+
+class TestingWidget(QWidget, Ui_TestingWidget):
+    """
+    Class implementing a widget to orchestrate unit test execution.
+    
+    @signal testFile(str, int, bool) emitted to show the source of a
+       test file
+    @signal testRunStopped() emitted after a test run has finished
+    """
+    testFile = pyqtSignal(str, int, bool)
+    testRunStopped = pyqtSignal()
+    
+    def __init__(self, testfile=None, parent=None):
+        """
+        Constructor
+        
+        @param testfile file name of the test to load
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.__resultsModel = TestResultsModel(self)
+        self.__resultsModel.summary.connect(self.__setStatusLabel)
+        self.__resultsTree = TestResultsTreeView(self)
+        self.__resultsTree.setModel(self.__resultsModel)
+        self.__resultsTree.goto.connect(self.__showSource)
+        self.resultsGroupBox.layout().addWidget(self.__resultsTree)
+        
+        self.versionsButton.setIcon(
+            UI.PixmapCache.getIcon("info"))
+        self.clearHistoriesButton.setIcon(
+            UI.PixmapCache.getIcon("clearPrivateData"))
+        
+        self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
+        self.testsuitePicker.setInsertPolicy(
+            QComboBox.InsertPolicy.InsertAtTop)
+        self.testsuitePicker.setSizeAdjustPolicy(
+            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
+        
+        self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
+        self.discoveryPicker.setInsertPolicy(
+            QComboBox.InsertPolicy.InsertAtTop)
+        self.discoveryPicker.setSizeAdjustPolicy(
+            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
+        
+        self.testComboBox.lineEdit().setClearButtonEnabled(True)
+        
+        # create some more dialog buttons for orchestration
+        self.__startButton = self.buttonBox.addButton(
+            self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
+        
+        self.__startButton.setToolTip(self.tr(
+            "Start the selected testsuite"))
+        self.__startButton.setWhatsThis(self.tr(
+            """<b>Start Test</b>"""
+            """<p>This button starts the test run.</p>"""))
+        
+        self.__startFailedButton = self.buttonBox.addButton(
+            self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.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 most recent"""
+            """ test run.</p>"""))
+        
+        self.__stopButton = self.buttonBox.addButton(
+            self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__stopButton.setToolTip(self.tr("Stop the running test"))
+        self.__stopButton.setWhatsThis(self.tr(
+            """<b>Stop Test</b>"""
+            """<p>This button stops a running test.</p>"""))
+        
+        self.setWindowFlags(
+            self.windowFlags() |
+            Qt.WindowType.WindowContextHelpButtonHint
+        )
+        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
+        self.setWindowTitle(self.tr("Testing"))
+        
+        try:
+            # we are called from within the eric IDE
+            self.__venvManager = ericApp().getObject("VirtualEnvManager")
+            self.__project = ericApp().getObject("Project")
+            self.__project.projectOpened.connect(self.__projectOpened)
+            self.__project.projectClosed.connect(self.__projectClosed)
+        except KeyError:
+            # we were called as a standalone application
+            from VirtualEnv.VirtualenvManager import VirtualenvManager
+            self.__venvManager = VirtualenvManager(self)
+            self.__venvManager.virtualEnvironmentAdded.connect(
+                self.__populateVenvComboBox)
+            self.__venvManager.virtualEnvironmentRemoved.connect(
+                self.__populateVenvComboBox)
+            self.__venvManager.virtualEnvironmentChanged.connect(
+                self.__populateVenvComboBox)
+            
+            self.__project = None
+        
+        self.__discoverHistory = []
+        self.__fileHistory = []
+        self.__testNameHistory = []
+        self.__recentFramework = ""
+        self.__recentEnvironment = ""
+        self.__failedTests = []
+        
+        self.__editors = []
+        self.__testExecutor = None
+        
+        # connect some signals
+        self.frameworkComboBox.currentIndexChanged.connect(
+            self.__resetResults)
+        self.discoveryPicker.editTextChanged.connect(
+            self.__resetResults)
+        self.testsuitePicker.editTextChanged.connect(
+            self.__resetResults)
+        self.testComboBox.editTextChanged.connect(
+            self.__resetResults)
+        
+        self.__frameworkRegistry = TestFrameworkRegistry()
+        for framework in Frameworks:
+            self.__frameworkRegistry.register(framework)
+        
+        self.__setIdleMode()
+        
+        self.__loadRecent()
+        self.__populateVenvComboBox()
+        
+        if self.__project and self.__project.isOpen():
+            self.venvComboBox.setCurrentText(self.__project.getProjectVenv())
+            self.frameworkComboBox.setCurrentText(
+                self.__project.getProjectTestingFramework())
+            self.__insertDiscovery(self.__project.getProjectPath())
+        else:
+            self.__insertDiscovery("")
+        
+        self.__insertTestFile(testfile)
+        self.__insertTestName("")
+        
+        self.clearHistoriesButton.clicked.connect(self.clearRecent)
+        
+        self.tabWidget.setCurrentIndex(0)
+    
+    def __populateVenvComboBox(self):
+        """
+        Private method to (re-)populate the virtual environments selector.
+        """
+        currentText = self.venvComboBox.currentText()
+        if not currentText:
+            currentText = self.__recentEnvironment
+        
+        self.venvComboBox.clear()
+        self.venvComboBox.addItem("")
+        self.venvComboBox.addItems(
+            sorted(self.__venvManager.getVirtualenvNames()))
+        self.venvComboBox.setCurrentText(currentText)
+    
+    def __populateTestFrameworkComboBox(self):
+        """
+        Private method to (re-)populate the test framework selector.
+        """
+        currentText = self.frameworkComboBox.currentText()
+        if not currentText:
+            currentText = self.__recentFramework
+        
+        self.frameworkComboBox.clear()
+        
+        if bool(self.venvComboBox.currentText()):
+            interpreter = self.__venvManager.getVirtualenvInterpreter(
+                self.venvComboBox.currentText())
+            self.frameworkComboBox.addItem("")
+            for index, (name, executor) in enumerate(
+                sorted(self.__frameworkRegistry.getFrameworks().items()),
+                start=1
+            ):
+                isInstalled = executor.isInstalled(interpreter)
+                entry = (
+                    name
+                    if isInstalled else
+                    self.tr("{0} (not available)").format(name)
+                )
+                self.frameworkComboBox.addItem(entry)
+                self.frameworkComboBox.model().item(index).setEnabled(
+                    isInstalled)
+            
+            self.frameworkComboBox.setCurrentText(self.__recentFramework)
+    
+    def getResultsModel(self):
+        """
+        Public method to get a reference to the model containing the test
+        result data.
+        
+        @return reference to the test results model
+        @rtype TestResultsModel
+        """
+        return self.__resultsModel
+    
+    def hasFailedTests(self):
+        """
+        Public method to check for failed tests.
+        
+        @return flag indicating the existence of failed tests
+        @rtype bool
+        """
+        return bool(self.__resultsModel.getFailedTests())
+        
+    def getFailedTests(self):
+        """
+        Public method to get the list of failed tests (if any).
+        
+        @return list of IDs of failed tests
+        @rtype list of str
+        """
+        return self.__failedTests[:]
+    
+    @pyqtSlot(str)
+    def __insertHistory(self, widget, history, item):
+        """
+        Private slot to insert an item into a history object.
+        
+        @param widget reference to the widget
+        @type QComboBox or EricComboPathPicker
+        @param history array containing the history
+        @type list of str
+        @param item item to be inserted
+        @type str
+        """
+        # prepend the given directory to the discovery picker
+        if item is None:
+            item = ""
+        if item in history:
+            history.remove(item)
+        history.insert(0, item)
+        widget.clear()
+        widget.addItems(history)
+        widget.setEditText(item)
+    
+    @pyqtSlot(str)
+    def __insertDiscovery(self, start):
+        """
+        Private slot to insert the discovery start directory into the
+        discoveryPicker object.
+        
+        @param start start directory name to be inserted
+        @type str
+        """
+        self.__insertHistory(self.discoveryPicker, self.__discoverHistory,
+                             start)
+    
+    @pyqtSlot(str)
+    def setTestFile(self, testFile):
+        """
+        Public slot to set the given test file as the current one.
+        
+        @param testFile path of the test file
+        @type str
+        """
+        if testFile:
+            self.__insertTestFile(testFile)
+        
+        self.discoverCheckBox.setChecked(not bool(testFile))
+        
+        self.tabWidget.setCurrentIndex(0)
+    
+    @pyqtSlot(str)
+    def __insertTestFile(self, prog):
+        """
+        Private slot to insert a test file name into the testsuitePicker
+        object.
+        
+        @param prog test file name to be inserted
+        @type str
+        """
+        self.__insertHistory(self.testsuitePicker, self.__fileHistory,
+                             prog)
+    
+    @pyqtSlot(str)
+    def __insertTestName(self, testName):
+        """
+        Private slot to insert a test name into the testComboBox object.
+        
+        @param testName name of the test to be inserted
+        @type str
+        """
+        self.__insertHistory(self.testComboBox, self.__testNameHistory,
+                             testName)
+    
+    def __loadRecent(self):
+        """
+        Private method to load the most recently used lists.
+        """
+        Preferences.Prefs.rsettings.sync()
+        
+        # 1. recently selected test framework and virtual environment
+        self.__recentEnvironment = Preferences.Prefs.rsettings.value(
+            recentNameTestEnvironment, "")
+        self.__recentFramework = Preferences.Prefs.rsettings.value(
+            recentNameTestFramework, "")
+        
+        # 2. discovery history
+        self.__discoverHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestDiscoverHistory)
+        if rs is not None:
+            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
+            self.__discoverHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+        
+        # 3. test file history
+        self.__fileHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestFileHistory)
+        if rs is not None:
+            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
+            self.__fileHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+        
+        # 4. test name history
+        self.__testNameHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestNameHistory)
+        if rs is not None:
+            recent = [n for n in Preferences.toList(rs) if n]
+            self.__testNameHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+    
+    def __saveRecent(self):
+        """
+        Private method to save the most recently used lists.
+        """
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestEnvironment, self.__recentEnvironment)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestFramework, self.__recentFramework)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestDiscoverHistory, self.__discoverHistory)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestFileHistory, self.__fileHistory)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestNameHistory, self.__testNameHistory)
+        
+        Preferences.Prefs.rsettings.sync()
+    
+    @pyqtSlot()
+    def clearRecent(self):
+        """
+        Public slot to clear the recently used lists.
+        """
+        # clear histories
+        self.__discoverHistory = []
+        self.__fileHistory = []
+        self.__testNameHistory = []
+        
+        # clear widgets with histories
+        self.discoveryPicker.clear()
+        self.testsuitePicker.clear()
+        self.testComboBox.clear()
+        
+        # sync histories
+        self.__saveRecent()
+    
+    @pyqtSlot()
+    def __resetResults(self):
+        """
+        Private slot to reset the test results tab and data.
+        """
+        self.__totalCount = 0
+        self.__runCount = 0
+        
+        self.progressCounterRunCount.setText("0")
+        self.progressCounterRemCount.setText("0")
+        self.progressProgressBar.setMaximum(100)
+        self.progressProgressBar.setValue(0)
+        
+        self.statusLabel.clear()
+        
+        self.__resultsModel.clear()
+        self.__updateButtonBoxButtons()
+    
+    @pyqtSlot()
+    def __updateButtonBoxButtons(self):
+        """
+        Private slot to update the state of the buttons of the button box.
+        """
+        failedAvailable = bool(self.__resultsModel.getFailedTests())
+        
+        # Start button
+        if self.__mode in (
+            TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
+        ):
+            self.__startButton.setEnabled(
+                bool(self.venvComboBox.currentText()) and
+                bool(self.frameworkComboBox.currentText()) and
+                (
+                    (self.discoverCheckBox.isChecked() and
+                     bool(self.discoveryPicker.currentText())) or
+                    bool(self.testsuitePicker.currentText())
+                )
+            )
+            self.__startButton.setDefault(
+                self.__mode == TestingWidgetModes.IDLE or
+                not failedAvailable
+            )
+        else:
+            self.__startButton.setEnabled(False)
+            self.__startButton.setDefault(False)
+        
+        # Start Failed button
+        self.__startFailedButton.setEnabled(
+            self.__mode == TestingWidgetModes.STOPPED and
+            failedAvailable
+        )
+        self.__startFailedButton.setDefault(
+            self.__mode == TestingWidgetModes.STOPPED and
+            failedAvailable
+        )
+        
+        # Stop button
+        self.__stopButton.setEnabled(
+            self.__mode == TestingWidgetModes.RUNNING)
+        self.__stopButton.setDefault(
+            self.__mode == TestingWidgetModes.RUNNING)
+        
+        # Close button
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Close
+        ).setEnabled(self.__mode in (
+            TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
+        ))
+    
+    @pyqtSlot()
+    def __updateProgress(self):
+        """
+        Private slot update the progress indicators.
+        """
+        self.progressCounterRunCount.setText(
+            str(self.__runCount))
+        self.progressCounterRemCount.setText(
+            str(self.__totalCount - self.__runCount))
+        self.progressProgressBar.setMaximum(self.__totalCount)
+        self.progressProgressBar.setValue(self.__runCount)
+    
+    @pyqtSlot()
+    def __setIdleMode(self):
+        """
+        Private slot to switch the widget to idle mode.
+        """
+        self.__mode = TestingWidgetModes.IDLE
+        self.__updateButtonBoxButtons()
+        self.progressGroupBox.hide()
+        self.tabWidget.setCurrentIndex(0)
+    
+    @pyqtSlot()
+    def __setRunningMode(self):
+        """
+        Private slot to switch the widget to running mode.
+        """
+        self.__mode = TestingWidgetModes.RUNNING
+        
+        self.__totalCount = 0
+        self.__runCount = 0
+        
+        self.__coverageFile = ""
+        # TODO: implement the handling of the 'Show Coverage' button
+        
+        self.sbLabel.setText(self.tr("Running"))
+        self.tabWidget.setCurrentIndex(1)
+        self.__updateButtonBoxButtons()
+        self.__updateProgress()
+        
+        self.progressGroupBox.show()
+    
+    @pyqtSlot()
+    def __setStoppedMode(self):
+        """
+        Private slot to switch the widget to stopped mode.
+        """
+        self.__mode = TestingWidgetModes.STOPPED
+        if self.__totalCount == 0:
+            self.progressProgressBar.setMaximum(100)
+        
+        self.progressGroupBox.hide()
+        
+        self.__updateButtonBoxButtons()
+        
+        self.testRunStopped.emit()
+        
+        self.raise_()
+        self.activateWindow()
+    
+    @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
+        """
+        if not bool(self.discoveryPicker.currentText()):
+            if self.__project and self.__project.isOpen():
+                self.__insertDiscovery(self.__project.getProjectPath())
+            else:
+                self.__insertDiscovery(
+                    Preferences.getMultiProject("Workspace"))
+        
+        self.__resetResults()
+    
+    @pyqtSlot()
+    def on_testsuitePicker_aboutToShowPathPickerDialog(self):
+        """
+        Private slot called before the test file selection dialog is shown.
+        """
+        if self.__project:
+            # we were called from within eric
+            py3Extensions = ' '.join([
+                "*{0}".format(ext)
+                for ext in
+                ericApp().getObject("DebugServer").getExtensions('Python3')
+            ])
+            fileFilter = self.tr(
+                "Python3 Files ({0});;All Files (*)"
+            ).format(py3Extensions)
+        else:
+            # standalone application
+            fileFilter = self.tr("Python Files (*.py);;All Files (*)")
+        self.testsuitePicker.setFilters(fileFilter)
+        
+        defaultDirectory = (
+            self.__project.getProjectPath()
+            if self.__project and self.__project.isOpen() else
+            Preferences.getMultiProject("Workspace")
+        )
+        if not defaultDirectory:
+            defaultDirectory = os.path.expanduser("~")
+        self.testsuitePicker.setDefaultDirectory(defaultDirectory)
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+        
+        @param button button that was clicked
+        @type QAbstractButton
+        """
+        if button == self.__startButton:
+            self.startTests()
+            self.__saveRecent()
+        elif button == self.__stopButton:
+            self.__stopTests()
+        elif button == self.__startFailedButton:
+            self.startTests(failedOnly=True)
+    
+    @pyqtSlot(int)
+    def on_venvComboBox_currentIndexChanged(self, index):
+        """
+        Private slot handling the selection of a virtual environment.
+        
+        @param index index of the selected environment
+        @type int
+        """
+        self.__populateTestFrameworkComboBox()
+        self.__updateButtonBoxButtons()
+        
+        self.versionsButton.setEnabled(bool(self.venvComboBox.currentText()))
+    
+    @pyqtSlot()
+    def on_versionsButton_clicked(self):
+        """
+        Private slot to show the versions of available plugins.
+        """
+        venvName = self.venvComboBox.currentText()
+        if venvName:
+            headerText = self.tr("<h3>Versions of Frameworks and their"
+                                 " Plugins</h3>")
+            versionsText = ""
+            interpreter = self.__venvManager.getVirtualenvInterpreter(venvName)
+            for framework in sorted(
+                self.__frameworkRegistry.getFrameworks().keys()
+            ):
+                executor = self.__frameworkRegistry.createExecutor(
+                    framework, self)
+                versions = executor.getVersions(interpreter)
+                if versions:
+                    txt = "<p><strong>{0} {1}</strong>".format(
+                        versions["name"], versions["version"])
+                    
+                    if versions["plugins"]:
+                        txt += "<table>"
+                        for pluginVersion in versions["plugins"]:
+                            txt += self.tr(
+                                "<tr><td>{0}</td><td>{1}</td></tr>"
+                            ).format(
+                                pluginVersion["name"], pluginVersion["version"]
+                            )
+                        txt += "</table>"
+                    txt += "</p>"
+                    
+                    versionsText += txt
+            
+            if not versionsText:
+                versionsText = self.tr("No version information available.")
+            
+            EricMessageBox.information(
+                self,
+                self.tr("Versions"),
+                headerText + versionsText
+            )
+    
+    @pyqtSlot()
+    def startTests(self, failedOnly=False):
+        """
+        Public slot to start the test run.
+        
+        @param failedOnly flag indicating to run only failed tests
+        @type bool
+        """
+        if self.__mode == TestingWidgetModes.RUNNING:
+            return
+        
+        self.__recentEnvironment = self.venvComboBox.currentText()
+        self.__recentFramework = self.frameworkComboBox.currentText()
+        
+        self.__failedTests = (
+            self.__resultsModel.getFailedTests()
+            if failedOnly else
+            []
+        )
+        discover = self.discoverCheckBox.isChecked()
+        if discover:
+            discoveryStart = self.discoveryPicker.currentText()
+            testFileName = ""
+            testName = ""
+            
+            if discoveryStart:
+                self.__insertDiscovery(discoveryStart)
+        else:
+            discoveryStart = ""
+            testFileName = self.testsuitePicker.currentText()
+            if testFileName:
+                self.__insertTestFile(testFileName)
+            testName = self.testComboBox.currentText()
+            if testName:
+                self.__insertTestName(testName)
+            if testFileName and not testName:
+                testName = "suite"
+        
+        self.sbLabel.setText(self.tr("Preparing Testsuite"))
+        QCoreApplication.processEvents()
+        
+        interpreter = self.__venvManager.getVirtualenvInterpreter(
+            self.__recentEnvironment)
+        config = TestConfig(
+            interpreter=interpreter,
+            discover=self.discoverCheckBox.isChecked(),
+            discoveryStart=discoveryStart,
+            testFilename=testFileName,
+            testName=testName,
+            failFast=self.failfastCheckBox.isChecked(),
+            failedOnly=failedOnly,
+            collectCoverage=self.coverageCheckBox.isChecked(),
+            eraseCoverage=self.coverageEraseCheckBox.isChecked(),
+        )
+        
+        self.__testExecutor = self.__frameworkRegistry.createExecutor(
+            self.__recentFramework, self)
+        self.__testExecutor.collected.connect(self.__testsCollected)
+        self.__testExecutor.collectError.connect(self.__testsCollectError)
+        self.__testExecutor.startTest.connect(self.__testStarted)
+        self.__testExecutor.testResult.connect(self.__processTestResult)
+        self.__testExecutor.testFinished.connect(self.__testProcessFinished)
+        self.__testExecutor.testRunFinished.connect(self.__testRunFinished)
+        self.__testExecutor.stop.connect(self.__testsStopped)
+        self.__testExecutor.coverageDataSaved.connect(self.__coverageData)
+        self.__testExecutor.testRunAboutToBeStarted.connect(
+            self.__testRunAboutToBeStarted)
+        
+        self.__setRunningMode()
+        self.__testExecutor.start(config, [])
+    
+    @pyqtSlot()
+    def __stopTests(self):
+        """
+        Private slot to stop the current test run.
+        """
+        self.__testExecutor.stopIfRunning()
+    
+    @pyqtSlot(list)
+    def __testsCollected(self, testNames):
+        """
+        Private slot handling the 'collected' signal of the executor.
+        
+        @param testNames list of tuples containing the test id and test name
+            of collected tests
+        @type list of tuple of (str, str)
+        """
+        testResults = [
+            TestResult(
+                category=TestResultCategory.PENDING,
+                status=self.tr("pending"),
+                name=name,
+                id=id,
+                message=desc,
+            ) for id, name, desc in testNames
+        ]
+        self.__resultsModel.setTestResults(testResults)
+        
+        self.__totalCount = len(testResults)
+        self.__updateProgress()
+    
+    @pyqtSlot(list)
+    def __testsCollectError(self, errors):
+        """
+        Private slot handling the 'collectError' signal of the executor.
+        
+        @param errors list of tuples containing the test name and a description
+            of the error
+        @type list of tuple of (str, str)
+        """
+        testResults = []
+        
+        for testFile, error in errors:
+            if testFile:
+                testResults.append(TestResult(
+                    category=TestResultCategory.FAIL,
+                    status=self.tr("Failure"),
+                    name=testFile,
+                    id=testFile,
+                    message=self.tr("Collection Error"),
+                    extra=error.splitlines()
+                ))
+            else:
+                EricMessageBox.critical(
+                    self,
+                    self.tr("Collection Error"),
+                    self.tr(
+                        "<p>There was an error while collecting unit tests."
+                        "</p><p>{0}</p>"
+                    ).format("<br/>".join(error.splitlines()))
+                )
+        
+        if testResults:
+            self.__resultsModel.addTestResults(testResults)
+    
+    @pyqtSlot(tuple)
+    def __testStarted(self, test):
+        """
+        Private slot handling the 'startTest' signal of the executor.
+        
+        @param test tuple containing the id, name and short description of the
+            tests about to be run
+        @type tuple of (str, str, str)
+        """
+        self.__resultsModel.updateTestResults([
+            TestResult(
+                category=TestResultCategory.RUNNING,
+                status=self.tr("running"),
+                id=test[0],
+                name=test[1],
+                message="" if test[2] is None else test[2],
+            )
+        ])
+    
+    @pyqtSlot(TestResult)
+    def __processTestResult(self, result):
+        """
+        Private slot to handle the receipt of a test result object.
+        
+        @param result test result object
+        @type TestResult
+        """
+        if not result.subtestResult:
+            self.__runCount += 1
+        self.__updateProgress()
+        
+        self.__resultsModel.updateTestResults([result])
+    
+    @pyqtSlot(list, str)
+    def __testProcessFinished(self, results, output):
+        """
+        Private slot to handle the 'testFinished' signal of the executor.
+        
+        @param results list of test result objects (if not sent via the
+            'testResult' signal
+        @type list of TestResult
+        @param output string containing the test process output (if any)
+        @type str
+        """
+        self.__setStoppedMode()
+        self.__testExecutor = None
+    
+    @pyqtSlot(int, float)
+    def __testRunFinished(self, noTests, duration):
+        """
+        Private slot to handle the 'testRunFinished' signal of the executor.
+        
+        @param noTests number of tests run by the executor
+        @type int
+        @param duration time needed in seconds to run the tests
+        @type float
+        """
+        self.sbLabel.setText(
+            self.tr("Ran %n test(s) in {0}s", "", noTests).format(
+                locale.format_string("%.3f", duration, grouping=True)
+            )
+        )
+        
+        self.__setStoppedMode()
+    
+    @pyqtSlot()
+    def __testsStopped(self):
+        """
+        Private slot to handle the 'stop' signal of the executor.
+        """
+        self.sbLabel.setText(self.tr("Ran %n test(s)", "", self.__runCount))
+        
+        self.__setStoppedMode()
+    
+    @pyqtSlot()
+    def __testRunAboutToBeStarted(self):
+        """
+        Private slot to handle the 'testRunAboutToBeStarted' signal of the
+        executor.
+        """
+        self.__resultsModel.clear()
+    
+    @pyqtSlot(str)
+    def __coverageData(self, coverageFile):
+        """
+        Private slot to handle the 'coverageData' signal of the executor.
+        
+        @param coverageFile file containing the coverage data
+        @type str
+        """
+        self.__coverageFile = coverageFile
+        
+        # TODO: implement the handling of the 'Show Coverage' button
+    
+    @pyqtSlot(str)
+    def __setStatusLabel(self, statusText):
+        """
+        Private slot to set the status label to the text sent by the model.
+        
+        @param statusText text to be shown
+        @type str
+        """
+        self.statusLabel.setText(f"<b>{statusText}</b>")
+    
+    @pyqtSlot()
+    def __projectOpened(self):
+        """
+        Private slot to handle a project being opened.
+        """
+        self.venvComboBox.setCurrentText(self.__project.getProjectVenv())
+        self.frameworkComboBox.setCurrentText(
+            self.__project.getProjectTestingFramework())
+        self.__insertDiscovery(self.__project.getProjectPath())
+    
+    @pyqtSlot()
+    def __projectClosed(self):
+        """
+        Private slot to handle a project being closed.
+        """
+        self.venvComboBox.setCurrentText("")
+        self.frameworkComboBox.setCurrentText("")
+        self.__insertDiscovery("")
+    
+    @pyqtSlot(str, int)
+    def __showSource(self, filename, lineno):
+        """
+        Private slot to show the source of a traceback in an editor.
+        
+        @param filename file name of the file to be shown
+        @type str
+        @param lineno line number to go to in the file
+        @type int
+        """
+        if self.__project:
+            # running as part of eric IDE
+            self.testFile.emit(filename, lineno, True)
+        else:
+            self.__openEditor(filename, lineno)
+    
+    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 testing 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:
+            with contextlib.suppress(Exception):
+                editor.close()
+
+
+class TestingWindow(EricMainWindow):
+    """
+    Main window class for the standalone dialog.
+    """
+    def __init__(self, testfile=None, parent=None):
+        """
+        Constructor
+        
+        @param testfile file name of the test script to open
+        @type str
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super().__init__(parent)
+        self.__cw = TestingWidget(testfile=testfile, 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.Type.Close:
+            QCoreApplication.exit(0)
+            return True
+        
+        return False
+
+
+def clearSavedHistories(self):
+    """
+    Function to clear the saved history lists.
+    """
+    Preferences.Prefs.rsettings.setValue(
+        recentNameTestDiscoverHistory, [])
+    Preferences.Prefs.rsettings.setValue(
+        recentNameTestFileHistory, [])
+    Preferences.Prefs.rsettings.setValue(
+        recentNameTestNameHistory, [])
+    
+    Preferences.Prefs.rsettings.sync()

eric ide

mercurial