Thu, 12 May 2022 08:59:13 +0200
Implemented the basic functionality of the new unit test framework.
--- a/eric7.epj Sun May 08 19:58:27 2022 +0200 +++ b/eric7.epj Thu May 12 08:59:13 2022 +0200 @@ -661,6 +661,7 @@ "eric7/UI/SearchWidgetLine.ui", "eric7/UI/SymbolsWidget.ui", "eric7/UI/VersionsDialog.ui", + "eric7/Unittest/UnittestWidget.ui", "eric7/VCS/CommandOptionsDialog.ui", "eric7/VCS/RepositoryInfoDialog.ui", "eric7/ViewManager/BookmarkedFilesDialog.ui", @@ -2009,6 +2010,16 @@ "eric7/UI/__init__.py", "eric7/UI/data/__init__.py", "eric7/UI/upgrader.py", + "eric7/Unittest/Interfaces/PytestExecutor.py", + "eric7/Unittest/Interfaces/PytestRunner.py", + "eric7/Unittest/Interfaces/UTExecutorBase.py", + "eric7/Unittest/Interfaces/UTFrameworkRegistry.py", + "eric7/Unittest/Interfaces/UnittestExecutor.py", + "eric7/Unittest/Interfaces/UnittestRunner.py", + "eric7/Unittest/Interfaces/__init__.py", + "eric7/Unittest/UTTestResultsTree.py", + "eric7/Unittest/UnittestWidget.py", + "eric7/Unittest/__init__.py", "eric7/Utilities/AutoSaver.py", "eric7/Utilities/BackgroundClient.py", "eric7/Utilities/BackgroundService.py",
--- a/eric7/Globals/__init__.py Sun May 08 19:58:27 2022 +0200 +++ b/eric7/Globals/__init__.py Thu May 12 08:59:13 2022 +0200 @@ -38,6 +38,8 @@ recentNameUnittestDiscoverHistory = "UTDiscoverHistory" recentNameUnittestFileHistory = "UTFileHistory" recentNameUnittestTestnameHistory = "UTTestnameHistory" +recentNameUnittestFramework = "UTTestFramework" +recentNameUnittestEnvironment = "UTEnvironmentName" configDir = None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/PytestExecutor.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the executor for the 'pytest' framework. +""" + +import contextlib +import json +import os + +from PyQt6.QtCore import QProcess + +from .UTExecutorBase import UTExecutorBase + + +class PytestExecutor(UTExecutorBase): + """ + Class implementing the executor for the 'pytest' framework. + """ + module = "pytest" + name = "pytest" + + runner = os.path.join(os.path.dirname(__file__), "PytestRunner.py") + + def getVersions(self, interpreter): + """ + Public method to get the test framework version and version information + of its installed plugins. + + @param interpreter interpreter to be used for the test + @type str + @return dictionary containing the framework name and version and the + list of available plugins with name and version each + @rtype dict + """ + proc = QProcess() + proc.start(interpreter, [PytestExecutor.runner, "versions"]) + if proc.waitForFinished(3000): + exitCode = proc.exitCode() + if exitCode == 0: + outputLines = self.readAllOutput(proc).splitlines() + for line in outputLines: + if line.startswith("{") and line.endswith("}"): + with contextlib.suppress(json.JSONDecodeError): + return json.loads(line) + + return {}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/PytestRunner.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the test runner script for the 'pytest' framework. +""" + +import json +import sys + + +class GetPluginVersionsPlugin(): + """ + Class implementing a pytest plugin to extract the version info of all + installed plugins. + """ + def __init__(self): + """ + Constructor + """ + super().__init__() + + self.versions = [] + + def pytest_cmdline_main(self, config): + """ + Public method called for performing the main command line action. + + @param config pytest config object + @type Config + """ + pluginInfo = config.pluginmanager.list_plugin_distinfo() + if pluginInfo: + for _plugin, dist in pluginInfo: + self.versions.append({ + "name": dist.project_name, + "version": dist.version + }) + + def getVersions(self): + """ + Public method to get the assembled list of plugin versions. + + @return list of collected plugin versions + @rtype list of dict + """ + return self.versions + + +def getVersions(): + """ + Function to determine the framework version and versions of all available + plugins. + """ + try: + import pytest # __IGNORE_WARNING__ + versions = { + "name": "pytest", + "version": pytest.__version__, + "plugins": [], + } + + # --capture=sys needed on Windows to avoid + # ValueError: saved filedescriptor not valid anymore + plugin = GetPluginVersionsPlugin() + pytest.main(['--version', '--capture=sys'], plugins=[plugin]) + versions["plugins"] = plugin.getVersions() + except ImportError: + versions = {} + + print(json.dumps(versions)) + sys.exit(0) + + +if __name__ == '__main__': + command = sys.argv[1] + if command == "installed": + try: + import pytest # __IGNORE_WARNING__ + sys.exit(0) + except ImportError: + sys.exit(1) + + elif command == "versions": + getVersions() + + sys.exit(42) + +# +# eflag: noqa = M801
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/UTExecutorBase.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the executor base class for the various testing frameworks +and supporting classes. +""" + +import os +from dataclasses import dataclass +from enum import IntEnum + +from PyQt6.QtCore import pyqtSignal, QObject, QProcess, QProcessEnvironment + +import Preferences + + +class ResultCategory(IntEnum): + """ + Class defining the supported result categories. + """ + FAIL = 1 + OK = 2 + SKIP = 3 + PENDING = 4 + + +@dataclass +class UTTestResult: + """ + Class containing the test result data. + """ + category: int # result category + status: str # test status + name: str # test name + message: str # short result message + extra: str # additional information text + duration: float # test duration + filename: str # file name of a failed test + lineno: int # line number of a failed test + + +@dataclass +class UTTestConfig: + """ + Class containing the test run configuration. + """ + interpreter: str # path of the Python interpreter + discover: bool # auto discovery flag + discoveryStart: str # start directory for auto discovery + testFilename: str # name of the test script + testName: str # name of the test function + failFast: bool # stop on first fail + collectCoverage: bool # coverage collection flag + eraseCoverage: bool # erase coverage data first + + +class UTExecutorBase(QObject): + """ + Base class for test framework specific implementations. + + @signal collected(list of str) emitted after all tests have been + collected + @signal collectError(list of tuple of (str, str)) emitted when errors + are encountered during test collection. Tuple elements are the + test name and the error message. + @signal startTest(list of str) emitted before tests are run + @signal testResult(UTTestResult) emitted when a test result is ready + @signal testFinished(list, str) emitted when the test has finished. + The elements are the list of test results and the captured output + of the test worker (if any). + @signal stop() emitted when the test process is being stopped. + """ + collected = pyqtSignal(list) + collectError = pyqtSignal(list) + startTest = pyqtSignal(list) + testResult = pyqtSignal(UTTestResult) + testFinished = pyqtSignal(list, str) + stop = pyqtSignal() + + module = "" + name = "" + runner = "" + + def __init__(self, testWidget, logfile=None): + """ + Constructor + + @param testWidget reference to the unit test widget + @type UnittestWidget + @param logfile file name to log test results to (defaults to None) + @type str (optional) + """ + super().__init__(testWidget) + + self.__process = None + self._logfile = logfile + # TODO: add log file creation + + @classmethod + def isInstalled(cls, interpreter): + """ + Class method to check whether a test framework is installed. + + The test is performed by checking, if a module loader can found. + + @param interpreter interpreter to be used for the test + @type str + @return flag indicating the test framework module is installed + @rtype bool + """ + if cls.runner: + proc = QProcess() + proc.start(interpreter, [cls.runner, "installed"]) + if proc.waitForFinished(3000): + exitCode = proc.exitCode() + return exitCode == 0 + + return False + + def getVersions(self, interpreter): + """ + Public method to get the test framework version and version information + of its installed plugins. + + @param interpreter interpreter to be used for the test + @type str + @return dictionary containing the framework name and version and the + list of available plugins with name and version each + @rtype dict + @exception NotImplementedError this method needs to be implemented by + derived classes + """ + raise NotImplementedError + + return {} + + def createArguments(self, config): + """ + Public method to create the arguments needed to start the test process. + + @param config configuration for the test execution + @type UTTestConfig + @return list of process arguments + @rtype list of str + @exception NotImplementedError this method needs to be implemented by + derived classes + """ + raise NotImplementedError + + return [] + + def _prepareProcess(self, workDir, pythonpath): + """ + Protected method to prepare a process object to be started. + + @param workDir working directory + @type str + @param pythonpath list of directories to be added to the Python path + @type list of str + @return prepared process object + @rtype QProcess + """ + process = QProcess(self) + process.setProcessChannelMode( + QProcess.ProcessChannelMode.MergedChannels) + process.setWorkingDirectory(workDir) + process.finished.connect(self.finished) + if pythonpath: + env = QProcessEnvironment.systemEnvironment() + currentPythonPath = env.value('PYTHONPATH', None) + newPythonPath = os.pathsep.join(pythonpath) + if currentPythonPath: + newPythonPath += os.pathsep + currentPythonPath + env.insert('PYTHONPATH', newPythonPath) + process.setProcessEnvironment(env) + + return process + + def start(self, config, pythonpath): + """ + Public method to start the testing process. + + @param config configuration for the test execution + @type UTTestConfig + @param pythonpath list of directories to be added to the Python path + @type list of str + @exception RuntimeError raised if the the testing process did not start + """ + workDir = ( + config.discoveryStart + if config.discover else + os.path.dirname(config.testFilename) + ) + self.__process = self._prepareProcess(workDir, pythonpath) + testArgs = self.createArguments(config) + self.__process.start(config.interpreter, testArgs) + running = self.__process.waitForStarted() + if not running: + raise RuntimeError + + def finished(self): + """ + Public method handling the unit test process been finished. + + This method should read the results (if necessary) and emit the signal + testFinished. + + @exception NotImplementedError this method needs to be implemented by + derived classes + """ + raise NotImplementedError + + def readAllOutput(self, process=None): + """ + Public method to read all output of the test process. + + @param process reference to the process object + @type QProcess + @return test process output + @rtype str + """ + if process is None: + process = self.__process + output = ( + str(process.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').strip() + if process else + "" + ) + return output + + def stopIfRunning(self): + """ + Public method to stop the testing process, if it is running. + """ + if ( + self.__process and + self.__process.state() == QProcess.ProcessState.Running + ): + self.__process.terminate() + self.__process.waitForFinished(2000) + self.__process.kill() + self.__process.waitForFinished(3000) + + self.stop.emit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/UTFrameworkRegistry.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a simple registry containing the available test framework +interfaces. +""" + +import copy + + +class UTFrameworkRegistry(): + """ + Class implementing a simple registry of test framework interfaces. + + The test executor for a framework is responsible for running the tests, + receiving the results and preparing them for display. It must implement + the interface of UTExecutorBase. + + Frameworks must first be registered using '.register()'. This registry + can then create the assoicated test executor when '.createExecutor()' is + called. + """ + def __init__(self): + """ + Constructor + """ + self.__frameworks = {} + + def register(self, executorClass): + """ + Public method to register a test framework executor. + + @param executorClass class implementing the test framework executor + @type UTExecutorBase + """ + self.__frameworks[executorClass.name] = executorClass + + def createExecutor(self, framework, widget, logfile=None): + """ + Public method to create a test framework executor. + + Note: The executor classes have to be registered first. + + @param framework name of the test framework + @type str + @param widget reference to the unit test widget + @type UnittestWidget + @param logfile file name to log test results to (defaults to None) + @type str (optional) + @return test framework executor object + """ + cls = self.__frameworks[framework] + return cls(widget, logfile=logfile) + + def getFrameworks(self): + """ + Public method to get a copy of the registered frameworks. + + @return copy of the registered frameworks + @rtype dict + """ + return copy.copy(self.__frameworks)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/UnittestExecutor.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the executor for the standard 'unittest' framework. +""" + +import contextlib +import json +import os + +from PyQt6.QtCore import QProcess + +from .UTExecutorBase import UTExecutorBase + + +class UnittestExecutor(UTExecutorBase): + """ + Class implementing the executor for the standard 'unittest' framework. + """ + module = "unittest" + name = "unittest" + + runner = os.path.join(os.path.dirname(__file__), "UnittestRunner.py") + + def getVersions(self, interpreter): + """ + Public method to get the test framework version and version information + of its installed plugins. + + @param interpreter interpreter to be used for the test + @type str + @return dictionary containing the framework name and version and the + list of available plugins with name and version each + @rtype dict + """ + proc = QProcess() + proc.start(interpreter, [UnittestExecutor.runner, "versions"]) + if proc.waitForFinished(3000): + exitCode = proc.exitCode() + if exitCode == 0: + versionsStr = self.readAllOutput(proc) + with contextlib.suppress(json.JSONDecodeError): + return json.loads(versionsStr) + + return {} + + def createArguments(self, config): + """ + Public method to create the arguments needed to start the test process. + + @param config configuration for the test execution + @type UTTestConfig + @return list of process arguments + @rtype list of str + @exception NotImplementedError this method needs to be implemented by + derived classes + """ + raise NotImplementedError + + return []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/UnittestRunner.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the test runner script for the 'unittest' framework. +""" + +import json +import sys + +if __name__ == '__main__': + command = sys.argv[1] + if command == "installed": + try: + import unittest # __IGNORE_WARNING__ + sys.exit(0) + except ImportError: + sys.exit(1) + + elif command == "versions": + import platform + versions = { + "name": "unittest", + "version": platform.python_version(), + "plugins": [], + } + print(json.dumps(versions)) + sys.exit(0) + + sys.exit(42) + +# +# eflag: noqa = M801
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/Interfaces/__init__.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containg the various test framework interfaces. +""" + +from .PytestExecutor import PytestExecutor +from .UnittestExecutor import UnittestExecutor + +Frameworks = ( + UnittestExecutor, + PytestExecutor, +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/UTTestResultsTree.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a tree view and associated model to show the test result +data. +""" + +from PyQt6.QtCore import ( + pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex +) +from PyQt6.QtWidgets import QTreeView + +TopLevelId = 2 ** 32 - 1 + + +class TestResultsModel(QAbstractItemModel): + """ + Class implementing the item model containing the test data. + """ + Headers = [ + QCoreApplication.translate("TestResultsModel", "Status"), + QCoreApplication.translate("TestResultsModel", "Name"), + QCoreApplication.translate("TestResultsModel", "Message"), + QCoreApplication.translate("TestResultsModel", "Duration (ms)"), + ] + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (defaults to None) + @type QObject (optional) + """ + super().__init__(parent) + + self.__testResults = [] + + def headerData(self, section, orientation, + role=Qt.ItemDataRole.DisplayRole): + """ + Public method to get the header string for the various sections. + + @param section section number + @type int + @param orientation orientation of the header + @type Qt.Orientation + @param role data role (defaults to Qt.ItemDataRole.DisplayRole) + @type Qt.ItemDataRole (optional) + @return header string of the section + @rtype str + """ + if ( + orientation == Qt.Orientation.Horizontal and + role == Qt.ItemDataRole.DisplayRole + ): + return TestResultsModel.Headers[section] + else: + return None + + def rowCount(self, parent=QModelIndex()): + """ + Public method to get the number of row for a given parent index. + + @param parent index of the parent item (defaults to QModelIndex()) + @type QModelIndex (optional) + @return number of rows + @rtype int + """ + if not parent.isValid(): + return len(self.__testResults) + + if parent.internalId() == TopLevelId and parent.column() == 0: + return len(self.__testResults[parent.row()].extra) + + return 0 + + def columnCount(self, parent=QModelIndex()): + """ + Public method to get the number of columns. + + @param parent index of the parent item (defaults to QModelIndex()) + @type QModelIndex (optional) + @return number of columns + @rtype int + """ + if not parent.isValid(): + return len(TestResultsModel.Headers) + else: + return 1 + + def clear(self): + """ + Public method to clear the model data. + """ + self.beginResetModel() + self.__testResults.clear() + self.endResetModel() + + +class TestResultsTreeView(QTreeView): + """ + Class implementing a tree view to show the test result data. + + @signal goto(str, int) emitted to go to the position given by file name + and line number + """ + goto = pyqtSignal(str, int) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + + self.setItemsExpandable(True) + self.setExpandsOnDoubleClick(False) + self.setSortingEnabled(True) + + self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) + self.header().setSortIndicatorShown(False) + + # connect signals and slots + self.doubleClicked.connect(self.__gotoTestDefinition) + + self.header().sortIndicatorChanged.connect(self.sortByColumn) + self.header().sortIndicatorChanged.connect( + lambda column, order: self.header().setSortIndicatorShown(True)) + + @pyqtSlot(QModelIndex) + def __gotoTestDefinition(self, index): + """ + Private slot to show the test definition. + + @param index index for the double-clicked item + @type QModelIndex + """ + # TODO: not implemented yet + pass + +# +# eflag: noqa = M822
--- /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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/UnittestWidget.ui Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,547 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>UnittestWidget</class> + <widget class="QWidget" name="UnittestWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>650</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>Unittest</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="parametersTab"> + <attribute name="title"> + <string>Parameters</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="venvLabel"> + <property name="text"> + <string>Virtual Environment:</string> + </property> + <property name="buddy"> + <cstring>venvComboBox</cstring> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QComboBox" name="venvComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the virtual environment to be used</string> + </property> + <property name="whatsThis"> + <string><b>Virtual Environment</b>\n<p>Enter the virtual environment to be used. Leave it empty to use the default environment, i.e. the one configured globally or per project.</p></string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Test Framework:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="frameworkComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the test framwork to be used</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QToolButton" name="versionsButton"> + <property name="toolTip"> + <string>Press to show the test framework versions</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Test Parameters</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QCheckBox" name="discoverCheckBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select to discover tests automatically</string> + </property> + <property name="text"> + <string>Discover tests (test modules must be importable)</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="clearHistoriesButton"> + <property name="toolTip"> + <string>Press to clear the various histories</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Discovery Start:</string> + </property> + <property name="buddy"> + <cstring>discoveryPicker</cstring> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="EricComboPathPicker" name="discoveryPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter name of the directory at which to start the test file discovery</string> + </property> + <property name="whatsThis"> + <string><b>Discovery Start</b> +<p>Enter name of the directory at which to start the test file discovery. +Note that all test modules must be importable from this directory.</p></string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="testsuiteLabel"> + <property name="text"> + <string>Test Filename:</string> + </property> + <property name="buddy"> + <cstring>testsuitePicker</cstring> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="EricComboPathPicker" name="testsuitePicker" native="true"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter name of file defining the testsuite</string> + </property> + <property name="whatsThis"> + <string><b>Testsuite</b> +<p>Enter the name of the file defining the testsuite. +It should have a method with a name given below. If no name is given, the suite() method will be tried. If no such method can be +found, the module will be inspected for proper test +cases.</p></string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Test Name:</string> + </property> + <property name="buddy"> + <cstring>testComboBox</cstring> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QComboBox" name="testComboBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Enter the test name. Leave empty to use the default name "suite".</string> + </property> + <property name="whatsThis"> + <string><b>Testname</b><p>Enter the name of the test to be performed. This name must follow the rules given by Python's unittest module. If this field is empty, the default name of "suite" will be used.</p></string> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="optionsGroup"> + <property name="title"> + <string>Run Parameters</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QCheckBox" name="coverageCheckBox"> + <property name="toolTip"> + <string>Select whether coverage data should be collected</string> + </property> + <property name="text"> + <string>Collect coverage data</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="coverageEraseCheckBox"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Select whether old coverage data should be erased</string> + </property> + <property name="text"> + <string>&Erase coverage data</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="failfastCheckBox"> + <property name="toolTip"> + <string>Select to stop the test run on the first error or failure</string> + </property> + <property name="text"> + <string>Stop on First Error or Failure</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>239</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="resultsTab"> + <attribute name="title"> + <string>Results</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QGroupBox" name="progressGroupBox"> + <property name="title"> + <string>Progress</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QProgressBar" name="progressProgressBar"> + <property name="value"> + <number>0</number> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="format"> + <string>%v/%m Tests</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="progressCounterRunLabel"> + <property name="text"> + <string>Run:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="progressCounterRunCount"> + <property name="toolTip"> + <string>Number of tests run</string> + </property> + <property name="text"> + <string notr="true">0</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="progressCounterRemLabel"> + <property name="text"> + <string>Remaining:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="progressCounterRemCount"> + <property name="toolTip"> + <string>Number of tests to be run</string> + </property> + <property name="text"> + <string notr="true">0</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="resultsGroupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Results</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QLabel" name="statusLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="_4"> + <item> + <widget class="QLabel" name="sbLabel_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Idle</string> + </property> + </widget> + </item> + <item> + <spacer> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>EricComboPathPicker</class> + <extends>QWidget</extends> + <header>EricWidgets/EricPathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>tabWidget</tabstop> + <tabstop>venvComboBox</tabstop> + <tabstop>frameworkComboBox</tabstop> + <tabstop>versionsButton</tabstop> + <tabstop>discoverCheckBox</tabstop> + <tabstop>clearHistoriesButton</tabstop> + <tabstop>discoveryPicker</tabstop> + <tabstop>testsuitePicker</tabstop> + <tabstop>testComboBox</tabstop> + <tabstop>coverageCheckBox</tabstop> + <tabstop>coverageEraseCheckBox</tabstop> + <tabstop>failfastCheckBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>UnittestWidget</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>31</x> + <y>648</y> + </hint> + <hint type="destinationlabel"> + <x>1</x> + <y>510</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>UnittestWidget</receiver> + <slot>close()</slot> + <hints> + <hint type="sourcelabel"> + <x>80</x> + <y>649</y> + </hint> + <hint type="destinationlabel"> + <x>3</x> + <y>580</y> + </hint> + </hints> + </connection> + <connection> + <sender>discoverCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>discoveryPicker</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>168</x> + <y>164</y> + </hint> + <hint type="destinationlabel"> + <x>170</x> + <y>191</y> + </hint> + </hints> + </connection> + <connection> + <sender>discoverCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>testsuitePicker</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>222</x> + <y>162</y> + </hint> + <hint type="destinationlabel"> + <x>222</x> + <y>209</y> + </hint> + </hints> + </connection> + <connection> + <sender>discoverCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>testComboBox</receiver> + <slot>setDisabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>301</x> + <y>163</y> + </hint> + <hint type="destinationlabel"> + <x>300</x> + <y>238</y> + </hint> + </hints> + </connection> + <connection> + <sender>coverageCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>coverageEraseCheckBox</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>160</x> + <y>320</y> + </hint> + <hint type="destinationlabel"> + <x>369</x> + <y>319</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Unittest/__init__.py Thu May 12 08:59:13 2022 +0200 @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing unit test functionality and interface to various unit test +frameworks. +"""
--- a/eric7/eric7_unittest.py Sun May 08 19:58:27 2022 +0200 +++ b/eric7/eric7_unittest.py Thu May 12 08:59:13 2022 +0200 @@ -41,10 +41,12 @@ """ Function to create the main widget. - @param argv list of commandline parameters (list of strings) - @return reference to the main widget (QWidget) + @param argv list of commandline parameters + @type list of str + @return reference to the main widget + @rtype QWidget """ - from PyUnit.UnittestDialog import UnittestWindow + from Unittest.UnittestWidget import UnittestWindow try: fn = argv[1] except IndexError: