eric7/Testing/TestingWidget.py

Mon, 23 May 2022 16:48:19 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 23 May 2022 16:48:19 +0200
branch
unittest
changeset 9089
b48a6d0f6309
parent 9084
ee36935f4edd
child 9093
437bfe0c5793
permissions
-rw-r--r--

Implemented support for the 'pytest' framework.

# -*- 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


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.__showCoverageButton = self.buttonBox.addButton(
            self.tr("Show Coverage..."),
            QDialogButtonBox.ButtonRole.ActionRole)
        self.__showCoverageButton.setToolTip(
            self.tr("Show code coverage in a new dialog"))
        self.__showCoverageButton.setWhatsThis(self.tr(
            """<b>Show Coverage...</b>"""
            """<p>This button opens a dialog containing the collected code"""
            """ coverage data.</p>"""))
        
        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)
            ericApp().registerObject("VirtualEnvManager", self.__venvManager)
            
            self.__project = None
        
        self.__discoverHistory = []
        self.__fileHistory = []
        self.__testNameHistory = []
        self.__recentFramework = ""
        self.__recentEnvironment = ""
        self.__failedTests = []
        
        self.__coverageFile = ""
        self.__coverageDialog = None
        
        self.__editors = []
        self.__testExecutor = None
        
        # connect some signals
        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, forProject=False):
        """
        Public slot to set the given test file as the current one.
        
        @param testFile path of the test file
        @type str
        @param forProject flag indicating that this call is for a project
            (defaults to False)
        @type bool (optional)
        """
        if testFile:
            self.__insertTestFile(testFile)
        
        self.discoverCheckBox.setChecked(forProject or not bool(testFile))
        
        if forProject:
            self.__projectOpened()
        
        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)
        
        # Code coverage button
        self.__showCoverageButton.setEnabled(
            self.__mode == TestingWidgetModes.STOPPED and
            bool(self.__coverageFile) and
            (
                (self.discoverCheckBox.isChecked() and
                 bool(self.discoveryPicker.currentText())) or
                bool(self.testsuitePicker.currentText())
            )
        )
        
        # 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 = ""
        
        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)
        elif button == self.__showCoverageButton:
            self.__showCoverageDialog()
    
    @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()))
        
        self.__updateCoverage()
    
    @pyqtSlot(int)
    def on_frameworkComboBox_currentIndexChanged(self, index):
        """
        Private slot handling the selection of a test framework.
        
        @param index index of the selected framework
        @type int
        """
        self.__resetResults()
        self.__updateCoverage()
    
    @pyqtSlot()
    def __updateCoverage(self):
        """
        Private slot to update the state of the coverage checkbox depending on
        the selected framework's capabilities.
        """
        hasCoverage = False
        
        venvName = self.venvComboBox.currentText()
        if venvName:
            framework = self.frameworkComboBox.currentText()
            if framework:
                interpreter = self.__venvManager.getVirtualenvInterpreter(
                    venvName)
                executor = self.__frameworkRegistry.createExecutor(
                    framework, self)
                hasCoverage = executor.hasCoverage(interpreter)
        
        self.coverageCheckBox.setEnabled(hasCoverage)
        if not hasCoverage:
            self.coverageCheckBox.setChecked(False)
    
    @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)
        
        self.sbLabel.setText(self.tr("Preparing Testsuite"))
        QCoreApplication.processEvents()
        
        if self.__project:
            mainScript = self.__project.getMainScript(True)
            coverageFile = (
                os.path.splitext(mainScript)[0] + ".coverage"
                if mainScript else
                ""
            )
        else:
            coverageFile = ""
        interpreter = self.__venvManager.getVirtualenvInterpreter(
            self.__recentEnvironment)
        config = TestConfig(
            interpreter=interpreter,
            discover=discover,
            discoveryStart=discoveryStart,
            testFilename=testFileName,
            testName=testName,
            failFast=self.failfastCheckBox.isChecked(),
            failedOnly=failedOnly,
            collectCoverage=self.coverageCheckBox.isChecked(),
            eraseCoverage=self.coverageEraseCheckBox.isChecked(),
            coverageFile=coverageFile,
        )
        
        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, the test name
            and a description of collected tests
        @type list of tuple of (str, 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.addTestResults(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 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
        
        self.__adjustPendingState()
    
    @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()
    
    def __adjustPendingState(self):
        """
        Private method to change the status indicator of all still pending
        tests to "not run".
        """
        newResults = []
        for result in self.__resultsModel.getTestResults():
            if result.category == TestResultCategory.PENDING:
                result.category = TestResultCategory.SKIP
                result.status = self.tr("not run")
                newResults.append(result)
        
        if newResults:
            self.__resultsModel.updateTestResults(newResults)
    
    @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
    
    @pyqtSlot()
    def __showCoverageDialog(self):
        """
        Private slot to show a code coverage dialog for the most recent test
        run.
        """
        if self.__coverageDialog is None:
            from DataViews.PyCoverageDialog import PyCoverageDialog
            self.__coverageDialog = PyCoverageDialog(self)
            self.__coverageDialog.openFile.connect(self.__openEditor)
        
        testDir = (
            self.discoveryPicker.currentText()
            if self.discoverCheckBox.isChecked() else
            os.path.dirname(self.testsuitePicker.currentText())
        )
        if testDir:
            self.__coverageDialog.show()
            self.__coverageDialog.start(self.__coverageFile, testDir)
    
    @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=1):
        """
        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 (defaults to 1)
        @type int (optional)
        """
        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