--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Testing/TestingWidget.py Sun Jul 24 11:29:56 2022 +0200 @@ -0,0 +1,1160 @@ +# -*- 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.__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) + 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 + self.__recentLog = "" + + # 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()) + ) + ) + + # 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() + + @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.__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.__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.__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.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()