Mon, 16 May 2022 19:46:51 +0200
Performed some refactoring to avoid possible name clashes on case-insensitive systems.
--- a/eric7.epj Mon May 16 17:22:43 2022 +0200 +++ b/eric7.epj Mon May 16 19:46:51 2022 +0200 @@ -643,6 +643,7 @@ "eric7/Tasks/TaskPropertiesDialog.ui", "eric7/Templates/TemplatePropertiesDialog.ui", "eric7/Templates/TemplateSingleVariableDialog.ui", + "eric7/Testing/TestingWidget.ui", "eric7/UI/AuthenticationDialog.ui", "eric7/UI/ClearPrivateDataDialog.ui", "eric7/UI/CompareDialog.ui", @@ -661,7 +662,6 @@ "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", @@ -1940,6 +1940,16 @@ "eric7/Templates/TemplateViewer.py", "eric7/Templates/TemplatesFile.py", "eric7/Templates/__init__.py", + "eric7/Testing/Interfaces/PytestExecutor.py", + "eric7/Testing/Interfaces/PytestRunner.py", + "eric7/Testing/Interfaces/TestExecutorBase.py", + "eric7/Testing/Interfaces/TestFrameworkRegistry.py", + "eric7/Testing/Interfaces/UnittestExecutor.py", + "eric7/Testing/Interfaces/UnittestRunner.py", + "eric7/Testing/Interfaces/__init__.py", + "eric7/Testing/TestResultsTree.py", + "eric7/Testing/TestingWidget.py", + "eric7/Testing/__init__.py", "eric7/ThirdParty/Jasy/__init__.py", "eric7/ThirdParty/Jasy/jasy/__init__.py", "eric7/ThirdParty/Jasy/jasy/core/Console.py", @@ -2010,16 +2020,6 @@ "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", @@ -2320,14 +2320,14 @@ "eric7/eric7_snap.pyw", "eric7/eric7_sqlbrowser.py", "eric7/eric7_sqlbrowser.pyw", + "eric7/eric7_testing.py", + "eric7/eric7_testing.pyw", "eric7/eric7_tray.py", "eric7/eric7_tray.pyw", "eric7/eric7_trpreviewer.py", "eric7/eric7_trpreviewer.pyw", "eric7/eric7_uipreviewer.py", "eric7/eric7_uipreviewer.pyw", - "eric7/eric7_unittest.py", - "eric7/eric7_unittest.pyw", "eric7/eric7_virtualenv.py", "eric7/eric7_virtualenv.pyw", "eric7/eric7config.py",
--- a/eric7/DebugClients/Python/DebugClientCapabilities.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/DebugClients/Python/DebugClientCapabilities.py Mon May 16 19:46:51 2022 +0200 @@ -7,6 +7,8 @@ Module defining the debug clients capabilities. """ +# TODO: remove unittest support from Python debugger + HasDebugger = 0x0001 HasInterpreter = 0x0002 HasProfiler = 0x0004
--- a/eric7/DebugClients/Python/__init__.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/DebugClients/Python/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -8,3 +8,5 @@ It consists of different kinds of debug clients. """ + +# TODO: remove the unittest support from the debug client
--- a/eric7/DebugClients/__init__.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/DebugClients/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -6,3 +6,5 @@ """ Package implementing debug clients for various languages. """ + +# TODO: remove the unittest support from the debug client
--- a/eric7/Debugger/__init__.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Debugger/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -9,3 +9,5 @@ This package implements the graphical debugger. It consists of the debugger related HMI part, supporting dialogs and the debug server. """ + +# TODO: remove the unittest support from the debugger
--- a/eric7/Globals/__init__.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Globals/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -35,11 +35,11 @@ recentNameHosts = "Hosts" recentNameBreakpointFiles = "BreakPointFiles" recentNameBreakpointConditions = "BreakPointConditions" -recentNameUnittestDiscoverHistory = "UTDiscoverHistory" -recentNameUnittestFileHistory = "UTFileHistory" -recentNameUnittestTestnameHistory = "UTTestnameHistory" -recentNameUnittestFramework = "UTTestFramework" -recentNameUnittestEnvironment = "UTEnvironmentName" +recentNameTestDiscoverHistory = "UTDiscoverHistory" +recentNameTestFileHistory = "UTFileHistory" +recentNameTestNameHistory = "UTTestnameHistory" +recentNameTestFramework = "UTTestFramework" +recentNameTestEnvironment = "UTEnvironmentName" configDir = None
--- a/eric7/Project/ProjectSourcesBrowser.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Project/ProjectSourcesBrowser.py Mon May 16 19:46:51 2022 +0200 @@ -161,8 +161,8 @@ self.tr('Coverage run of Script...'), self.__contextMenuCoverageScript) - self.unittestAction = self.sourceMenu.addAction( - self.tr('Run unittest...'), self.handleUnittest) + self.testingAction = self.sourceMenu.addAction( + self.tr('Run tests...'), self.handleTesting) self.sourceMenu.addSeparator() act = self.sourceMenu.addAction( self.tr('Rename file'), self._renameFile) @@ -646,7 +646,7 @@ act.setEnabled(False) self.classDiagramAction.setEnabled(True) self.importsDiagramAction.setEnabled(True) - self.unittestAction.setEnabled(False) + self.testingAction.setEnabled(False) self.checksMenu.menuAction().setEnabled( False) elif fn.endswith('.rb'): @@ -655,14 +655,14 @@ act.setEnabled(False) self.classDiagramAction.setEnabled(True) self.importsDiagramAction.setEnabled(False) - self.unittestAction.setEnabled(False) + self.testingAction.setEnabled(False) self.checksMenu.menuAction().setEnabled( False) elif fn.endswith('.js'): # entry for mixed mode programs for act in self.sourceMenuActions.values(): act.setEnabled(False) - self.unittestAction.setEnabled(False) + self.testingAction.setEnabled(False) self.checksMenu.menuAction().setEnabled( False) self.graphicsMenu.menuAction().setEnabled( @@ -673,7 +673,7 @@ act.setEnabled(True) self.classDiagramAction.setEnabled(True) self.importsDiagramAction.setEnabled(True) - self.unittestAction.setEnabled(True) + self.testingAction.setEnabled(True) self.checksMenu.menuAction().setEnabled( True) self.sourceMenu.popup(self.mapToGlobal(coord))
--- a/eric7/Project/PropertiesDialog.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Project/PropertiesDialog.py Mon May 16 19:46:51 2022 +0200 @@ -20,7 +20,7 @@ from QScintilla.DocstringGenerator import getSupportedDocstringTypes -from Unittest.Interfaces import FrameworkNames +from Testing.Interfaces import FrameworkNames import Utilities import Preferences
--- a/eric7/PyUnit/UnittestDialog.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/PyUnit/UnittestDialog.py Mon May 16 19:46:51 2022 +0200 @@ -33,8 +33,8 @@ import Preferences from Globals import ( - recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory, - recentNameUnittestTestnameHistory + recentNameTestDiscoverHistory, recentNameTestFileHistory, + recentNameTestNameHistory ) @@ -417,7 +417,7 @@ # 1. discovery history self.discoverHistory = [] rs = Preferences.Prefs.rsettings.value( - recentNameUnittestDiscoverHistory) + recentNameTestDiscoverHistory) if rs is not None: recent = [f for f in Preferences.toList(rs) @@ -428,7 +428,7 @@ # 2. test file history self.fileHistory = [] rs = Preferences.Prefs.rsettings.value( - recentNameUnittestFileHistory) + recentNameTestFileHistory) if rs is not None: recent = [f for f in Preferences.toList(rs) @@ -439,7 +439,7 @@ # 3. test name history self.testNameHistory = [] rs = Preferences.Prefs.rsettings.value( - recentNameUnittestTestnameHistory) + recentNameTestNameHistory) if rs is not None: recent = [n for n in Preferences.toList(rs) if n] self.testNameHistory = recent[ @@ -450,11 +450,11 @@ Private method to save the most recently used lists. """ Preferences.Prefs.rsettings.setValue( - recentNameUnittestDiscoverHistory, self.discoverHistory) + recentNameTestDiscoverHistory, self.discoverHistory) Preferences.Prefs.rsettings.setValue( - recentNameUnittestFileHistory, self.fileHistory) + recentNameTestFileHistory, self.fileHistory) Preferences.Prefs.rsettings.setValue( - recentNameUnittestTestnameHistory, self.testNameHistory) + recentNameTestNameHistory, self.testNameHistory) Preferences.Prefs.rsettings.sync() @@ -1497,10 +1497,10 @@ Function to clear the saved history lists. """ Preferences.Prefs.rsettings.setValue( - recentNameUnittestDiscoverHistory, []) + recentNameTestDiscoverHistory, []) Preferences.Prefs.rsettings.setValue( - recentNameUnittestFileHistory, []) + recentNameTestFileHistory, []) Preferences.Prefs.rsettings.setValue( - recentNameUnittestTestnameHistory, []) + recentNameTestNameHistory, []) Preferences.Prefs.rsettings.sync()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/Interfaces/PytestExecutor.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,51 @@ +# -*- 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 .TestExecutorBase import TestExecutorBase + + +# TODO: implement 'pytest' support in PytestExecutor +class PytestExecutor(TestExecutorBase): + """ + 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/Testing/Interfaces/PytestRunner.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,94 @@ +# -*- 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 + +# TODO: implement 'pytest' support in PytestRunner + + +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/Testing/Interfaces/TestExecutorBase.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,262 @@ +# -*- 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 TestResultCategory(IntEnum): + """ + Class defining the supported result categories. + """ + RUNNING = 0 + FAIL = 1 + OK = 2 + SKIP = 3 + PENDING = 4 + + +@dataclass +class TestResult: + """ + Class containing the test result data. + """ + category: TestResultCategory # result category + status: str # test status + name: str # test name + id: str # test id + description: str = "" # short description of test + message: str = "" # short result message + extra: list = None # additional information text + duration: float = None # test duration + filename: str = None # file name of a failed test + lineno: int = None # line number of a failed test + subtestResult: bool = False # flag indicating the result of a subtest + + +@dataclass +class TestConfig: + """ + 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 + failedOnly: bool # run failed tests only + collectCoverage: bool # coverage collection flag + eraseCoverage: bool # erase coverage data first + + +class TestExecutorBase(QObject): + """ + Base class for test framework specific implementations. + + @signal collected(list of tuple of (str, str, str)) emitted after all tests + have been collected. Tuple elements are the test id, the test name and + a short description of the test. + @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(tuple of (str, str, str) emitted before tests are run. + Tuple elements are test id, test name and short description. + @signal testResult(TestResult) 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 testRunAboutToBeStarted() emitted just before the test run will + be started. + @signal testRunFinished(int, float) emitted when the test run has finished. + The elements are the number of tests run and the duration in seconds + @signal stop() emitted when the test process is being stopped. + @signal coverageDataSaved(str) emitted after the coverage data was saved. + The element is the absolute path of the coverage data file. + """ + collected = pyqtSignal(list) + collectError = pyqtSignal(list) + startTest = pyqtSignal(tuple) + testResult = pyqtSignal(TestResult) + testFinished = pyqtSignal(list, str) + testRunAboutToBeStarted = pyqtSignal() + testRunFinished = pyqtSignal(int, float) + stop = pyqtSignal() + coverageDataSaved = pyqtSignal(str) + + module = "" + name = "" + runner = "" + + def __init__(self, testWidget): + """ + Constructor + + @param testWidget reference to the unit test widget + @type TestingWidget + """ + super().__init__(testWidget) + + self.__process = None + + @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 TestConfig + @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 TestConfig + @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.testRunAboutToBeStarted.emit() + 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/Testing/Interfaces/TestFrameworkRegistry.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,64 @@ +# -*- 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 TestFrameworkRegistry(): + """ + 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 TestExecutorBase. + + 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 TestExecutorBase + """ + self.__frameworks[executorClass.name] = executorClass + + def createExecutor(self, framework, widget): + """ + 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 TestingWidget + @return test framework executor object + @rtype TestExecutorBase + """ + cls = self.__frameworks[framework] + return cls(widget) + + 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/Testing/Interfaces/UnittestExecutor.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,221 @@ +# -*- 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 +import re + +from PyQt6.QtCore import pyqtSlot, QProcess + +from EricNetwork.EricJsonStreamReader import EricJsonReader + +from .TestExecutorBase import TestExecutorBase, TestResult, TestResultCategory + + +class UnittestExecutor(TestExecutorBase): + """ + Class implementing the executor for the standard 'unittest' framework. + """ + module = "unittest" + name = "unittest" + + runner = os.path.join(os.path.dirname(__file__), "UnittestRunner.py") + + def __init__(self, testWidget): + """ + Constructor + + @param testWidget reference to the unit test widget + @type TestingWidget + """ + super().__init__(testWidget) + + self.__statusCategoryMapping = { + "failure": TestResultCategory.FAIL, + "error": TestResultCategory.FAIL, + "skipped": TestResultCategory.SKIP, + "expected failure": TestResultCategory.OK, + "unexpected success": TestResultCategory.FAIL, + "success": TestResultCategory.OK, + } + + self.__statusDisplayMapping = { + "failure": self.tr("Failure"), + "error": self.tr("Error"), + "skipped": self.tr("Skipped"), + "expected failure": self.tr("Expected Failure"), + "unexpected success": self.tr("Unexpected Success"), + "success": self.tr("Success"), + } + + self.__testWidget = testWidget + + 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 TestConfig + @return list of process arguments + @rtype list of str + """ + args = [ + UnittestExecutor.runner, + "runtest", + self.reader.address(), + str(self.reader.port()), + ] + + if config.discover: + args.extend([ + "discover", + "--start-directory", + config.discoveryStart, + ]) + + if config.failFast: + args.append("--failfast") + + if config.collectCoverage: + args.append("--cover") + if config.eraseCoverage: + args.append("--cover-erase") + + if config.failedOnly: + args.append("--failed-only") + if config.testFilename: + args.append(config.testFilename) + args.extend(self.__testWidget.getFailedTests()) + + elif config.testFilename and config.testName: + args.append(config.testFilename) + args.append(config.testName) + + return args + + def start(self, config, pythonpath): + """ + Public method to start the testing process. + + @param config configuration for the test execution + @type TestConfig + @param pythonpath list of directories to be added to the Python path + @type list of str + """ + self.reader = EricJsonReader(name="Unittest Reader", parent=self) + self.reader.dataReceived.connect(self.__processData) + + super().start(config, pythonpath) + + 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. + """ + self.reader.close() + + output = self.readAllOutput() + self.testFinished.emit([], output) + + @pyqtSlot(object) + def __processData(self, data): + """ + Private slot to process the received data. + + @param data data object received + @type dict + """ + # error collecting tests + if data["event"] == "collecterror": + self.collectError.emit([("", data["error"])]) + + # tests collected + elif data["event"] == "collected": + self.collected.emit([ + (t["id"], t["name"], t["description"]) for t in data["tests"] + ]) + + # test started + elif data["event"] == "started": + self.startTest.emit( + (data["id"], data["name"], data["description"]) + ) + + # test result + elif data["event"] == "result": + filename, lineno = None, None + tracebackLines = [] + if "traceback" in data: + # get the error info + tracebackLines = data["traceback"].splitlines() + # find the last entry matching the pattern + for index in range(len(tracebackLines) - 1, -1, -1): + fmatch = re.search(r'File "(.*?)", line (\d*?),.*', + tracebackLines[index]) + if fmatch: + break + if fmatch: + filename = fmatch.group(1) + lineno = int(fmatch.group(2)) + + if "shortmsg" in data: + message = data["shortmsg"] + elif tracebackLines: + message = tracebackLines[-1].split(":", 1)[1].strip() + else: + message = "" + + self.testResult.emit(TestResult( + category=self.__statusCategoryMapping[data["status"]], + status=self.__statusDisplayMapping[data["status"]], + name=data["name"], + id=data["id"], + description=data["description"], + message=message, + extra=tracebackLines, + duration=( + data["duration_ms"] if "duration_ms" in data else None + ), + filename=filename, + lineno=lineno, + subtestResult=data["subtest"] if "subtest" in data else False + )) + + # test run finished + elif data["event"] == "finished": + self.testRunFinished.emit(data["tests"], data["duration_s"]) + + # coverage data + elif data["event"] == "coverage": + self.coverageDataSaved.emit(data["file"])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/Interfaces/UnittestRunner.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,423 @@ +# -*- 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 os +import sys +import time +import unittest + + +sys.path.insert( + 2, + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +) + + +class EricTestResult(unittest.TestResult): + """ + Class implementing a TestResult derivative to send the data via a network + connection. + """ + def __init__(self, writer, failfast): + """ + Constructor + + @param writer reference to the object to write the results to + @type EricJsonWriter + @param failfast flag indicating to stop at the first error + @type bool + """ + super().__init__() + self.__writer = writer + self.failfast = failfast + self.__testsRun = 0 + + self.__currentTestStatus = {} + + def addFailure(self, test, err): + """ + Public method called if a test failed. + + @param test reference to the test object + @type TestCase + @param err tuple containing the exception data like sys.exc_info + (exception type, exception instance, traceback) + @type tuple + """ + super().addFailure(test, err) + tracebackLines = self._exc_info_to_string(err, test) + + self.__currentTestStatus.update({ + "status": "failure", + "traceback": tracebackLines, + }) + + def addError(self, test, err): + """ + Public method called if a test errored. + + @param test reference to the test object + @type TestCase + @param err tuple containing the exception data like sys.exc_info + (exception type, exception instance, traceback) + @type tuple + """ + super().addError(test, err) + tracebackLines = self._exc_info_to_string(err, test) + + self.__currentTestStatus.update({ + "status": "error", + "traceback": tracebackLines, + }) + + def addSkip(self, test, reason): + """ + Public method called if a test was skipped. + + @param test reference to the test object + @type TestCase + @param reason reason for skipping the test + @type str + """ + super().addSkip(test, reason) + + self.__currentTestStatus.update({ + "status": "skipped", + "shortmsg": reason, + }) + + def addExpectedFailure(self, test, err): + """ + Public method called if a test failed expected. + + @param test reference to the test object + @type TestCase + @param err tuple containing the exception data like sys.exc_info + (exception type, exception instance, traceback) + @type tuple + """ + super().addExpectedFailure(test, err) + tracebackLines = self._exc_info_to_string(err, test) + + self.__currentTestStatus.update({ + "status": "expected failure", + "traceback": tracebackLines, + }) + + def addUnexpectedSuccess(self, test): + """ + Public method called if a test succeeded expectedly. + + @param test reference to the test object + @type TestCase + """ + super().addUnexpectedSuccess(test) + + self.__currentTestStatus["status"] = "unexpected success" + + def addSubTest(self, test, subtest, err): + """ + Public method called for each subtest to record its result. + + @param test reference to the test object + @type TestCase + @param subtest reference to the subtest object + @type TestCase + @param err tuple containing the exception data like sys.exc_info + (exception type, exception instance, traceback) + @type tuple + """ + if err is not None: + super().addSubTest(test, subtest, err) + tracebackLines = self._exc_info_to_string(err, test) + status = ( + "failure" + if issubclass(err[0], test.failureException) else + "error" + ) + + # record the last subtest fail status as the overall status + self.__currentTestStatus["status"] = status + + self.__writer.write({ + "event": "result", + "status": status, + "name": str(subtest), + "id": subtest.id(), + "description": subtest.shortDescription(), + "traceback": tracebackLines, + "subtest": True, + }) + + if self.failfast: + self.stop() + else: + self.__writer.write({ + "event": "result", + "status": "success", + "name": str(subtest), + "id": subtest.id(), + "description": subtest.shortDescription(), + "subtest": True, + }) + + def startTest(self, test): + """ + Public method called at the start of a test. + + @param test reference to the test object + @type TestCase + """ + super().startTest(test) + + self.__testsRun += 1 + self.__currentTestStatus = { + "event": "result", + "status": "success", + "name": str(test), + "id": test.id(), + "description": test.shortDescription(), + "subtest": False, + } + + self.__writer.write({ + "event": "started", + "name": str(test), + "id": test.id(), + "description": test.shortDescription(), + }) + + self.__startTime = time.monotonic_ns() + + def stopTest(self, test): + """ + Public method called at the end of a test. + + @param test reference to the test object + @type TestCase + """ + stopTime = time.monotonic_ns() + duration = (stopTime - self.__startTime) / 1_000_000 # ms + + super().stopTest(test) + + self.__currentTestStatus["duration_ms"] = duration + self.__writer.write(self.__currentTestStatus) + + def startTestRun(self): + """ + Public method called once before any tests are executed. + """ + self.__totalStartTime = time.monotonic_ns() + self.__testsRun = 0 + + def stopTestRun(self): + """ + Public method called once after all tests are executed. + """ + stopTime = time.monotonic_ns() + duration = (stopTime - self.__totalStartTime) / 1_000_000_000 # s + + self.__writer.write({ + "event": "finished", + "duration_s": duration, + "tests": self.__testsRun, + }) + + +def _assembleTestCasesList(suite): + """ + Protected function to assemble a list of test cases included in a test + suite. + + @param suite test suite to be inspected + @type unittest.TestSuite + @return list of tuples containing the test case ID, the string + representation and the short description + @rtype list of tuples of (str, str) + """ + testCases = [] + for test in suite: + if isinstance(test, unittest.TestSuite): + testCases.extend(_assembleTestCasesList(test)) + else: + testId = test.id() + if ( + "ModuleImportFailure" not in testId and + "LoadTestsFailure" not in testId and + "_FailedTest" not in testId + ): + testCases.append( + (testId, str(test), test.shortDescription()) + ) + return testCases + + +def runtest(argv): + """ + Function to run the tests. + + @param argv list of command line parameters. + @type list of str + """ + from EricNetwork.EricJsonStreamWriter import EricJsonWriter + writer = EricJsonWriter(argv[0], int(argv[1])) + del argv[:2] + + # process arguments + if argv[0] == "discover": + discover = True + argv.pop(0) + if argv[0] == "--start-directory": + discoveryStart = argv[1] + del argv[:2] + else: + discover = False + discoveryStart = "" + + failfast = "--failfast" in argv + if failfast: + argv.remove("--failfast") + + coverage = "--cover" in argv + if coverage: + argv.remove("--cover") + coverageErase = "--cover-erase" in argv + if coverageErase: + argv.remove("--cover-erase") + + if argv and argv[0] == "--failed-only": + if discover: + testFileName = "" + failed = argv[1:] + else: + testFileName = argv[1] + failed = argv[2:] + else: + failed = [] + if discover: + testFileName = testName = "" + else: + testFileName, testName = argv[:2] + del argv[:2] + + testCases = argv[:] + + if testFileName: + sys.path.insert(1, os.path.dirname(os.path.abspath(testFileName))) + elif discoveryStart: + sys.path.insert(1, os.path.abspath(discoveryStart)) + + try: + testLoader = unittest.TestLoader() + if discover and not failed: + if testCases: + test = testLoader.loadTestsFromNames(testCases) + else: + test = testLoader.discover(discoveryStart) + else: + if testFileName: + module = __import__(os.path.splitext( + os.path.basename(testFileName))[0]) + else: + module = None + if failed: + if module: + failed = [t.split(".", 1)[1] + for t in failed] + test = testLoader.loadTestsFromNames( + failed, module) + else: + test = testLoader.loadTestsFromName( + testName, module) + except Exception as err: + print("Exception:", str(err)) + writer.write({ + "event": "collecterror", + "error": str(err), + }) + sys.exit(1) + + collectedTests = { + "event": "collected", + "tests": [ + {"id": id, "name": name, "description": desc} + for id, name, desc in _assembleTestCasesList(test) + ] + } + writer.write(collectedTests) + + # setup test coverage + if coverage: + if discover: + covname = os.path.join(discoveryStart, "unittest") + elif testFileName: + covname = os.path.splitext( + os.path.abspath(testFileName))[0] + else: + covname = "unittest" + covDataFile = "{0}.coverage".format(covname) + if not os.path.isabs(covDataFile): + covDataFile = os.path.abspath(covDataFile) + + from DebugClients.Python.coverage import coverage as cov + cover = cov(data_file=covDataFile) + if coverageErase: + cover.erase() + else: + cover = None + + testResult = EricTestResult(writer, failfast) + startTestRun = getattr(testResult, 'startTestRun', None) + if startTestRun is not None: + startTestRun() + try: + if cover: + cover.start() + test.run(testResult) + finally: + if cover: + cover.stop() + cover.save() + writer.write({ + "event": "coverage", + "file": covDataFile, + }) + stopTestRun = getattr(testResult, 'stopTestRun', None) + if stopTestRun is not None: + stopTestRun() + + writer.close() + sys.exit(0) + +if __name__ == '__main__': + if len(sys.argv) > 1: + command = sys.argv[1] + if command == "installed": + sys.exit(0) + + elif command == "versions": + import platform + versions = { + "name": "unittest", + "version": platform.python_version(), + "plugins": [], + } + print(json.dumps(versions)) + sys.exit(0) + + elif command == "runtest": + runtest(sys.argv[2:]) + sys.exit(0) + + sys.exit(42) + +# +# eflag: noqa = M801
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/Interfaces/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,21 @@ +# -*- 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, +) + +FrameworkNames = ( + UnittestExecutor.name, + PytestExecutor.name, +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/TestResultsTree.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,611 @@ +# -*- 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. +""" + +import contextlib +import copy +import locale + +from collections import Counter +from operator import attrgetter + +from PyQt6.QtCore import ( + pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, + QModelIndex, QPoint +) +from PyQt6.QtGui import QBrush, QColor +from PyQt6.QtWidgets import QMenu, QTreeView + +from EricWidgets.EricApplication import ericApp + +import Preferences + +from .Interfaces.TestExecutorBase import TestResultCategory + +TopLevelId = 2 ** 32 - 1 + + +class TestResultsModel(QAbstractItemModel): + """ + Class implementing the item model containing the test data. + + @signal summary(str) emitted whenever the model data changes. The element + is a summary of the test results of the model. + """ + summary = pyqtSignal(str) + + Headers = [ + QCoreApplication.translate("TestResultsModel", "Status"), + QCoreApplication.translate("TestResultsModel", "Name"), + QCoreApplication.translate("TestResultsModel", "Message"), + QCoreApplication.translate("TestResultsModel", "Duration [ms]"), + ] + + StatusColumn = 0 + NameColumn = 1 + MessageColumn = 2 + DurationColumn = 3 + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (defaults to None) + @type QObject (optional) + """ + super().__init__(parent) + + if ericApp().usesDarkPalette(): + self.__backgroundColors = { + TestResultCategory.RUNNING: None, + TestResultCategory.FAIL: QBrush(QColor("#880000")), + TestResultCategory.OK: QBrush(QColor("#005500")), + TestResultCategory.SKIP: QBrush(QColor("#3f3f3f")), + TestResultCategory.PENDING: QBrush(QColor("#004768")), + } + else: + self.__backgroundColors = { + TestResultCategory.RUNNING: None, + TestResultCategory.FAIL: QBrush(QColor("#ff8080")), + TestResultCategory.OK: QBrush(QColor("#c1ffba")), + TestResultCategory.SKIP: QBrush(QColor("#c5c5c5")), + TestResultCategory.PENDING: QBrush(QColor("#6fbaff")), + } + + self.__testResults = [] + + def index(self, row, column, parent=QModelIndex()): + """ + Public method to generate an index for the given row and column to + identify the item. + + @param row row for the index + @type int + @param column column for the index + @type int + @param parent index of the parent item (defaults to QModelIndex()) + @type QModelIndex (optional) + @return index for the item + @rtype QModelIndex + """ + if not self.hasIndex(row, column, parent): # check bounds etc. + return QModelIndex() + + if not parent.isValid(): + # top level item + return self.createIndex(row, column, TopLevelId) + else: + testResultIndex = parent.row() + return self.createIndex(row, column, testResultIndex) + + def data(self, index, role): + """ + Public method to get the data for the various columns and roles. + + @param index index of the data to be returned + @type QModelIndex + @param role role designating the data to return + @type Qt.ItemDataRole + @return requested data item + @rtype Any + """ + if not index.isValid(): + return None + + row = index.row() + column = index.column() + idx = index.internalId() + + if role == Qt.ItemDataRole.DisplayRole: + if idx != TopLevelId: + if bool(self.__testResults[idx].extra): + return self.__testResults[idx].extra[index.row()] + else: + return None + elif column == TestResultsModel.StatusColumn: + return self.__testResults[row].status + elif column == TestResultsModel.NameColumn: + return self.__testResults[row].name + elif column == TestResultsModel.MessageColumn: + return self.__testResults[row].message + elif column == TestResultsModel.DurationColumn: + duration = self.__testResults[row].duration + return ( + "" + if duration is None else + locale.format_string("%.2f", duration, grouping=True) + ) + elif role == Qt.ItemDataRole.ToolTipRole: + if idx == TopLevelId and column == TestResultsModel.NameColumn: + return self.__testResults[row].name + elif role == Qt.ItemDataRole.FontRole: + if idx != TopLevelId: + return Preferences.getEditorOtherFonts("MonospacedFont") + elif role == Qt.ItemDataRole.BackgroundRole: + if idx == TopLevelId: + testResult = self.__testResults[row] + with contextlib.suppress(KeyError): + return self.__backgroundColors[testResult.category] + elif role == Qt.ItemDataRole.TextAlignmentRole: + if idx == TopLevelId and column == TestResultsModel.DurationColumn: + return Qt.AlignmentFlag.AlignRight + elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ + if idx == TopLevelId: + testresult = self.__testResults[row] + return (testresult.filename, testresult.lineno) + + return None + + 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 parent(self, index): + """ + Public method to get the parent of the item pointed to by index. + + @param index index of the item + @type QModelIndex + @return index of the parent item + @rtype QModelIndex + """ + if not index.isValid(): + return QModelIndex() + + idx = index.internalId() + if idx == TopLevelId: + return QModelIndex() + else: + return self.index(idx, 0) + + 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 and + self.__testResults[parent.row()].extra is not None + ): + 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() + + self.summary.emit("") + + def sort(self, column, order): + """ + Public method to sort the model data by column in order. + + @param column sort column number + @type int + @param order sort order + @type Qt.SortOrder + """ # __IGNORE_WARNING_D234r__ + def durationKey(result): + """ + Function to generate a key for duration sorting + + @param result result object + @type TestResult + @return sort key + @rtype float + """ + return result.duration or -1.0 + + self.beginResetModel() + reverse = order == Qt.SortOrder.DescendingOrder + if column == TestResultsModel.StatusColumn: + self.__testResults.sort(key=attrgetter('category', 'status'), + reverse=reverse) + elif column == TestResultsModel.NameColumn: + self.__testResults.sort(key=attrgetter('name'), reverse=reverse) + elif column == TestResultsModel.MessageColumn: + self.__testResults.sort(key=attrgetter('message'), reverse=reverse) + elif column == TestResultsModel.DurationColumn: + self.__testResults.sort(key=durationKey, reverse=reverse) + self.endResetModel() + + def getTestResults(self): + """ + Public method to get the list of test results managed by the model. + + @return list of test results managed by the model + @rtype list of TestResult + """ + return copy.deepcopy(self.__testResults) + + def setTestResults(self, testResults): + """ + Public method to set the list of test results of the model. + + @param testResults test results to be managed by the model + @type list of TestResult + """ + self.beginResetModel() + self.__testResults = copy.deepcopy(testResults) + self.endResetModel() + + self.summary.emit(self.__summary()) + + def addTestResults(self, testResults): + """ + Public method to add test results to the ones already managed by the + model. + + @param testResults test results to be added to the model + @type list of TestResult + """ + firstRow = len(self.__testResults) + lastRow = firstRow + len(testResults) - 1 + self.beginInsertRows(QModelIndex(), firstRow, lastRow) + self.__testResults.extend(testResults) + self.endInsertRows() + + self.summary.emit(self.__summary()) + + def updateTestResults(self, testResults): + """ + Public method to update the data of managed test result items. + + @param testResults test results to be updated + @type list of TestResult + """ + minIndex = None + maxIndex = None + + testResultsToBeAdded = [] + + for testResult in testResults: + for (index, currentResult) in enumerate(self.__testResults): + if currentResult.id == testResult.id: + self.__testResults[index] = testResult + if minIndex is None: + minIndex = index + maxIndex = index + else: + minIndex = min(minIndex, index) + maxIndex = max(maxIndex, index) + + break + else: + # Test result with given id was not found. + # Just add it to the list (could be a sub test) + testResultsToBeAdded.append(testResult) + + if minIndex is not None: + self.dataChanged.emit( + self.index(minIndex, 0), + self.index(maxIndex, len(TestResultsModel.Headers) - 1) + ) + + self.summary.emit(self.__summary()) + + if testResultsToBeAdded: + self.addTestResults(testResultsToBeAdded) + + def getFailedTests(self): + """ + Public method to extract the test ids of all failed tests. + + @return test ids of all failed tests + @rtype list of str + """ + failedIds = [ + res.id for res in self.__testResults if ( + res.category == TestResultCategory.FAIL and + not res.subtestResult + ) + ] + return failedIds + + def __summary(self): + """ + Private method to generate a test results summary text. + + @return test results summary text + @rtype str + """ + if len(self.__testResults) == 0: + return self.tr("No results to show") + + counts = Counter(res.category for res in self.__testResults) + if all( + counts[category] == 0 + for category in (TestResultCategory.FAIL, TestResultCategory.OK, + TestResultCategory.SKIP) + ): + return self.tr("Collected %n test(s)", "", len(self.__testResults)) + + return self.tr( + "%n test(s)/subtest(s) total, {0} failed, {1} passed," + " {2} skipped, {3} pending", + "", len(self.__testResults) + ).format( + counts[TestResultCategory.FAIL], + counts[TestResultCategory.OK], + counts[TestResultCategory.SKIP], + counts[TestResultCategory.PENDING] + ) + + +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) + + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + + # connect signals and slots + self.doubleClicked.connect(self.__gotoTestDefinition) + self.customContextMenuRequested.connect(self.__showContextMenu) + + self.header().sortIndicatorChanged.connect(self.sortByColumn) + self.header().sortIndicatorChanged.connect( + lambda column, order: self.header().setSortIndicatorShown(True)) + + def reset(self): + """ + Public method to reset the internal state of the view. + """ + super().reset() + + self.resizeColumns() + self.spanFirstColumn(0, self.model().rowCount() - 1) + + def rowsInserted(self, parent, startRow, endRow): + """ + Public method called when rows are inserted. + + @param parent model index of the parent item + @type QModelIndex + @param startRow first row been inserted + @type int + @param endRow last row been inserted + @type int + """ + super().rowsInserted(parent, startRow, endRow) + + self.resizeColumns() + self.spanFirstColumn(startRow, endRow) + + def dataChanged(self, topLeft, bottomRight, roles=[]): + """ + Public method called when the model data has changed. + + @param topLeft index of the top left element + @type QModelIndex + @param bottomRight index of the bottom right element + @type QModelIndex + @param roles list of roles changed (defaults to []) + @type list of Qt.ItemDataRole (optional) + """ + super().dataChanged(topLeft, bottomRight, roles) + + self.resizeColumns() + while topLeft.parent().isValid(): + topLeft = topLeft.parent() + while bottomRight.parent().isValid(): + bottomRight = bottomRight.parent() + self.spanFirstColumn(topLeft.row(), bottomRight.row()) + + def resizeColumns(self): + """ + Public method to resize the columns to their contents. + """ + for column in range(self.model().columnCount()): + self.resizeColumnToContents(column) + + def spanFirstColumn(self, startRow, endRow): + """ + Public method to make the first column span the row for second level + items. + + These items contain the test results. + + @param startRow index of the first row to span + @type QModelIndex + @param endRow index of the last row (including) to span + @type QModelIndex + """ + model = self.model() + for row in range(startRow, endRow + 1): + index = model.index(row, 0) + for i in range(model.rowCount(index)): + self.setFirstColumnSpanned(i, index, True) + + def __canonicalIndex(self, index): + """ + Private method to create the canonical index for a given index. + + The canonical index is the index of the first column of the test + result entry (i.e. the top-level item). If the index is invalid, + None is returned. + + @param index index to determine the canonical index for + @type QModelIndex + @return index of the firt column of the associated top-level item index + @rtype QModelIndex + """ + if not index.isValid(): + return None + + while index.parent().isValid(): # find the top-level node + index = index.parent() + index = index.sibling(index.row(), 0) # go to first column + return index + + @pyqtSlot(QModelIndex) + def __gotoTestDefinition(self, index): + """ + Private slot to show the test definition. + + @param index index for the double-clicked item + @type QModelIndex + """ + cindex = self.__canonicalIndex(index) + filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole) + if filename is not None: + if lineno is None: + lineno = 1 + self.goto.emit(filename, lineno) + + @pyqtSlot(QPoint) + def __showContextMenu(self, pos): + """ + Private slot to show the context menu. + + @param pos relative position for the context menu + @type QPoint + """ + index = self.indexAt(pos) + cindex = self.__canonicalIndex(index) + + contextMenu = ( + self.__createContextMenu(cindex) + if cindex else + self.__createBackgroundContextMenu() + ) + contextMenu.exec(self.mapToGlobal(pos)) + + def __createContextMenu(self, index): + """ + Private method to create a context menu for the item pointed to by the + given index. + + @param index index of the item + @type QModelIndex + @return created context menu + @rtype QMenu + """ + menu = QMenu(self) + if self.isExpanded(index): + menu.addAction(self.tr("Collapse"), + lambda: self.collapse(index)) + else: + act = menu.addAction(self.tr("Expand"), + lambda: self.expand(index)) + act.setEnabled(self.model().hasChildren(index)) + menu.addSeparator() + + act = menu.addAction(self.tr("Show Source"), + lambda: self.__gotoTestDefinition(index)) + act.setEnabled( + self.model().data(index, Qt.ItemDataRole.UserRole) is not None + ) + menu.addSeparator() + + menu.addAction(self.tr("Collapse All"), self.collapseAll) + menu.addAction(self.tr("Expand All"), self.expandAll) + + return menu + + def __createBackgroundContextMenu(self): + """ + Private method to create a context menu for the background. + + @return created context menu + @rtype QMenu + """ + menu = QMenu(self) + menu.addAction(self.tr("Collapse All"), self.collapseAll) + menu.addAction(self.tr("Expand All"), self.expandAll) + + return menu + +# +# eflag: noqa = M821, M822
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/TestingWidget.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,1024 @@ +# -*- 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 + + +# TODO: add a "Show Coverage" function using PyCoverageDialog + +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.__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) + + self.__project = None + + self.__discoverHistory = [] + self.__fileHistory = [] + self.__testNameHistory = [] + self.__recentFramework = "" + self.__recentEnvironment = "" + self.__failedTests = [] + + self.__editors = [] + self.__testExecutor = None + + # connect some signals + self.frameworkComboBox.currentIndexChanged.connect( + self.__resetResults) + 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): + """ + Public slot to set the given test file as the current one. + + @param testFile path of the test file + @type str + """ + if testFile: + self.__insertTestFile(testFile) + + self.discoverCheckBox.setChecked(not bool(testFile)) + + 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) + + # 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 = "" + # TODO: implement the handling of the 'Show Coverage' button + + 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) + + @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 == TestingWidgetModes.RUNNING: + return + + self.__recentEnvironment = self.venvComboBox.currentText() + self.__recentFramework = self.frameworkComboBox.currentText() + + self.__failedTests = ( + self.__resultsModel.getFailedTests() + if failedOnly else + [] + ) + discover = self.discoverCheckBox.isChecked() + if discover: + discoveryStart = self.discoveryPicker.currentText() + testFileName = "" + testName = "" + + if discoveryStart: + self.__insertDiscovery(discoveryStart) + else: + discoveryStart = "" + testFileName = self.testsuitePicker.currentText() + if testFileName: + self.__insertTestFile(testFileName) + testName = self.testComboBox.currentText() + if testName: + self.__insertTestName(testName) + if testFileName and not testName: + testName = "suite" + + self.sbLabel.setText(self.tr("Preparing Testsuite")) + QCoreApplication.processEvents() + + interpreter = self.__venvManager.getVirtualenvInterpreter( + self.__recentEnvironment) + config = TestConfig( + interpreter=interpreter, + discover=self.discoverCheckBox.isChecked(), + discoveryStart=discoveryStart, + testFilename=testFileName, + testName=testName, + failFast=self.failfastCheckBox.isChecked(), + failedOnly=failedOnly, + collectCoverage=self.coverageCheckBox.isChecked(), + eraseCoverage=self.coverageEraseCheckBox.isChecked(), + ) + + 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 and test name + of collected tests + @type list of tuple of (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.setTestResults(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 unit tests." + "</p><p>{0}</p>" + ).format("<br/>".join(error.splitlines())) + ) + + if testResults: + self.__resultsModel.addTestResults(testResults) + + @pyqtSlot(tuple) + def __testStarted(self, test): + """ + Private slot handling the 'startTest' signal of the executor. + + @param test tuple containing the id, name and short description of the + tests about to be run + @type tuple of (str, str, str) + """ + self.__resultsModel.updateTestResults([ + TestResult( + category=TestResultCategory.RUNNING, + status=self.tr("running"), + id=test[0], + name=test[1], + message="" if test[2] is None else test[2], + ) + ]) + + @pyqtSlot(TestResult) + def __processTestResult(self, result): + """ + Private slot to handle the receipt of a test result object. + + @param result test result object + @type TestResult + """ + if not result.subtestResult: + self.__runCount += 1 + self.__updateProgress() + + self.__resultsModel.updateTestResults([result]) + + @pyqtSlot(list, str) + def __testProcessFinished(self, results, output): + """ + Private slot to handle the 'testFinished' signal of the executor. + + @param results list of test result objects (if not sent via the + 'testResult' signal + @type list of TestResult + @param output string containing the test process output (if any) + @type str + """ + self.__setStoppedMode() + self.__testExecutor = None + + @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() + + @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 + + # TODO: implement the handling of the 'Show Coverage' button + + @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): + """ + 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 + @type int + """ + 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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/Testing/TestingWidget.ui Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,547 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TestingWidget</class> + <widget class="QWidget" name="TestingWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>850</width> + <height>700</height> + </rect> + </property> + <property name="windowTitle"> + <string>Testing</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"> + <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>TestingWidget</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>TestingWidget</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/Testing/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing testing functionality and interface to various test +frameworks. +"""
--- a/eric7/Tools/TrayStarter.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Tools/TrayStarter.py Mon May 16 19:46:51 2022 +0200 @@ -106,7 +106,7 @@ self.tr("Translations Previewer"), self.__startTRPreviewer) self.__menu.addAction( UI.PixmapCache.getIcon("unittest"), - self.tr("Unittest"), self.__startUnittest) + self.tr("Testing"), self.__startTesting) self.__menu.addSeparator() self.__menu.addAction( @@ -369,11 +369,11 @@ """ self.__startProc("eric7_trpreviewer.py") - def __startUnittest(self): + def __startTesting(self): """ - Private slot to start the eric unittest dialog. + Private slot to start the eric testing dialog. """ - self.__startProc("eric7_unittest.py") + self.__startProc("eric7_testing.py") def __startDiff(self): """
--- a/eric7/UI/Browser.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/UI/Browser.py Mon May 16 19:46:51 2022 +0200 @@ -69,7 +69,7 @@ @signal svgFile(filename) emitted to open a SVG file (str) @signal umlFile(filename) emitted to open an eric UML file (str) @signal binaryFile(filename) emitted to open a file as binary (str) - @signal unittestOpen(filename) emitted to open a Python file for a + @signal testFile(filename) emitted to open a Python file for a unit test (str) """ sourceFile = pyqtSignal((str, ), (str, int), (str, list), (str, int, str)) @@ -83,7 +83,7 @@ svgFile = pyqtSignal(str) umlFile = pyqtSignal(str) binaryFile = pyqtSignal(str) - unittestOpen = pyqtSignal(str) + testFile = pyqtSignal(str) def __init__(self, parent=None): """ @@ -138,7 +138,7 @@ """ suffix) are identified in the hierarchies with a Python""" """ icon. The right mouse button will popup a menu which lets""" """ you open the file in a Source Viewer window, open the file""" - """ for debugging or use it for a unittest run.</p>""" + """ for debugging or use it for a test run.</p>""" """<p>The context menu of a class, function or method allows you""" """ to open the file defining this class, function or method and""" """ will ensure, that the correct source line is visible.</p>""" @@ -232,9 +232,9 @@ self.sourceMenu = QMenu(self) self.sourceMenu.addAction( QCoreApplication.translate('Browser', 'Open'), self._openItem) - self.unittestAct = self.sourceMenu.addAction( - QCoreApplication.translate('Browser', 'Run unittest...'), - self.handleUnittest) + self.testingAct = self.sourceMenu.addAction( + QCoreApplication.translate('Browser', 'Run Test...'), + self.handleTesting) self.sourceMenu.addSeparator() self.mimeTypeAct = self.sourceMenu.addAction( QCoreApplication.translate('Browser', 'Show Mime-Type'), @@ -399,9 +399,9 @@ if isinstance(itm, BrowserFileItem): if itm.isPython3File(): if itm.fileName().endswith('.py'): - self.unittestAct.setEnabled(True) + self.testingAct.setEnabled(True) else: - self.unittestAct.setEnabled(False) + self.testingAct.setEnabled(False) self.sourceMenu.popup(coord) else: self.editPixmapAct.setVisible(itm.isPixmapFile()) @@ -640,9 +640,9 @@ # remember the current state Preferences.setUI("BrowsersListHiddenFiles", checked) - def handleUnittest(self): + def handleTesting(self): """ - Public slot to handle the unittest popup menu entry. + Public slot to handle the testing popup menu entry. """ try: index = self.currentIndex() @@ -652,7 +652,7 @@ pyfn = None if pyfn is not None: - self.unittestOpen.emit(pyfn) + self.testFile.emit(pyfn) def __newToplevelDir(self): """
--- a/eric7/UI/ClearPrivateDataDialog.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/UI/ClearPrivateDataDialog.py Mon May 16 19:46:51 2022 +0200 @@ -35,7 +35,7 @@ @return flags indicating which data to clear (recent files, recent projects, recent multi projects, - debug histories, shell histories, unittest histories, + debug histories, shell histories, test histories, VCS histories, private data of plugins) @rtype tuple of bool """ @@ -45,7 +45,7 @@ self.multiProjectsCheckBox.isChecked(), self.debugCheckBox.isChecked(), self.shellCheckBox.isChecked(), - self.unittestCheckBox.isChecked(), + self.testCheckBox.isChecked(), self.vcsCheckBox.isChecked(), self.pluginsCheckBox.isChecked(), )
--- a/eric7/UI/ClearPrivateDataDialog.ui Mon May 16 17:22:43 2022 +0200 +++ b/eric7/UI/ClearPrivateDataDialog.ui Mon May 16 19:46:51 2022 +0200 @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>400</width> - <height>263</height> + <height>293</height> </rect> </property> <property name="windowTitle"> @@ -83,12 +83,12 @@ </widget> </item> <item> - <widget class="QCheckBox" name="unittestCheckBox"> + <widget class="QCheckBox" name="testCheckBox"> <property name="toolTip"> - <string>Select to clear the unittest histories</string> + <string>Select to clear the test histories</string> </property> <property name="text"> - <string>Unittest histories</string> + <string>Test histories</string> </property> <property name="checked"> <bool>true</bool> @@ -146,7 +146,7 @@ <tabstop>multiProjectsCheckBox</tabstop> <tabstop>debugCheckBox</tabstop> <tabstop>shellCheckBox</tabstop> - <tabstop>unittestCheckBox</tabstop> + <tabstop>testCheckBox</tabstop> <tabstop>vcsCheckBox</tabstop> <tabstop>pluginsCheckBox</tabstop> </tabstops>
--- a/eric7/UI/UserInterface.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/UI/UserInterface.py Mon May 16 19:46:51 2022 +0200 @@ -341,7 +341,7 @@ # set a few dialog members for non-modal dialogs created on demand self.programsDialog = None self.shortcutsDialog = None - self.__unittestWidget = None + self.__testingWidget = None self.findFileNameDialog = None self.diffDlg = None self.compareDlg = None @@ -372,8 +372,8 @@ self.viewmanager.openSourceFile) self.projectBrowser.psBrowser.closeSourceWindow.connect( self.viewmanager.closeWindow) - self.projectBrowser.psBrowser.unittestOpen.connect( - self.__unittestScript) + self.projectBrowser.psBrowser.testFile.connect( + self.__startTestScript) self.projectBrowser.pfBrowser.designerFile.connect(self.__designer) self.projectBrowser.pfBrowser.sourceFile.connect( @@ -531,7 +531,7 @@ self.browser.svgFile.connect(self.__showSvg) self.browser.umlFile.connect(self.__showUml) self.browser.binaryFile.connect(self.__openHexEditor) - self.browser.unittestOpen.connect(self.__unittestScript) + self.browser.testFile.connect(self.__startTestScript) self.browser.trpreview.connect(self.__TRPreviewer) self.debuggerUI.debuggingStarted.connect( @@ -661,6 +661,7 @@ # Initialize the instance variables. self.currentProg = None self.isProg = False + # TODO: rename/eliminate/rework these two related to unittest self.utEditorOpen = False self.utProjectOpen = False @@ -2510,82 +2511,81 @@ self.requestFeatureAct.triggered.connect(self.__requestFeature) self.actions.append(self.requestFeatureAct) - self.utActGrp = createActionGroup(self) - - self.utDialogAct = EricAction( - self.tr('Unittest'), + self.testingActGrp = createActionGroup(self) + + self.testingDialogAct = EricAction( + self.tr('Testing'), UI.PixmapCache.getIcon("unittest"), - self.tr('&Unittest...'), - 0, 0, self.utActGrp, 'unittest') - self.utDialogAct.setStatusTip(self.tr('Start unittest dialog')) - self.utDialogAct.setWhatsThis(self.tr( - """<b>Unittest</b>""" - """<p>Perform unit tests. The dialog gives the""" - """ ability to select and run a unittest suite or""" + self.tr('&Testing...'), + 0, 0, self.testingActGrp, 'unittest') + self.testingDialogAct.setStatusTip(self.tr('Start the testing dialog')) + self.testingDialogAct.setWhatsThis(self.tr( + """<b>Testing</b>""" + """<p>Perform test runss. The dialog gives the""" + """ ability to select and run a test suite or""" """auto discover them.</p>""" )) - self.utDialogAct.triggered.connect(self.__unittest) - self.actions.append(self.utDialogAct) + self.testingDialogAct.triggered.connect(self.__startTesting) + self.actions.append(self.testingDialogAct) - self.utRestartAct = EricAction( - self.tr('Unittest Restart'), + self.restartTestAct = EricAction( + self.tr('Restart Last Test'), UI.PixmapCache.getIcon("unittestRestart"), - self.tr('&Restart Unittest...'), - 0, 0, self.utActGrp, 'unittest_restart') - self.utRestartAct.setStatusTip(self.tr('Restart last unittest')) - self.utRestartAct.setWhatsThis(self.tr( - """<b>Restart Unittest</b>""" - """<p>Restart the unittest performed last.</p>""" + self.tr('&Restart Last Test...'), + 0, 0, self.testingActGrp, 'unittest_restart') + self.restartTestAct.setStatusTip(self.tr('Restarts the last test')) + self.restartTestAct.setWhatsThis(self.tr( + """<b>Restart Last Test</b>""" + """<p>Restarts the test performed last.</p>""" )) - self.utRestartAct.triggered.connect(self.__unittestRestart) - self.utRestartAct.setEnabled(False) - self.actions.append(self.utRestartAct) - - self.utRerunFailedAct = EricAction( - self.tr('Unittest Rerun Failed'), + self.restartTestAct.triggered.connect(self.__restartTest) + self.restartTestAct.setEnabled(False) + self.actions.append(self.restartTestAct) + + self.rerunFailedTestsAct = EricAction( + self.tr('Rerun Failed Tests'), UI.PixmapCache.getIcon("unittestRerunFailed"), self.tr('Rerun Failed Tests...'), - 0, 0, self.utActGrp, 'unittest_rerun_failed') - self.utRerunFailedAct.setStatusTip(self.tr( + 0, 0, self.testingActGrp, 'unittest_rerun_failed') + self.rerunFailedTestsAct.setStatusTip(self.tr( 'Rerun failed tests of the last run')) - self.utRerunFailedAct.setWhatsThis(self.tr( + self.rerunFailedTestsAct.setWhatsThis(self.tr( """<b>Rerun Failed Tests</b>""" - """<p>Rerun all tests that failed during the last unittest""" - """ run.</p>""" + """<p>Rerun all tests that failed during the last test run.</p>""" )) - self.utRerunFailedAct.triggered.connect(self.__unittestRerunFailed) - self.utRerunFailedAct.setEnabled(False) - self.actions.append(self.utRerunFailedAct) - - self.utScriptAct = EricAction( - self.tr('Unittest Script'), + self.rerunFailedTestsAct.triggered.connect(self.__rerunFailedTests) + self.rerunFailedTestsAct.setEnabled(False) + self.actions.append(self.rerunFailedTestsAct) + + self.testScriptAct = EricAction( + self.tr('Test Script'), UI.PixmapCache.getIcon("unittestScript"), - self.tr('Unittest &Script...'), - 0, 0, self.utActGrp, 'unittest_script') - self.utScriptAct.setStatusTip(self.tr( - 'Run unittest with current script')) - self.utScriptAct.setWhatsThis(self.tr( - """<b>Unittest Script</b>""" - """<p>Run unittest with current script.</p>""" + self.tr('Test &Script...'), + 0, 0, self.testingActGrp, 'unittest_script') + self.testScriptAct.setStatusTip(self.tr( + 'Run tests of the current script')) + self.testScriptAct.setWhatsThis(self.tr( + """<b>Test Script</b>""" + """<p>Run tests with the current script.</p>""" )) - self.utScriptAct.triggered.connect(self.__unittestScript) - self.utScriptAct.setEnabled(False) - self.actions.append(self.utScriptAct) - - self.utProjectAct = EricAction( - self.tr('Unittest Project'), + self.testScriptAct.triggered.connect(self.__startTestScript) + self.testScriptAct.setEnabled(False) + self.actions.append(self.testScriptAct) + + self.testProjectAct = EricAction( + self.tr('Test Project'), UI.PixmapCache.getIcon("unittestProject"), - self.tr('Unittest &Project...'), - 0, 0, self.utActGrp, 'unittest_project') - self.utProjectAct.setStatusTip(self.tr( - 'Run unittest with current project')) - self.utProjectAct.setWhatsThis(self.tr( - """<b>Unittest Project</b>""" - """<p>Run unittest with current project.</p>""" + self.tr('Test &Project...'), + 0, 0, self.testingActGrp, 'unittest_project') + self.testProjectAct.setStatusTip(self.tr( + 'Run tests of the current project')) + self.testProjectAct.setWhatsThis(self.tr( + """<b>Test Project</b>""" + """<p>Run test of the current project.</p>""" )) - self.utProjectAct.triggered.connect(self.__unittestProject) - self.utProjectAct.setEnabled(False) - self.actions.append(self.utProjectAct) + self.testProjectAct.triggered.connect(self.__startTestProject) + self.testProjectAct.setEnabled(False) + self.actions.append(self.testProjectAct) # check for Qt5 designer and linguist if Utilities.isWindowsPlatform(): @@ -3400,17 +3400,17 @@ ## Extras/Unittest menu ############################################################## - self.__menus["unittest"] = QMenu(self.tr('&Unittest'), self) - self.__menus["unittest"].setTearOffEnabled(True) - self.__menus["unittest"].addAction(self.utDialogAct) - self.__menus["unittest"].addSeparator() - self.__menus["unittest"].addAction(self.utRestartAct) - self.__menus["unittest"].addAction(self.utRerunFailedAct) - self.__menus["unittest"].addSeparator() - self.__menus["unittest"].addAction(self.utScriptAct) - self.__menus["unittest"].addAction(self.utProjectAct) - - self.__menus["extras"].addMenu(self.__menus["unittest"]) + self.__menus["testing"] = QMenu(self.tr('&Testing'), self) + self.__menus["testing"].setTearOffEnabled(True) + self.__menus["testing"].addAction(self.testingDialogAct) + self.__menus["testing"].addSeparator() + self.__menus["testing"].addAction(self.restartTestAct) + self.__menus["testing"].addAction(self.rerunFailedTestsAct) + self.__menus["testing"].addSeparator() + self.__menus["testing"].addAction(self.testScriptAct) + self.__menus["testing"].addAction(self.testProjectAct) + + self.__menus["extras"].addMenu(self.__menus["testing"]) self.__menus["extras"].addSeparator() ############################################################## @@ -3598,7 +3598,7 @@ multiprojecttb = self.multiProject.initToolbar(self.toolbarManager) projecttb, vcstb = self.project.initToolbars(self.toolbarManager) toolstb = QToolBar(self.tr("Tools"), self) - unittesttb = QToolBar(self.tr("Unittest"), self) + testingtb = QToolBar(self.tr("Testing"), self) bookmarktb = self.viewmanager.initBookmarkToolbar(self.toolbarManager) spellingtb = self.viewmanager.initSpellingToolbar(self.toolbarManager) settingstb = QToolBar(self.tr("Settings"), self) @@ -3607,21 +3607,21 @@ pluginstb = QToolBar(self.tr("Plugins"), self) toolstb.setIconSize(Config.ToolBarIconSize) - unittesttb.setIconSize(Config.ToolBarIconSize) + testingtb.setIconSize(Config.ToolBarIconSize) settingstb.setIconSize(Config.ToolBarIconSize) helptb.setIconSize(Config.ToolBarIconSize) profilestb.setIconSize(Config.ToolBarIconSize) pluginstb.setIconSize(Config.ToolBarIconSize) toolstb.setObjectName("ToolsToolbar") - unittesttb.setObjectName("UnittestToolbar") + testingtb.setObjectName("UnittestToolbar") settingstb.setObjectName("SettingsToolbar") helptb.setObjectName("HelpToolbar") profilestb.setObjectName("ProfilesToolbar") pluginstb.setObjectName("PluginsToolbar") toolstb.setToolTip(self.tr("Tools")) - unittesttb.setToolTip(self.tr("Unittest")) + testingtb.setToolTip(self.tr("Unittest")) settingstb.setToolTip(self.tr("Settings")) helptb.setToolTip(self.tr("Help")) profilestb.setToolTip(self.tr("Profiles")) @@ -3635,15 +3635,15 @@ filetb.insertAction(sep, self.newWindowAct) self.toolbarManager.addToolBar(filetb, filetb.windowTitle()) - # setup the unittest toolbar - unittesttb.addAction(self.utDialogAct) - unittesttb.addSeparator() - unittesttb.addAction(self.utRestartAct) - unittesttb.addAction(self.utRerunFailedAct) - unittesttb.addSeparator() - unittesttb.addAction(self.utScriptAct) - unittesttb.addAction(self.utProjectAct) - self.toolbarManager.addToolBar(unittesttb, unittesttb.windowTitle()) + # setup the testing toolbar + testingtb.addAction(self.testingDialogAct) + testingtb.addSeparator() + testingtb.addAction(self.restartTestAct) + testingtb.addAction(self.rerunFailedTestsAct) + testingtb.addSeparator() + testingtb.addAction(self.testScriptAct) + testingtb.addAction(self.testProjectAct) + self.toolbarManager.addToolBar(testingtb, testingtb.windowTitle()) # setup the tools toolbar if self.designer4Act is not None: @@ -3723,7 +3723,7 @@ self.addToolBar(helptb) self.addToolBar(bookmarktb) self.addToolBar(spellingtb) - self.addToolBar(unittesttb) + self.addToolBar(testingtb) self.addToolBar(profilestb) self.addToolBar(pluginstb) @@ -3734,7 +3734,7 @@ multiprojecttb.hide() helptb.hide() spellingtb.hide() - unittesttb.hide() + testingtb.hide() pluginstb.hide() # just add new toolbars to the end of the list @@ -3752,8 +3752,7 @@ ""] self.__toolbars["bookmarks"] = [bookmarktb.windowTitle(), bookmarktb, ""] - self.__toolbars["unittest"] = [unittesttb.windowTitle(), unittesttb, - ""] + self.__toolbars["testing"] = [testingtb.windowTitle(), testingtb, ""] self.__toolbars["view_profiles"] = [profilestb.windowTitle(), profilestb, ""] self.__toolbars["plugins"] = [pluginstb.windowTitle(), pluginstb, ""] @@ -5333,42 +5332,42 @@ if dlg.exec() == QDialog.DialogCode.Accepted: self.toolGroups, self.currentToolGroup = dlg.getToolGroups() - def __createUnitTestDialog(self): - """ - Private slot to generate the unit test dialog on demand. - """ - if self.__unittestWidget is None: - from Unittest.UnittestWidget import UnittestWidget - self.__unittestWidget = UnittestWidget() - self.__unittestWidget.unittestFile.connect( + def __createTestingDialog(self): + """ + Private slot to generate the testing dialog on demand. + """ + if self.__testingWidget is None: + from Testing.TestingWidget import TestingWidget + self.__testingWidget = TestingWidget() + self.__testingWidget.testFile.connect( self.viewmanager.setFileLine) - self.__unittestWidget.unittestStopped.connect( - self.__unittestStopped) - - def __unittestStopped(self): - """ - Private slot to handle the end of a unit test run. - """ - self.utRerunFailedAct.setEnabled( - self.__unittestWidget.hasFailedTests()) - self.utRestartAct.setEnabled(True) - - def __unittest(self): - """ - Private slot for displaying the unittest dialog. - """ - self.__createUnitTestDialog() - self.__unittestWidget.show() - self.__unittestWidget.raise_() + self.__testingWidget.testRunStopped.connect( + self.__testingStopped) + + def __testingStopped(self): + """ + Private slot to handle the end of a test run. + """ + self.rerunFailedTestsAct.setEnabled( + self.__testingWidget.hasFailedTests()) + self.restartTestAct.setEnabled(True) + + def __startTesting(self): + """ + Private slot for displaying the testing dialog. + """ + self.__createTestingDialog() + self.__testingWidget.show() + self.__testingWidget.raise_() @pyqtSlot() @pyqtSlot(str) - def __unittestScript(self, testFile=None): - """ - Private slot for displaying the unittest dialog and run the current + def __startTestScript(self, testFile=None): + """ + Private slot for displaying the testing dialog and run the current script. - @param testFile file containing the unit tests to be run + @param testFile file containing the tests to be run @type str """ if testFile is None: @@ -5380,16 +5379,16 @@ else: testFile = fn - self.__unittest() - self.__unittestWidget.setTestFile(testFile) - self.utRestartAct.setEnabled(False) - self.utRerunFailedAct.setEnabled(False) + self.__startTesting() + self.__testingWidget.setTestFile(testFile) + self.restartTestAct.setEnabled(False) + self.rerunFailedTestsAct.setEnabled(False) @pyqtSlot() - def __unittestProject(self): - """ - Private slot for displaying the unittest dialog and run the current - project. + def __startTestProject(self): + """ + Private slot for displaying the testing dialog and run the test for + the current project. """ testFile = None fn = self.project.getMainScript(True) @@ -5398,26 +5397,26 @@ if os.path.exists(tfn): testFile = tfn - self.__unittest() - self.__unittestWidget.setTestFile(testFile) - self.utRestartAct.setEnabled(False) - self.utRerunFailedAct.setEnabled(False) - - def __unittestRestart(self): - """ - Private slot to display the unittest dialog and rerun the last - unit test. - """ - self.__unittest() - self.__unittestWidget.startTests() - - def __unittestRerunFailed(self): - """ - Private slot to display the unittest dialog and rerun all failed tests + self.__startTesting() + self.__testingWidget.setTestFile(testFile) + self.restartTestAct.setEnabled(False) + self.rerunFailedTestsAct.setEnabled(False) + + def __restartTest(self): + """ + Private slot to display the testing dialog and rerun the last + test run. + """ + self.__startTesting() + self.__testingWidget.startTests() + + def __rerunFailedTests(self): + """ + Private slot to display the testing dialog and rerun all failed tests of the last run. """ - self.__unittest() - self.__unittestWidget.startTests(failedOnly=True) + self.__startTesting() + self.__testingWidget.startTests(failedOnly=True) @pyqtSlot() @pyqtSlot(str) @@ -6816,7 +6815,7 @@ if dlg.exec() == QDialog.DialogCode.Accepted: # recent files, recent projects, recent multi projects, # debug histories, shell histories - (files, projects, multiProjects, debug, shell, unittests, vcs, + (files, projects, multiProjects, debug, shell, testing, vcs, plugins) = dlg.getData() if files: # clear list of recently opened files @@ -6833,13 +6832,13 @@ if shell: # clear the shell histories self.shell.clearAllHistories() - if unittests: + if testing: # clear the unit test histories - if self.__unittestWidget is None: - from Unittest.UnittestWidget import clearSavedHistories + if self.__testingWidget is None: + from Testing.UnittestWidget import clearSavedHistories clearSavedHistories() else: - self.__unittestWidget.clearRecent() + self.__testingWidget.clearRecent() if vcs: # clear the VCS related histories self.pluginManager.clearPluginsPrivateData("version_control") @@ -6863,7 +6862,7 @@ self.__setWindowCaption(project=self.project.name) cap = self.__debugServer.getClientCapabilities( self.project.getProjectLanguage()) - self.utProjectAct.setEnabled(cap & HasUnittest) + self.testProjectAct.setEnabled(cap & HasUnittest) self.utProjectOpen = cap & HasUnittest def __projectClosed(self): @@ -6871,10 +6870,10 @@ Private slot to handle the projectClosed signal. """ self.__setWindowCaption(project="") - self.utProjectAct.setEnabled(False) + self.testProjectAct.setEnabled(False) if not self.utEditorOpen: - self.utRestartAct.setEnabled(False) - self.utRerunFailedAct.setEnabled(False) + self.restartTestAct.setEnabled(False) + self.rerunFailedTestsAct.setEnabled(False) self.utProjectOpen = False def __programChange(self, fn): @@ -6896,11 +6895,11 @@ Private slot to handle the lastEditorClosed signal. """ self.wizardsMenuAct.setEnabled(False) - self.utScriptAct.setEnabled(False) + self.testScriptAct.setEnabled(False) self.utEditorOpen = False if not self.utProjectOpen: - self.utRestartAct.setEnabled(False) - self.utRerunFailedAct.setEnabled(False) + self.restartTestAct.setEnabled(False) + self.rerunFailedTestsAct.setEnabled(False) self.__setWindowCaption(editor="") def __editorOpened(self, fn): @@ -6919,12 +6918,12 @@ if fn.endswith(exts): from Debugger.DebugClientCapabilities import HasUnittest cap = dbs.getClientCapabilities(language) - self.utScriptAct.setEnabled(cap & HasUnittest) + self.testScriptAct.setEnabled(cap & HasUnittest) self.utEditorOpen = cap & HasUnittest return if self.viewmanager.getOpenEditor(fn).isPyFile(): - self.utScriptAct.setEnabled(True) + self.testScriptAct.setEnabled(True) self.utEditorOpen = True def __checkActions(self, editor): @@ -6942,16 +6941,16 @@ if fn.endswith(exts): from Debugger.DebugClientCapabilities import HasUnittest cap = dbs.getClientCapabilities(language) - self.utScriptAct.setEnabled(cap & HasUnittest) + self.testScriptAct.setEnabled(cap & HasUnittest) self.utEditorOpen = cap & HasUnittest return if editor.isPyFile(): - self.utScriptAct.setEnabled(True) + self.testScriptAct.setEnabled(True) self.utEditorOpen = True return - self.utScriptAct.setEnabled(False) + self.testScriptAct.setEnabled(False) def __writeTasks(self): """
--- a/eric7/Unittest/Interfaces/PytestExecutor.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ -# -*- 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 - - -# TODO: implement 'pytest' support in PytestExecutor -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 {}
--- a/eric7/Unittest/Interfaces/PytestRunner.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -# -*- 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 - -# TODO: implement 'pytest' support in PytestRunner - - -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
--- a/eric7/Unittest/Interfaces/UTExecutorBase.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,262 +0,0 @@ -# -*- 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. - """ - RUNNING = 0 - FAIL = 1 - OK = 2 - SKIP = 3 - PENDING = 4 - - -@dataclass -class UTTestResult: - """ - Class containing the test result data. - """ - category: ResultCategory # result category - status: str # test status - name: str # test name - id: str # test id - description: str = "" # short description of test - message: str = "" # short result message - extra: list = None # additional information text - duration: float = None # test duration - filename: str = None # file name of a failed test - lineno: int = None # line number of a failed test - subtestResult: bool = False # flag indicating the result of a subtest - - -@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 - failedOnly: bool # run failed tests only - 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 tuple of (str, str, str)) emitted after all tests - have been collected. Tuple elements are the test id, the test name and - a short description of the test. - @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(tuple of (str, str, str) emitted before tests are run. - Tuple elements are test id, test name and short description. - @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 testRunAboutToBeStarted() emitted just before the test run will - be started. - @signal testRunFinished(int, float) emitted when the test run has finished. - The elements are the number of tests run and the duration in seconds - @signal stop() emitted when the test process is being stopped. - @signal coverageDataSaved(str) emitted after the coverage data was saved. - The element is the absolute path of the coverage data file. - """ - collected = pyqtSignal(list) - collectError = pyqtSignal(list) - startTest = pyqtSignal(tuple) - testResult = pyqtSignal(UTTestResult) - testFinished = pyqtSignal(list, str) - testRunAboutToBeStarted = pyqtSignal() - testRunFinished = pyqtSignal(int, float) - stop = pyqtSignal() - coverageDataSaved = pyqtSignal(str) - - module = "" - name = "" - runner = "" - - def __init__(self, testWidget): - """ - Constructor - - @param testWidget reference to the unit test widget - @type UnittestWidget - """ - super().__init__(testWidget) - - self.__process = None - - @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.testRunAboutToBeStarted.emit() - 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()
--- a/eric7/Unittest/Interfaces/UTFrameworkRegistry.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ -# -*- 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): - """ - 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 - @return test framework executor object - @rtype UTExecutorBase - """ - cls = self.__frameworks[framework] - return cls(widget) - - 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)
--- a/eric7/Unittest/Interfaces/UnittestExecutor.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,221 +0,0 @@ -# -*- 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 -import re - -from PyQt6.QtCore import pyqtSlot, QProcess - -from EricNetwork.EricJsonStreamReader import EricJsonReader - -from .UTExecutorBase import UTExecutorBase, UTTestResult, ResultCategory - - -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 __init__(self, testWidget): - """ - Constructor - - @param testWidget reference to the unit test widget - @type UnittestWidget - """ - super().__init__(testWidget) - - self.__statusCategoryMapping = { - "failure": ResultCategory.FAIL, - "error": ResultCategory.FAIL, - "skipped": ResultCategory.SKIP, - "expected failure": ResultCategory.OK, - "unexpected success": ResultCategory.FAIL, - "success": ResultCategory.OK, - } - - self.__statusDisplayMapping = { - "failure": self.tr("Failure"), - "error": self.tr("Error"), - "skipped": self.tr("Skipped"), - "expected failure": self.tr("Expected Failure"), - "unexpected success": self.tr("Unexpected Success"), - "success": self.tr("Success"), - } - - self.__testWidget = testWidget - - 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 - """ - args = [ - UnittestExecutor.runner, - "runtest", - self.reader.address(), - str(self.reader.port()), - ] - - if config.discover: - args.extend([ - "discover", - "--start-directory", - config.discoveryStart, - ]) - - if config.failFast: - args.append("--failfast") - - if config.collectCoverage: - args.append("--cover") - if config.eraseCoverage: - args.append("--cover-erase") - - if config.failedOnly: - args.append("--failed-only") - if config.testFilename: - args.append(config.testFilename) - args.extend(self.__testWidget.getFailedTests()) - - elif config.testFilename and config.testName: - args.append(config.testFilename) - args.append(config.testName) - - return args - - 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 - """ - self.reader = EricJsonReader(name="Unittest Reader", parent=self) - self.reader.dataReceived.connect(self.__processData) - - super().start(config, pythonpath) - - 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. - """ - self.reader.close() - - output = self.readAllOutput() - self.testFinished.emit([], output) - - @pyqtSlot(object) - def __processData(self, data): - """ - Private slot to process the received data. - - @param data data object received - @type dict - """ - # error collecting tests - if data["event"] == "collecterror": - self.collectError.emit([("", data["error"])]) - - # tests collected - elif data["event"] == "collected": - self.collected.emit([ - (t["id"], t["name"], t["description"]) for t in data["tests"] - ]) - - # test started - elif data["event"] == "started": - self.startTest.emit( - (data["id"], data["name"], data["description"]) - ) - - # test result - elif data["event"] == "result": - filename, lineno = None, None - tracebackLines = [] - if "traceback" in data: - # get the error info - tracebackLines = data["traceback"].splitlines() - # find the last entry matching the pattern - for index in range(len(tracebackLines) - 1, -1, -1): - fmatch = re.search(r'File "(.*?)", line (\d*?),.*', - tracebackLines[index]) - if fmatch: - break - if fmatch: - filename = fmatch.group(1) - lineno = int(fmatch.group(2)) - - if "shortmsg" in data: - message = data["shortmsg"] - elif tracebackLines: - message = tracebackLines[-1].split(":", 1)[1].strip() - else: - message = "" - - self.testResult.emit(UTTestResult( - category=self.__statusCategoryMapping[data["status"]], - status=self.__statusDisplayMapping[data["status"]], - name=data["name"], - id=data["id"], - description=data["description"], - message=message, - extra=tracebackLines, - duration=( - data["duration_ms"] if "duration_ms" in data else None - ), - filename=filename, - lineno=lineno, - subtestResult=data["subtest"] if "subtest" in data else False - )) - - # test run finished - elif data["event"] == "finished": - self.testRunFinished.emit(data["tests"], data["duration_s"]) - - # coverage data - elif data["event"] == "coverage": - self.coverageDataSaved.emit(data["file"])
--- a/eric7/Unittest/Interfaces/UnittestRunner.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,423 +0,0 @@ -# -*- 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 os -import sys -import time -import unittest - - -sys.path.insert( - 2, - os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -) - - -class EricTestResult(unittest.TestResult): - """ - Class implementing a TestResult derivative to send the data via a network - connection. - """ - def __init__(self, writer, failfast): - """ - Constructor - - @param writer reference to the object to write the results to - @type EricJsonWriter - @param failfast flag indicating to stop at the first error - @type bool - """ - super().__init__() - self.__writer = writer - self.failfast = failfast - self.__testsRun = 0 - - self.__currentTestStatus = {} - - def addFailure(self, test, err): - """ - Public method called if a test failed. - - @param test reference to the test object - @type TestCase - @param err tuple containing the exception data like sys.exc_info - (exception type, exception instance, traceback) - @type tuple - """ - super().addFailure(test, err) - tracebackLines = self._exc_info_to_string(err, test) - - self.__currentTestStatus.update({ - "status": "failure", - "traceback": tracebackLines, - }) - - def addError(self, test, err): - """ - Public method called if a test errored. - - @param test reference to the test object - @type TestCase - @param err tuple containing the exception data like sys.exc_info - (exception type, exception instance, traceback) - @type tuple - """ - super().addError(test, err) - tracebackLines = self._exc_info_to_string(err, test) - - self.__currentTestStatus.update({ - "status": "error", - "traceback": tracebackLines, - }) - - def addSkip(self, test, reason): - """ - Public method called if a test was skipped. - - @param test reference to the test object - @type TestCase - @param reason reason for skipping the test - @type str - """ - super().addSkip(test, reason) - - self.__currentTestStatus.update({ - "status": "skipped", - "shortmsg": reason, - }) - - def addExpectedFailure(self, test, err): - """ - Public method called if a test failed expected. - - @param test reference to the test object - @type TestCase - @param err tuple containing the exception data like sys.exc_info - (exception type, exception instance, traceback) - @type tuple - """ - super().addExpectedFailure(test, err) - tracebackLines = self._exc_info_to_string(err, test) - - self.__currentTestStatus.update({ - "status": "expected failure", - "traceback": tracebackLines, - }) - - def addUnexpectedSuccess(self, test): - """ - Public method called if a test succeeded expectedly. - - @param test reference to the test object - @type TestCase - """ - super().addUnexpectedSuccess(test) - - self.__currentTestStatus["status"] = "unexpected success" - - def addSubTest(self, test, subtest, err): - """ - Public method called for each subtest to record its result. - - @param test reference to the test object - @type TestCase - @param subtest reference to the subtest object - @type TestCase - @param err tuple containing the exception data like sys.exc_info - (exception type, exception instance, traceback) - @type tuple - """ - if err is not None: - super().addSubTest(test, subtest, err) - tracebackLines = self._exc_info_to_string(err, test) - status = ( - "failure" - if issubclass(err[0], test.failureException) else - "error" - ) - - # record the last subtest fail status as the overall status - self.__currentTestStatus["status"] = status - - self.__writer.write({ - "event": "result", - "status": status, - "name": str(subtest), - "id": subtest.id(), - "description": subtest.shortDescription(), - "traceback": tracebackLines, - "subtest": True, - }) - - if self.failfast: - self.stop() - else: - self.__writer.write({ - "event": "result", - "status": "success", - "name": str(subtest), - "id": subtest.id(), - "description": subtest.shortDescription(), - "subtest": True, - }) - - def startTest(self, test): - """ - Public method called at the start of a test. - - @param test reference to the test object - @type TestCase - """ - super().startTest(test) - - self.__testsRun += 1 - self.__currentTestStatus = { - "event": "result", - "status": "success", - "name": str(test), - "id": test.id(), - "description": test.shortDescription(), - "subtest": False, - } - - self.__writer.write({ - "event": "started", - "name": str(test), - "id": test.id(), - "description": test.shortDescription(), - }) - - self.__startTime = time.monotonic_ns() - - def stopTest(self, test): - """ - Public method called at the end of a test. - - @param test reference to the test object - @type TestCase - """ - stopTime = time.monotonic_ns() - duration = (stopTime - self.__startTime) / 1_000_000 # ms - - super().stopTest(test) - - self.__currentTestStatus["duration_ms"] = duration - self.__writer.write(self.__currentTestStatus) - - def startTestRun(self): - """ - Public method called once before any tests are executed. - """ - self.__totalStartTime = time.monotonic_ns() - self.__testsRun = 0 - - def stopTestRun(self): - """ - Public method called once after all tests are executed. - """ - stopTime = time.monotonic_ns() - duration = (stopTime - self.__totalStartTime) / 1_000_000_000 # s - - self.__writer.write({ - "event": "finished", - "duration_s": duration, - "tests": self.__testsRun, - }) - - -def _assembleTestCasesList(suite): - """ - Protected function to assemble a list of test cases included in a test - suite. - - @param suite test suite to be inspected - @type unittest.TestSuite - @return list of tuples containing the test case ID, the string - representation and the short description - @rtype list of tuples of (str, str) - """ - testCases = [] - for test in suite: - if isinstance(test, unittest.TestSuite): - testCases.extend(_assembleTestCasesList(test)) - else: - testId = test.id() - if ( - "ModuleImportFailure" not in testId and - "LoadTestsFailure" not in testId and - "_FailedTest" not in testId - ): - testCases.append( - (testId, str(test), test.shortDescription()) - ) - return testCases - - -def runtest(argv): - """ - Function to run the tests. - - @param argv list of command line parameters. - @type list of str - """ - from EricNetwork.EricJsonStreamWriter import EricJsonWriter - writer = EricJsonWriter(argv[0], int(argv[1])) - del argv[:2] - - # process arguments - if argv[0] == "discover": - discover = True - argv.pop(0) - if argv[0] == "--start-directory": - discoveryStart = argv[1] - del argv[:2] - else: - discover = False - discoveryStart = "" - - failfast = "--failfast" in argv - if failfast: - argv.remove("--failfast") - - coverage = "--cover" in argv - if coverage: - argv.remove("--cover") - coverageErase = "--cover-erase" in argv - if coverageErase: - argv.remove("--cover-erase") - - if argv and argv[0] == "--failed-only": - if discover: - testFileName = "" - failed = argv[1:] - else: - testFileName = argv[1] - failed = argv[2:] - else: - failed = [] - if discover: - testFileName = testName = "" - else: - testFileName, testName = argv[:2] - del argv[:2] - - testCases = argv[:] - - if testFileName: - sys.path.insert(1, os.path.dirname(os.path.abspath(testFileName))) - elif discoveryStart: - sys.path.insert(1, os.path.abspath(discoveryStart)) - - try: - testLoader = unittest.TestLoader() - if discover and not failed: - if testCases: - test = testLoader.loadTestsFromNames(testCases) - else: - test = testLoader.discover(discoveryStart) - else: - if testFileName: - module = __import__(os.path.splitext( - os.path.basename(testFileName))[0]) - else: - module = None - if failed: - if module: - failed = [t.split(".", 1)[1] - for t in failed] - test = testLoader.loadTestsFromNames( - failed, module) - else: - test = testLoader.loadTestsFromName( - testName, module) - except Exception as err: - print("Exception:", str(err)) - writer.write({ - "event": "collecterror", - "error": str(err), - }) - sys.exit(1) - - collectedTests = { - "event": "collected", - "tests": [ - {"id": id, "name": name, "description": desc} - for id, name, desc in _assembleTestCasesList(test) - ] - } - writer.write(collectedTests) - - # setup test coverage - if coverage: - if discover: - covname = os.path.join(discoveryStart, "unittest") - elif testFileName: - covname = os.path.splitext( - os.path.abspath(testFileName))[0] - else: - covname = "unittest" - covDataFile = "{0}.coverage".format(covname) - if not os.path.isabs(covDataFile): - covDataFile = os.path.abspath(covDataFile) - - from DebugClients.Python.coverage import coverage as cov - cover = cov(data_file=covDataFile) - if coverageErase: - cover.erase() - else: - cover = None - - testResult = EricTestResult(writer, failfast) - startTestRun = getattr(testResult, 'startTestRun', None) - if startTestRun is not None: - startTestRun() - try: - if cover: - cover.start() - test.run(testResult) - finally: - if cover: - cover.stop() - cover.save() - writer.write({ - "event": "coverage", - "file": covDataFile, - }) - stopTestRun = getattr(testResult, 'stopTestRun', None) - if stopTestRun is not None: - stopTestRun() - - writer.close() - sys.exit(0) - -if __name__ == '__main__': - if len(sys.argv) > 1: - command = sys.argv[1] - if command == "installed": - sys.exit(0) - - elif command == "versions": - import platform - versions = { - "name": "unittest", - "version": platform.python_version(), - "plugins": [], - } - print(json.dumps(versions)) - sys.exit(0) - - elif command == "runtest": - runtest(sys.argv[2:]) - sys.exit(0) - - sys.exit(42) - -# -# eflag: noqa = M801
--- a/eric7/Unittest/Interfaces/__init__.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -# -*- 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, -) - -FrameworkNames = ( - UnittestExecutor.name, - PytestExecutor.name, -)
--- a/eric7/Unittest/UTTestResultsTree.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,611 +0,0 @@ -# -*- 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. -""" - -import contextlib -import copy -import locale - -from collections import Counter -from operator import attrgetter - -from PyQt6.QtCore import ( - pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, - QModelIndex, QPoint -) -from PyQt6.QtGui import QBrush, QColor -from PyQt6.QtWidgets import QMenu, QTreeView - -from EricWidgets.EricApplication import ericApp - -import Preferences - -from .Interfaces.UTExecutorBase import ResultCategory - -TopLevelId = 2 ** 32 - 1 - - -class TestResultsModel(QAbstractItemModel): - """ - Class implementing the item model containing the test data. - - @signal summary(str) emitted whenever the model data changes. The element - is a summary of the test results of the model. - """ - summary = pyqtSignal(str) - - Headers = [ - QCoreApplication.translate("TestResultsModel", "Status"), - QCoreApplication.translate("TestResultsModel", "Name"), - QCoreApplication.translate("TestResultsModel", "Message"), - QCoreApplication.translate("TestResultsModel", "Duration [ms]"), - ] - - StatusColumn = 0 - NameColumn = 1 - MessageColumn = 2 - DurationColumn = 3 - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent object (defaults to None) - @type QObject (optional) - """ - super().__init__(parent) - - if ericApp().usesDarkPalette(): - self.__backgroundColors = { - ResultCategory.RUNNING: None, - ResultCategory.FAIL: QBrush(QColor("#880000")), - ResultCategory.OK: QBrush(QColor("#005500")), - ResultCategory.SKIP: QBrush(QColor("#3f3f3f")), - ResultCategory.PENDING: QBrush(QColor("#004768")), - } - else: - self.__backgroundColors = { - ResultCategory.RUNNING: None, - ResultCategory.FAIL: QBrush(QColor("#ff8080")), - ResultCategory.OK: QBrush(QColor("#c1ffba")), - ResultCategory.SKIP: QBrush(QColor("#c5c5c5")), - ResultCategory.PENDING: QBrush(QColor("#6fbaff")), - } - - self.__testResults = [] - - def index(self, row, column, parent=QModelIndex()): - """ - Public method to generate an index for the given row and column to - identify the item. - - @param row row for the index - @type int - @param column column for the index - @type int - @param parent index of the parent item (defaults to QModelIndex()) - @type QModelIndex (optional) - @return index for the item - @rtype QModelIndex - """ - if not self.hasIndex(row, column, parent): # check bounds etc. - return QModelIndex() - - if not parent.isValid(): - # top level item - return self.createIndex(row, column, TopLevelId) - else: - testResultIndex = parent.row() - return self.createIndex(row, column, testResultIndex) - - def data(self, index, role): - """ - Public method to get the data for the various columns and roles. - - @param index index of the data to be returned - @type QModelIndex - @param role role designating the data to return - @type Qt.ItemDataRole - @return requested data item - @rtype Any - """ - if not index.isValid(): - return None - - row = index.row() - column = index.column() - idx = index.internalId() - - if role == Qt.ItemDataRole.DisplayRole: - if idx != TopLevelId: - if bool(self.__testResults[idx].extra): - return self.__testResults[idx].extra[index.row()] - else: - return None - elif column == TestResultsModel.StatusColumn: - return self.__testResults[row].status - elif column == TestResultsModel.NameColumn: - return self.__testResults[row].name - elif column == TestResultsModel.MessageColumn: - return self.__testResults[row].message - elif column == TestResultsModel.DurationColumn: - duration = self.__testResults[row].duration - return ( - "" - if duration is None else - locale.format_string("%.2f", duration, grouping=True) - ) - elif role == Qt.ItemDataRole.ToolTipRole: - if idx == TopLevelId and column == TestResultsModel.NameColumn: - return self.__testResults[row].name - elif role == Qt.ItemDataRole.FontRole: - if idx != TopLevelId: - return Preferences.getEditorOtherFonts("MonospacedFont") - elif role == Qt.ItemDataRole.BackgroundRole: - if idx == TopLevelId: - testResult = self.__testResults[row] - with contextlib.suppress(KeyError): - return self.__backgroundColors[testResult.category] - elif role == Qt.ItemDataRole.TextAlignmentRole: - if idx == TopLevelId and column == TestResultsModel.DurationColumn: - return Qt.AlignmentFlag.AlignRight - elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ - if idx == TopLevelId: - testresult = self.__testResults[row] - return (testresult.filename, testresult.lineno) - - return None - - 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 parent(self, index): - """ - Public method to get the parent of the item pointed to by index. - - @param index index of the item - @type QModelIndex - @return index of the parent item - @rtype QModelIndex - """ - if not index.isValid(): - return QModelIndex() - - idx = index.internalId() - if idx == TopLevelId: - return QModelIndex() - else: - return self.index(idx, 0) - - 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 and - self.__testResults[parent.row()].extra is not None - ): - 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() - - self.summary.emit("") - - def sort(self, column, order): - """ - Public method to sort the model data by column in order. - - @param column sort column number - @type int - @param order sort order - @type Qt.SortOrder - """ # __IGNORE_WARNING_D234r__ - def durationKey(result): - """ - Function to generate a key for duration sorting - - @param result result object - @type UTTestResult - @return sort key - @rtype float - """ - return result.duration or -1.0 - - self.beginResetModel() - reverse = order == Qt.SortOrder.DescendingOrder - if column == TestResultsModel.StatusColumn: - self.__testResults.sort(key=attrgetter('category', 'status'), - reverse=reverse) - elif column == TestResultsModel.NameColumn: - self.__testResults.sort(key=attrgetter('name'), reverse=reverse) - elif column == TestResultsModel.MessageColumn: - self.__testResults.sort(key=attrgetter('message'), reverse=reverse) - elif column == TestResultsModel.DurationColumn: - self.__testResults.sort(key=durationKey, reverse=reverse) - self.endResetModel() - - def getTestResults(self): - """ - Public method to get the list of test results managed by the model. - - @return list of test results managed by the model - @rtype list of UTTestResult - """ - return copy.deepcopy(self.__testResults) - - def setTestResults(self, testResults): - """ - Public method to set the list of test results of the model. - - @param testResults test results to be managed by the model - @type list of UTTestResult - """ - self.beginResetModel() - self.__testResults = copy.deepcopy(testResults) - self.endResetModel() - - self.summary.emit(self.__summary()) - - def addTestResults(self, testResults): - """ - Public method to add test results to the ones already managed by the - model. - - @param testResults test results to be added to the model - @type list of UTTestResult - """ - firstRow = len(self.__testResults) - lastRow = firstRow + len(testResults) - 1 - self.beginInsertRows(QModelIndex(), firstRow, lastRow) - self.__testResults.extend(testResults) - self.endInsertRows() - - self.summary.emit(self.__summary()) - - def updateTestResults(self, testResults): - """ - Public method to update the data of managed test result items. - - @param testResults test results to be updated - @type list of UTTestResult - """ - minIndex = None - maxIndex = None - - testResultsToBeAdded = [] - - for testResult in testResults: - for (index, currentResult) in enumerate(self.__testResults): - if currentResult.id == testResult.id: - self.__testResults[index] = testResult - if minIndex is None: - minIndex = index - maxIndex = index - else: - minIndex = min(minIndex, index) - maxIndex = max(maxIndex, index) - - break - else: - # Test result with given id was not found. - # Just add it to the list (could be a sub test) - testResultsToBeAdded.append(testResult) - - if minIndex is not None: - self.dataChanged.emit( - self.index(minIndex, 0), - self.index(maxIndex, len(TestResultsModel.Headers) - 1) - ) - - self.summary.emit(self.__summary()) - - if testResultsToBeAdded: - self.addTestResults(testResultsToBeAdded) - - def getFailedTests(self): - """ - Public method to extract the test ids of all failed tests. - - @return test ids of all failed tests - @rtype list of str - """ - failedIds = [ - res.id for res in self.__testResults if ( - res.category == ResultCategory.FAIL and - not res.subtestResult - ) - ] - return failedIds - - def __summary(self): - """ - Private method to generate a test results summary text. - - @return test results summary text - @rtype str - """ - if len(self.__testResults) == 0: - return self.tr("No results to show") - - counts = Counter(res.category for res in self.__testResults) - if all( - counts[category] == 0 - for category in (ResultCategory.FAIL, ResultCategory.OK, - ResultCategory.SKIP) - ): - return self.tr("Collected %n test(s)", "", len(self.__testResults)) - - return self.tr( - "%n test(s)/subtest(s) total, {0} failed, {1} passed," - " {2} skipped, {3} pending", - "", len(self.__testResults) - ).format( - counts[ResultCategory.FAIL], - counts[ResultCategory.OK], - counts[ResultCategory.SKIP], - counts[ResultCategory.PENDING] - ) - - -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) - - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - - # connect signals and slots - self.doubleClicked.connect(self.__gotoTestDefinition) - self.customContextMenuRequested.connect(self.__showContextMenu) - - self.header().sortIndicatorChanged.connect(self.sortByColumn) - self.header().sortIndicatorChanged.connect( - lambda column, order: self.header().setSortIndicatorShown(True)) - - def reset(self): - """ - Public method to reset the internal state of the view. - """ - super().reset() - - self.resizeColumns() - self.spanFirstColumn(0, self.model().rowCount() - 1) - - def rowsInserted(self, parent, startRow, endRow): - """ - Public method called when rows are inserted. - - @param parent model index of the parent item - @type QModelIndex - @param startRow first row been inserted - @type int - @param endRow last row been inserted - @type int - """ - super().rowsInserted(parent, startRow, endRow) - - self.resizeColumns() - self.spanFirstColumn(startRow, endRow) - - def dataChanged(self, topLeft, bottomRight, roles=[]): - """ - Public method called when the model data has changed. - - @param topLeft index of the top left element - @type QModelIndex - @param bottomRight index of the bottom right element - @type QModelIndex - @param roles list of roles changed (defaults to []) - @type list of Qt.ItemDataRole (optional) - """ - super().dataChanged(topLeft, bottomRight, roles) - - self.resizeColumns() - while topLeft.parent().isValid(): - topLeft = topLeft.parent() - while bottomRight.parent().isValid(): - bottomRight = bottomRight.parent() - self.spanFirstColumn(topLeft.row(), bottomRight.row()) - - def resizeColumns(self): - """ - Public method to resize the columns to their contents. - """ - for column in range(self.model().columnCount()): - self.resizeColumnToContents(column) - - def spanFirstColumn(self, startRow, endRow): - """ - Public method to make the first column span the row for second level - items. - - These items contain the test results. - - @param startRow index of the first row to span - @type QModelIndex - @param endRow index of the last row (including) to span - @type QModelIndex - """ - model = self.model() - for row in range(startRow, endRow + 1): - index = model.index(row, 0) - for i in range(model.rowCount(index)): - self.setFirstColumnSpanned(i, index, True) - - def __canonicalIndex(self, index): - """ - Private method to create the canonical index for a given index. - - The canonical index is the index of the first column of the test - result entry (i.e. the top-level item). If the index is invalid, - None is returned. - - @param index index to determine the canonical index for - @type QModelIndex - @return index of the firt column of the associated top-level item index - @rtype QModelIndex - """ - if not index.isValid(): - return None - - while index.parent().isValid(): # find the top-level node - index = index.parent() - index = index.sibling(index.row(), 0) # go to first column - return index - - @pyqtSlot(QModelIndex) - def __gotoTestDefinition(self, index): - """ - Private slot to show the test definition. - - @param index index for the double-clicked item - @type QModelIndex - """ - cindex = self.__canonicalIndex(index) - filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole) - if filename is not None: - if lineno is None: - lineno = 1 - self.goto.emit(filename, lineno) - - @pyqtSlot(QPoint) - def __showContextMenu(self, pos): - """ - Private slot to show the context menu. - - @param pos relative position for the context menu - @type QPoint - """ - index = self.indexAt(pos) - cindex = self.__canonicalIndex(index) - - contextMenu = ( - self.__createContextMenu(cindex) - if cindex else - self.__createBackgroundContextMenu() - ) - contextMenu.exec(self.mapToGlobal(pos)) - - def __createContextMenu(self, index): - """ - Private method to create a context menu for the item pointed to by the - given index. - - @param index index of the item - @type QModelIndex - @return created context menu - @rtype QMenu - """ - menu = QMenu(self) - if self.isExpanded(index): - menu.addAction(self.tr("Collapse"), - lambda: self.collapse(index)) - else: - act = menu.addAction(self.tr("Expand"), - lambda: self.expand(index)) - act.setEnabled(self.model().hasChildren(index)) - menu.addSeparator() - - act = menu.addAction(self.tr("Show Source"), - lambda: self.__gotoTestDefinition(index)) - act.setEnabled( - self.model().data(index, Qt.ItemDataRole.UserRole) is not None - ) - menu.addSeparator() - - menu.addAction(self.tr("Collapse All"), self.collapseAll) - menu.addAction(self.tr("Expand All"), self.expandAll) - - return menu - - def __createBackgroundContextMenu(self): - """ - Private method to create a context menu for the background. - - @return created context menu - @rtype QMenu - """ - menu = QMenu(self) - menu.addAction(self.tr("Collapse All"), self.collapseAll) - menu.addAction(self.tr("Expand All"), self.expandAll) - - return menu - -# -# eflag: noqa = M821, M822
--- a/eric7/Unittest/UnittestWidget.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1024 +0,0 @@ -# -*- 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_UnittestWidget import Ui_UnittestWidget - -from .UTTestResultsTree import TestResultsModel, TestResultsTreeView -from .Interfaces import Frameworks -from .Interfaces.UTExecutorBase import ( - UTTestConfig, UTTestResult, ResultCategory -) -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 - - -# TODO: add a "Show Coverage" function using PyCoverageDialog - -class UnittestWidget(QWidget, Ui_UnittestWidget): - """ - Class implementing a widget to orchestrate unit test execution. - - @signal unittestFile(str, int, bool) emitted to show the source of a - unittest file - @signal unittestStopped() emitted after a unit test was run - """ - unittestFile = pyqtSignal(str, int, bool) - unittestStopped = 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.__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 unittest")) - 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("Unittest")) - - 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) - - self.__project = None - - self.__discoverHistory = [] - self.__fileHistory = [] - self.__testNameHistory = [] - self.__recentFramework = "" - self.__recentEnvironment = "" - self.__failedTests = [] - - self.__editors = [] - self.__testExecutor = None - - # connect some signals - self.frameworkComboBox.currentIndexChanged.connect( - self.__resetResults) - self.discoveryPicker.editTextChanged.connect( - self.__resetResults) - self.testsuitePicker.editTextChanged.connect( - self.__resetResults) - self.testComboBox.editTextChanged.connect( - self.__resetResults) - - self.__frameworkRegistry = UTFrameworkRegistry() - 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): - """ - Public slot to set the given test file as the current one. - - @param testFile path of the test file - @type str - """ - if testFile: - self.__insertTestFile(testFile) - - self.discoverCheckBox.setChecked(not bool(testFile)) - - 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( - 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() - - @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 ( - 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 - self.__startFailedButton.setEnabled( - self.__mode == UnittestWidgetModes.STOPPED and - failedAvailable - ) - self.__startFailedButton.setDefault( - self.__mode == UnittestWidgetModes.STOPPED and - failedAvailable - ) - - # Stop button - self.__stopButton.setEnabled( - self.__mode == UnittestWidgetModes.RUNNING) - self.__stopButton.setDefault( - self.__mode == UnittestWidgetModes.RUNNING) - - # Close button - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close - ).setEnabled(self.__mode in ( - UnittestWidgetModes.IDLE, UnittestWidgetModes.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 = UnittestWidgetModes.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 = UnittestWidgetModes.RUNNING - - self.__totalCount = 0 - self.__runCount = 0 - - self.__coverageFile = "" - # TODO: implement the handling of the 'Show Coverage' button - - 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 = UnittestWidgetModes.STOPPED - if self.__totalCount == 0: - self.progressProgressBar.setMaximum(100) - - self.progressGroupBox.hide() - - self.__updateButtonBoxButtons() - - self.unittestStopped.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) - - @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() - - 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) - if testFileName and not testName: - testName = "suite" - - self.sbLabel.setText(self.tr("Preparing Testsuite")) - QCoreApplication.processEvents() - - interpreter = self.__venvManager.getVirtualenvInterpreter( - self.__recentEnvironment) - config = UTTestConfig( - interpreter=interpreter, - discover=self.discoverCheckBox.isChecked(), - discoveryStart=discoveryStart, - testFilename=testFileName, - testName=testName, - failFast=self.failfastCheckBox.isChecked(), - failedOnly=failedOnly, - collectCoverage=self.coverageCheckBox.isChecked(), - eraseCoverage=self.coverageEraseCheckBox.isChecked(), - ) - - 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 and test name - of collected tests - @type list of tuple of (str, str) - """ - testResults = [ - UTTestResult( - category=ResultCategory.PENDING, - status=self.tr("pending"), - name=name, - id=id, - message=desc, - ) for id, name, desc in testNames - ] - self.__resultsModel.setTestResults(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(UTTestResult( - category=ResultCategory.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 unit 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([ - UTTestResult( - category=ResultCategory.RUNNING, - status=self.tr("running"), - id=test[0], - name=test[1], - message="" if test[2] is None else test[2], - ) - ]) - - @pyqtSlot(UTTestResult) - def __processTestResult(self, result): - """ - Private slot to handle the receipt of a test result object. - - @param result test result object - @type UTTestResult - """ - 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 UTTestResult - @param output string containing the test process output (if any) - @type str - """ - self.__setStoppedMode() - self.__testExecutor = None - - @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() - - @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 - - # TODO: implement the handling of the 'Show Coverage' button - - @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.unittestFile.emit(filename, lineno, True) - else: - self.__openEditor(filename, lineno) - - def __openEditor(self, filename, linenumber): - """ - Private method to open an editor window for the given file. - - Note: This method opens an editor window when the unittest 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 - @type int - """ - 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 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()
--- a/eric7/Unittest/UnittestWidget.ui Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,547 +0,0 @@ -<?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>850</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"> - <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>
--- a/eric7/Unittest/__init__.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -# -*- 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/Utilities/__init__.py Mon May 16 17:22:43 2022 +0200 +++ b/eric7/Utilities/__init__.py Mon May 16 19:46:51 2022 +0200 @@ -1304,15 +1304,18 @@ return volumeDirectory +# TODO: rename to getTestFileNames and add a name beginning with 'test_' def getTestFileName(fn): """ - Function to build the filename of a unittest file. + Function to build the filename of a test file. + + The filename for the test file is built by prepending + the string "test" to the file name passed into this function. - The filename for the unittest file is built by prepending - the string "test" to the filename passed into this function. - - @param fn filename basis to be used for the unittest filename (string) - @return filename of the corresponding unittest file (string) + @param fn file name basis to be used for the test file name + @type str + @return file name of the corresponding test file + @rtype str """ dn, fn = os.path.split(fn) return os.path.join(dn, "test{0}".format(fn))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/eric7_testing.py Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +eric testing. + +This is the main Python script that performs the necessary initialization +of the testing module and starts the Qt event loop. This is a standalone +version of the integrated testing module. +""" + +import sys +import os + +sys.path.insert(1, os.path.dirname(__file__)) + +for arg in sys.argv[:]: + if arg.startswith("--config="): + import Globals + configDir = arg.replace("--config=", "") + Globals.setConfigDir(configDir) + sys.argv.remove(arg) + elif arg.startswith("--settings="): + from PyQt6.QtCore import QSettings + settingsDir = os.path.expanduser(arg.replace("--settings=", "")) + if not os.path.isdir(settingsDir): + os.makedirs(settingsDir) + QSettings.setPath( + QSettings.Format.IniFormat, QSettings.Scope.UserScope, settingsDir) + sys.argv.remove(arg) + +from Globals import AppInfo + +from Toolbox import Startup + + +def createMainWidget(argv): + """ + Function to create the main widget. + + @param argv list of commandline parameters + @type list of str + @return reference to the main widget + @rtype QWidget + """ + from Testing.TestingWidget import TestingWindow + try: + fn = argv[1] + except IndexError: + fn = None + return TestingWindow(fn) + + +def main(): + """ + Main entry point into the application. + """ + from PyQt6.QtGui import QGuiApplication + QGuiApplication.setDesktopFileName("eric7_testing.desktop") + + options = [ + ("--config=configDir", + "use the given directory as the one containing the config files"), + ("--settings=settingsDir", + "use the given directory to store the settings files"), + ] + appinfo = AppInfo.makeAppInfo(sys.argv, + "eric Testing", + "file", + "Graphical test application", + options) + res = Startup.simpleAppStartup(sys.argv, + appinfo, + createMainWidget) + sys.exit(res) + +if __name__ == '__main__': + main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/eric7_testing.pyw Mon May 16 19:46:51 2022 +0200 @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Windows entry point. +""" + +from eric7_testing import main + +main()
--- a/eric7/eric7_unittest.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -eric Unittest. - -This is the main Python script that performs the necessary initialization -of the unittest module and starts the Qt event loop. This is a standalone -version of the integrated unittest module. -""" - -import sys -import os - -sys.path.insert(1, os.path.dirname(__file__)) - -for arg in sys.argv[:]: - if arg.startswith("--config="): - import Globals - configDir = arg.replace("--config=", "") - Globals.setConfigDir(configDir) - sys.argv.remove(arg) - elif arg.startswith("--settings="): - from PyQt6.QtCore import QSettings - settingsDir = os.path.expanduser(arg.replace("--settings=", "")) - if not os.path.isdir(settingsDir): - os.makedirs(settingsDir) - QSettings.setPath( - QSettings.Format.IniFormat, QSettings.Scope.UserScope, settingsDir) - sys.argv.remove(arg) - -from Globals import AppInfo - -from Toolbox import Startup - - -def createMainWidget(argv): - """ - Function to create the main widget. - - @param argv list of commandline parameters - @type list of str - @return reference to the main widget - @rtype QWidget - """ - from Unittest.UnittestWidget import UnittestWindow - try: - fn = argv[1] - except IndexError: - fn = None - return UnittestWindow(fn) - - -def main(): - """ - Main entry point into the application. - """ - from PyQt6.QtGui import QGuiApplication - QGuiApplication.setDesktopFileName("eric7_unittest.desktop") - - options = [ - ("--config=configDir", - "use the given directory as the one containing the config files"), - ("--settings=settingsDir", - "use the given directory to store the settings files"), - ] - appinfo = AppInfo.makeAppInfo(sys.argv, - "eric Unittest", - "file", - "Graphical unit test application", - options) - res = Startup.simpleAppStartup(sys.argv, - appinfo, - createMainWidget) - sys.exit(res) - -if __name__ == '__main__': - main()
--- a/eric7/eric7_unittest.pyw Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2011 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing the Windows entry point. -""" - -from eric7_unittest import main - -main()
--- a/scripts/install.py Mon May 16 17:22:43 2022 +0200 +++ b/scripts/install.py Mon May 16 19:46:51 2022 +0200 @@ -508,9 +508,11 @@ "eric7_diff", "eric7_doc", "eric7_editor", "eric7_hexeditor", "eric7_iconeditor", "eric7_plugininstall", "eric7_pluginrepository", "eric7_pluginuninstall", "eric7_qregularexpression", "eric7_re", - "eric7_shell", "eric7_snap", "eric7_sqlbrowser", "eric7_tray", - "eric7_trpreviewer", "eric7_uipreviewer", "eric7_unittest", - "eric7_virtualenv", "eric7", + "eric7_shell", "eric7_snap", "eric7_sqlbrowser", "eric7_testing", + "eric7_tray", "eric7_trpreviewer", "eric7_uipreviewer", + "eric7_virtualenv", "eric7", + # obsolete scripts below + "eric7_unittest", ] try: @@ -709,7 +711,7 @@ "eric7_pluginrepository", "eric7_pluginuninstall", "eric7_qregularexpression", "eric7_re", "eric7_shell", "eric7_snap", "eric7_sqlbrowser", "eric7_tray", - "eric7_trpreviewer", "eric7_uipreviewer", "eric7_unittest", + "eric7_trpreviewer", "eric7_uipreviewer", "eric7_testing", "eric7_virtualenv", "eric7"]: wnames.append(createPyWrapper(cfg['ericDir'], name, scriptsDir))
--- a/scripts/uninstall.py Mon May 16 17:22:43 2022 +0200 +++ b/scripts/uninstall.py Mon May 16 19:46:51 2022 +0200 @@ -125,9 +125,11 @@ "eric7_diff", "eric7_doc", "eric7_editor", "eric7_hexeditor", "eric7_iconeditor", "eric7_plugininstall", "eric7_pluginrepository", "eric7_pluginuninstall", "eric7_qregularexpression", "eric7_re", - "eric7_shell", "eric7_snap", "eric7_sqlbrowser", "eric7_tray", - "eric7_trpreviewer", "eric7_uipreviewer", "eric7_unittest", - "eric7_virtualenv", "eric7", + "eric7_shell", "eric7_snap", "eric7_sqlbrowser", "eric7_testing", + "eric7_tray", "eric7_trpreviewer", "eric7_uipreviewer", + "eric7_virtualenv", "eric7", + # obsolete scripts below + "eric7_unittest", ] try:
--- a/setup.py Mon May 16 17:22:43 2022 +0200 +++ b/setup.py Mon May 16 19:46:51 2022 +0200 @@ -394,7 +394,7 @@ "eric7_tray = eric7.eric7_tray:main", "eric7_trpreviewer = eric7.eric7_trpreviewer:main", "eric7_uipreviewer = eric7.eric7_uipreviewer:main", - "eric7_unittest = eric7.eric7_unittest:main", + "eric7_testing = eric7.eric7_testing:main", "eric7_virtualenv = eric7.eric7_virtualenv:main", ], "console_scripts": [