Performed some refactoring to avoid possible name clashes on case-insensitive systems. unittest

Mon, 16 May 2022 19:46:51 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 16 May 2022 19:46:51 +0200
branch
unittest
changeset 9066
a219ade50f7c
parent 9065
39405e6eba20
child 9069
938039ea15ca

Performed some refactoring to avoid possible name clashes on case-insensitive systems.

eric7.epj file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/DebugClientCapabilities.py file | annotate | diff | comparison | revisions
eric7/DebugClients/Python/__init__.py file | annotate | diff | comparison | revisions
eric7/DebugClients/__init__.py file | annotate | diff | comparison | revisions
eric7/Debugger/__init__.py file | annotate | diff | comparison | revisions
eric7/Globals/__init__.py file | annotate | diff | comparison | revisions
eric7/Project/ProjectSourcesBrowser.py file | annotate | diff | comparison | revisions
eric7/Project/PropertiesDialog.py file | annotate | diff | comparison | revisions
eric7/PyUnit/UnittestDialog.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/PytestExecutor.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/PytestRunner.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/TestExecutorBase.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/TestFrameworkRegistry.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/UnittestExecutor.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/UnittestRunner.py file | annotate | diff | comparison | revisions
eric7/Testing/Interfaces/__init__.py file | annotate | diff | comparison | revisions
eric7/Testing/TestResultsTree.py file | annotate | diff | comparison | revisions
eric7/Testing/TestingWidget.py file | annotate | diff | comparison | revisions
eric7/Testing/TestingWidget.ui file | annotate | diff | comparison | revisions
eric7/Testing/__init__.py file | annotate | diff | comparison | revisions
eric7/Tools/TrayStarter.py file | annotate | diff | comparison | revisions
eric7/UI/Browser.py file | annotate | diff | comparison | revisions
eric7/UI/ClearPrivateDataDialog.py file | annotate | diff | comparison | revisions
eric7/UI/ClearPrivateDataDialog.ui file | annotate | diff | comparison | revisions
eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/PytestExecutor.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/PytestRunner.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UTExecutorBase.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UTFrameworkRegistry.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UnittestExecutor.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UnittestRunner.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/__init__.py file | annotate | diff | comparison | revisions
eric7/Unittest/UTTestResultsTree.py file | annotate | diff | comparison | revisions
eric7/Unittest/UnittestWidget.py file | annotate | diff | comparison | revisions
eric7/Unittest/UnittestWidget.ui file | annotate | diff | comparison | revisions
eric7/Unittest/__init__.py file | annotate | diff | comparison | revisions
eric7/Utilities/__init__.py file | annotate | diff | comparison | revisions
eric7/eric7_testing.py file | annotate | diff | comparison | revisions
eric7/eric7_testing.pyw file | annotate | diff | comparison | revisions
eric7/eric7_unittest.py file | annotate | diff | comparison | revisions
eric7/eric7_unittest.pyw file | annotate | diff | comparison | revisions
scripts/install.py file | annotate | diff | comparison | revisions
scripts/uninstall.py file | annotate | diff | comparison | revisions
setup.py file | annotate | diff | comparison | revisions
--- 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>&lt;b&gt;Virtual Environment&lt;/b&gt;\n&lt;p&gt;Enter the virtual environment to be used. Leave it empty to use the default environment, i.e. the one configured globally or per project.&lt;/p&gt;</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>&lt;b&gt;Discovery Start&lt;/b&gt;
+&lt;p&gt;Enter name of the directory at which to start the test file discovery.
+Note that all test modules must be importable from this directory.&lt;/p&gt;</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>&lt;b&gt;Testsuite&lt;/b&gt;
+&lt;p&gt;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.&lt;/p&gt;</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 &quot;suite&quot;.</string>
+            </property>
+            <property name="whatsThis">
+             <string>&lt;b&gt;Testname&lt;/b&gt;&lt;p&gt;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 &quot;suite&quot; will be used.&lt;/p&gt;</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>&amp;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>&lt;b&gt;Virtual Environment&lt;/b&gt;\n&lt;p&gt;Enter the virtual environment to be used. Leave it empty to use the default environment, i.e. the one configured globally or per project.&lt;/p&gt;</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>&lt;b&gt;Discovery Start&lt;/b&gt;
-&lt;p&gt;Enter name of the directory at which to start the test file discovery.
-Note that all test modules must be importable from this directory.&lt;/p&gt;</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>&lt;b&gt;Testsuite&lt;/b&gt;
-&lt;p&gt;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.&lt;/p&gt;</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 &quot;suite&quot;.</string>
-            </property>
-            <property name="whatsThis">
-             <string>&lt;b&gt;Testname&lt;/b&gt;&lt;p&gt;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 &quot;suite&quot; will be used.&lt;/p&gt;</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>&amp;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": [

eric ide

mercurial