diff -r ddc46e93ccc4 -r e7fd342f8bfc eric7/Unittest/UnittestWidget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/UnittestWidget.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,680 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget to orchestrate unit test execution. +""" + +import enum +import os + +from PyQt6.QtCore import 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_UnittestWidget import Ui_UnittestWidget + +from .UTTestResultsTree import TestResultsModel, TestResultsTreeView +from .Interfaces import Frameworks +from .Interfaces.UTExecutorBase import UTTestConfig, UTTestResult +from .Interfaces.UTFrameworkRegistry import UTFrameworkRegistry + +import Preferences +import UI.PixmapCache + +from Globals import ( + recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory, + recentNameUnittestTestnameHistory, recentNameUnittestFramework, + recentNameUnittestEnvironment +) + + +class UnittestWidgetModes(enum.Enum): + """ + Class defining the various modes of the unittest widget. + """ + IDLE = 0 # idle, no test were run yet + RUNNING = 1 # test run being performed + STOPPED = 2 # test run finished + + +class UnittestWidget(QWidget, Ui_UnittestWidget): + """ + Class implementing a widget to orchestrate unit test execution. + """ + 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.__resultsTree = TestResultsTreeView(self) + self.__resultsTree.setModel(self.__resultsModel) + self.resultsGroupBox.layout().addWidget(self.__resultsTree) + + self.versionsButton.setIcon( + UI.PixmapCache.getIcon("info")) + self.clearHistoriesButton.setIcon( + UI.PixmapCache.getIcon("clearPrivateData")) + + self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) + self.testsuitePicker.setInsertPolicy( + QComboBox.InsertPolicy.InsertAtTop) + self.testsuitePicker.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) + + self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE) + self.discoveryPicker.setInsertPolicy( + QComboBox.InsertPolicy.InsertAtTop) + self.discoveryPicker.setSizeAdjustPolicy( + QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) + + self.testComboBox.lineEdit().setClearButtonEnabled(True) + + # create some more dialog buttons for orchestration + self.__startButton = self.buttonBox.addButton( + self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole) + + self.__startButton.setToolTip(self.tr( + "Start the selected testsuite")) + self.__startButton.setWhatsThis(self.tr( + """<b>Start Test</b>""" + """<p>This button starts the selected testsuite.</p>""")) + + # TODO: implement "Rerun Failed" +## 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 selected""" +## """ testsuite.</p>""")) +## + self.__stopButton = self.buttonBox.addButton( + self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole) + self.__stopButton.setToolTip(self.tr("Stop the running unittest")) + self.__stopButton.setWhatsThis(self.tr( + """<b>Stop Test</b>""" + """<p>This button stops a running unittest.</p>""")) + + self.__stopButton.setEnabled(False) + self.__startButton.setDefault(True) + self.__startButton.setEnabled(False) +## self.__startFailedButton.setEnabled(False) + + self.setWindowFlags( + self.windowFlags() | + Qt.WindowType.WindowContextHelpButtonHint + ) + self.setWindowIcon(UI.PixmapCache.getIcon("eric")) + self.setWindowTitle(self.tr("Unittest")) + + 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) + + # TODO: implement project mode + self.__forProject = False + + self.__discoverHistory = [] + self.__fileHistory = [] + self.__testNameHistory = [] + self.__recentFramework = "" + self.__recentEnvironment = "" + + self.__failedTests = set() + + self.__editors = [] + self.__testExecutor = None + + # connect some signals + self.frameworkComboBox.currentIndexChanged.connect( + self.__updateButtonBoxButtons) + self.discoverCheckBox.toggled.connect( + self.__updateButtonBoxButtons) + self.discoveryPicker.editTextChanged.connect( + self.__updateButtonBoxButtons) + self.testsuitePicker.editTextChanged.connect( + self.__updateButtonBoxButtons) + + self.__frameworkRegistry = UTFrameworkRegistry() + for framework in Frameworks: + self.__frameworkRegistry.register(framework) + + self.__setIdleMode() + + self.__loadRecent() + self.__populateVenvComboBox() + + if self.__forProject: + project = ericApp().getObject("Project") + if project.isOpen(): + self.__insertDiscovery(project.getProjectPath()) + else: + self.__insertDiscovery("") + else: + self.__insertDiscovery("") + self.__insertProg(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())) + index = self.venvComboBox.findText(currentText) + if index < 0: + index = 0 + self.venvComboBox.setCurrentIndex(index) + + 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) + + @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 + """ + current = widget.currentText() + + # 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) + + if current: + widget.setText(current) + + @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 __insertProg(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( + recentNameUnittestEnvironment, "") + self.__recentFramework = Preferences.Prefs.rsettings.value( + recentNameUnittestFramework, "") + + # 2. discovery history + self.__discoverHistory = [] + rs = Preferences.Prefs.rsettings.value( + recentNameUnittestDiscoverHistory) + 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( + recentNameUnittestFileHistory) + 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( + recentNameUnittestTestnameHistory) + 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( + recentNameUnittestEnvironment, self.__recentEnvironment) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestFramework, self.__recentFramework) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestDiscoverHistory, self.__discoverHistory) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestFileHistory, self.__fileHistory) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestTestnameHistory, 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() + + def __updateButtonBoxButtons(self): + """ + Private method to update the state of the buttons of the button box. + """ + failedAvailable = bool(self.__failedTests) + + # Start button + if self.__mode in ( + UnittestWidgetModes.IDLE, UnittestWidgetModes.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 == UnittestWidgetModes.IDLE or + not failedAvailable + ) + else: + self.__startButton.setEnabled(False) + self.__startButton.setDefault(False) + + # Start Failed button + # TODO: not implemented yet + + # Stop button + self.__stopButton.setEnabled( + self.__mode == UnittestWidgetModes.RUNNING) + self.__stopButton.setDefault( + self.__mode == UnittestWidgetModes.RUNNING) + + def __setIdleMode(self): + """ + Private method to switch the widget to idle mode. + """ + self.__mode = UnittestWidgetModes.IDLE + self.__updateButtonBoxButtons() + + def __setRunningMode(self): + """ + Private method to switch the widget to running mode. + """ + # TODO: not implemented yet + pass + + def __setStoppedMode(self): + """ + Private method to switch the widget to stopped mode. + """ + # TODO: not implemented yet + pass + + @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.discoverButton: +## self.__discover() +## self.__saveRecent() +## elif button == self.__startButton: + if button == self.__startButton: + self.startTests() + self.__saveRecent() + elif button == self.__stopButton: + self.__stopTests() +# elif button == self.__startFailedButton: +# self.startTests(failedOnly=True) + + @pyqtSlot(int) + def on_venvComboBox_currentIndexChanged(self, index): + """ + Private slot handling the selection of a virtual environment. + + @param index index of the selected environment + @type int + """ + self.__populateTestFrameworkComboBox() + self.__updateButtonBoxButtons() + + self.versionsButton.setEnabled(bool(self.venvComboBox.currentText())) + + @pyqtSlot() + def on_versionsButton_clicked(self): + """ + Private slot to show the versions of available plugins. + """ + venvName = self.venvComboBox.currentText() + if venvName: + headerText = self.tr("<h3>Versions of Frameworks and their" + " Plugins</h3>") + versionsText = "" + interpreter = self.__venvManager.getVirtualenvInterpreter(venvName) + for framework in sorted( + self.__frameworkRegistry.getFrameworks().keys() + ): + executor = self.__frameworkRegistry.createExecutor( + framework, self) + versions = executor.getVersions(interpreter) + if versions: + txt = "<p><strong>{0} {1}</strong>".format( + versions["name"], versions["version"]) + + if versions["plugins"]: + txt += "<table>" + for pluginVersion in versions["plugins"]: + txt += self.tr( + "<tr><td>{0}</td><td>{1}</td></tr>" + ).format( + pluginVersion["name"], pluginVersion["version"] + ) + txt += "</table>" + txt += "</p>" + + versionsText += txt + + if not versionsText: + versionsText = self.tr("No version information available.") + + EricMessageBox.information( + self, + self.tr("Versions"), + headerText + versionsText + ) + + @pyqtSlot() + def startTests(self, failedOnly=False): + """ + Public slot to start the test run. + + @param failedOnly flag indicating to run only failed tests + @type bool + """ + if self.__mode == UnittestWidgetModes.RUNNING: + return + + self.__recentEnvironment = self.venvComboBox.currentText() + self.__recentFramework = self.frameworkComboBox.currentText() + + 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.__insertProg(testFileName) + testName = self.testComboBox.currentText() + if testName: + self.insertTestName(testName) + if testFileName and not testName: + testName = "suite" + + interpreter = self.__venvManager.getVirtualenvInterpreter( + self.__recentEnvironment) + config = UTTestConfig( + interpreter=interpreter, + discover=self.discoverCheckBox.isChecked(), + discoveryStart=discoveryStart, + testFilename=testFileName, + testName=testName, + failFast=self.failfastCheckBox.isChecked(), + collectCoverage=self.coverageCheckBox.isChecked(), + eraseCoverage=self.coverageEraseCheckBox.isChecked(), + ) + + self.__resultsModel.clear() + self.__testExecutor = self.__frameworkRegistry.createExecutor( + self.__recentFramework, self) + self.__testExecutor.collected.connect(self.__testCollected) + self.__testExecutor.collectError.connect(self.__testsCollectError) + self.__testExecutor.startTest.connect(self.__testsStarted) + self.__testExecutor.testResult.connect(self.__processTestResult) + self.__testExecutor.testFinished.connect(self.__testProcessFinished) + self.__testExecutor.stop.connect(self.__testsStopped) + self.__testExecutor.start(config, []) + + # TODO: not yet implemented + pass + + @pyqtSlot(list) + def __testCollected(self, testNames): + """ + Private slot handling the 'collected' signal of the executor. + + @param testNames list of names of collected tests + @type list of str + """ + # TODO: not implemented yet + pass + + @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) + """ + # TODO: not implemented yet + pass + + @pyqtSlot(list) + def __testsStarted(self, testNames): + """ + Private slot handling the 'startTest' signal of the executor. + + @param testNames list of names of tests about to be run + @type list of str + """ + # TODO: not implemented yet + pass + + @pyqtSlot(UTTestResult) + def __processTestResult(self, result): + """ + Private slot to handle the receipt of a test result object. + + @param result test result object + @type UTTestResult + """ + # TODO: not implemented yet + pass + + @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 UTTestResult + @param output string containing the test process output (if any) + @type str + """ + # TODO: not implemented yet + pass + + @pyqtSlot() + def __testsStopped(self): + """ + Private slot to handle the 'stop' signal of the executor. + """ + # TODO: not implemented yet + pass + + +class UnittestWindow(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 = UnittestWidget(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( + recentNameUnittestDiscoverHistory, []) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestFileHistory, []) + Preferences.Prefs.rsettings.setValue( + recentNameUnittestTestnameHistory, []) + + Preferences.Prefs.rsettings.sync()