src/eric7/Testing/TestingWidget.py

Wed, 05 Oct 2022 16:19:31 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 05 Oct 2022 16:19:31 +0200
branch
eric7
changeset 9388
bfe7ea6599a3
parent 9313
6bac6775abb2
child 9413
80c06d472826
permissions
-rw-r--r--

Added support for project embedded environments to the Testing framework and the Start dialog.

# -*- 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.showMarkersButton.setIcon(UI.PixmapCache.getIcon("select"))

        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.__showLogButton = self.buttonBox.addButton(
            self.tr("Show Output..."), QDialogButtonBox.ButtonRole.ActionRole
        )
        self.__showLogButton.setToolTip(
            self.tr("Show the output of the test runner process")
        )
        self.__showLogButton.setWhatsThis(
            self.tr(
                """<b>Show Output...</b"""
                """<p>This button opens a dialog containing the output of the"""
                """ test runner process of the most recent run.</p>"""
            )
        )

        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)
            self.__projectEnvironmentMarker = self.tr("<project>")
        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.__projectEnvironmentMarker = ""

        self.__discoverHistory = []
        self.__fileHistory = []
        self.__testNameHistory = []
        self.__recentFramework = ""
        self.__recentEnvironment = ""
        self.__failedTests = []

        self.__coverageFile = ""
        self.__coverageDialog = None

        self.__editors = []
        self.__testExecutor = None
        self.__recentLog = ""

        self.__markersWindow = 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 __determineInterpreter(self, venvName):
        """
        Private method to determine the interpreter to be used.

        @param venvName name of the virtual environment
        @type str
        @return path of the interpreter executable
        @rtype str
        """
        if self.__project and venvName == self.__projectEnvironmentMarker:
            return self.__project.getProjectInterpreter()
        else:
            return self.__venvManager.getVirtualenvInterpreter(venvName)

    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("")
        if self.__project and self.__project.isOpen():
            self.venvComboBox.addItem(self.__projectEnvironmentMarker)
        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.__determineInterpreter(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())
            )
        )

        # Log output button
        self.__showLogButton.setEnabled(bool(self.__recentLog))

        # 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()
        elif button == self.__showLogButton:
            self.__showLogOutput()

    @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()
        self.__updateMarkerSupport()
        self.__updatePatternSupport()

    @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.__determineInterpreter(venvName)
                executor = self.__frameworkRegistry.createExecutor(framework, self)
                hasCoverage = executor.hasCoverage(interpreter)

        self.coverageCheckBox.setEnabled(hasCoverage)
        if not hasCoverage:
            self.coverageCheckBox.setChecked(False)

    @pyqtSlot()
    def __updateMarkerSupport(self):
        """
        Private slot to update the state of the marker related widgets depending on
        the selected framework's capabilities.
        """
        supportsMarkers = False

        venvName = self.venvComboBox.currentText()
        if venvName:
            framework = self.frameworkComboBox.currentText()
            if framework:
                interpreter = self.__determineInterpreter(venvName)
                executor = self.__frameworkRegistry.createExecutor(framework, self)
                supportsMarkers = executor.supportsMarkers(interpreter)

        # 1. marker expression line edit
        self.markerExpressionEdit.setEnabled(supportsMarkers)
        if not supportsMarkers:
            self.markerExpressionEdit.clear()

        # 2. show markers button
        self.showMarkersButton.setEnabled(supportsMarkers)
        if self.__markersWindow is not None:
            self.__markersWindow.close()

    @pyqtSlot()
    def on_showMarkersButton_clicked(self):
        """
        Private slot to show a window containing the list of defined markers.
        """
        venvName = self.venvComboBox.currentText()
        if venvName:
            framework = self.frameworkComboBox.currentText()
            if framework:
                if self.discoverCheckBox.isChecked():
                    workdir = self.discoveryPicker.currentText()
                elif self.testsuitePicker.currentText():
                    workdir = os.path.dirname(self.testsuitePicker.currentText())
                else:
                    workdir = ""

                interpreter = self.__determineInterpreter(venvName)
                executor = self.__frameworkRegistry.createExecutor(framework, self)
                markers = executor.getMarkers(interpreter, workdir)

                if self.__markersWindow is None:
                    from .MarkersWindow import MarkersWindow

                    self.__markersWindow = MarkersWindow()
                self.__markersWindow.showMarkers(markers)

    @pyqtSlot()
    def __updatePatternSupport(self):
        """
        Private slot to update the state of the test name pattern line edit depending on
        the selected framework's capabilities.
        """
        supportsPatterns = False

        venvName = self.venvComboBox.currentText()
        if venvName:
            framework = self.frameworkComboBox.currentText()
            if framework:
                interpreter = self.__determineInterpreter(venvName)
                executor = self.__frameworkRegistry.createExecutor(framework, self)
                supportsPatterns = executor.supportsPatterns(interpreter)

        self.testNamePatternEdit.setEnabled(supportsPatterns)
        self.testNamePatternEdit.clear()

    @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.__determineInterpreter(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.__recentLog = ""

        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.__determineInterpreter(self.__recentEnvironment)
        config = TestConfig(
            interpreter=interpreter,
            discover=discover,
            discoveryStart=discoveryStart,
            testFilename=testFileName,
            testName=testName,
            testNamePattern=self.testNamePatternEdit.text(),
            testMarkerExpression=self.markerExpressionEdit.text(),
            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.__recentLog = output

        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()
    def __showLogOutput(self):
        """
        Private slot to show the output of the most recent test run.
        """
        from EricWidgets.EricPlainTextDialog import EricPlainTextDialog

        dlg = EricPlainTextDialog(
            title=self.tr("Test Run Output"), text=self.__recentLog
        )
        dlg.exec()

    @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.insertItem(1, self.__projectEnvironmentMarker)
        self.venvComboBox.setCurrentIndex(1)
        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.removeItem(1)  # <project> is always at index 1
        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