Merged with branch 'unittest' to get the changed functionality to the main development branch. eric7

Thu, 19 May 2022 14:40:15 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 19 May 2022 14:40:15 +0200
branch
eric7
changeset 9080
4cadd6b03e82
parent 9077
33827549f187 (current diff)
parent 9079
1311dc91e846 (diff)
child 9081
2bbbc95972bf

Merged with branch 'unittest' to get the changed functionality to the main development branch.

--- a/docs/changelog	Thu May 19 10:45:41 2022 +0200
+++ b/docs/changelog	Thu May 19 14:40:15 2022 +0200
@@ -2,6 +2,10 @@
 ----------
 Version 22.6:
 - bug fixes
+- Dataview Coverage
+  -- added support to write coverage reports as HTML, JSON or LCOV files
+  -- removed the support for writing annotated sources
+     (deprecated in coverage.py)
 - Mercurial Interface
   -- added configuration option to override the automatic search for the hg
      executable
--- a/eric7.epj	Thu May 19 10:45:41 2022 +0200
+++ b/eric7.epj	Thu May 19 14:40:15 2022 +0200
@@ -277,6 +277,8 @@
       "eric7/Cooperation/ChatWidget.ui",
       "eric7/DataViews/CodeMetricsDialog.ui",
       "eric7/DataViews/PyCoverageDialog.ui",
+      "eric7/DataViews/PyCoverageHtmlReportDialog.ui",
+      "eric7/DataViews/PyCoverageJsonReportDialog.ui",
       "eric7/DataViews/PyProfileDialog.ui",
       "eric7/Debugger/CallTraceViewer.ui",
       "eric7/Debugger/EditBreakpointDialog.ui",
@@ -623,8 +625,6 @@
       "eric7/Project/TranslationPropertiesDialog.ui",
       "eric7/Project/UicCompilerOptionsDialog.ui",
       "eric7/Project/UserPropertiesDialog.ui",
-      "eric7/PyUnit/UnittestDialog.ui",
-      "eric7/PyUnit/UnittestStacktraceDialog.ui",
       "eric7/QScintilla/EditorOutlineSizesDialog.ui",
       "eric7/QScintilla/GotoDialog.ui",
       "eric7/QScintilla/MarkupProviders/HyperlinkMarkupDialog.ui",
@@ -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",
@@ -989,11 +990,12 @@
       "eric7/DataViews/CodeMetrics.py",
       "eric7/DataViews/CodeMetricsDialog.py",
       "eric7/DataViews/PyCoverageDialog.py",
+      "eric7/DataViews/PyCoverageHtmlReportDialog.py",
+      "eric7/DataViews/PyCoverageJsonReportDialog.py",
       "eric7/DataViews/PyProfileDialog.py",
       "eric7/DataViews/__init__.py",
       "eric7/DebugClients/Python/AsyncFile.py",
       "eric7/DebugClients/Python/BreakpointWatch.py",
-      "eric7/DebugClients/Python/DCTestResult.py",
       "eric7/DebugClients/Python/DebugBase.py",
       "eric7/DebugClients/Python/DebugClient.py",
       "eric7/DebugClients/Python/DebugClientBase.py",
@@ -1094,6 +1096,8 @@
       "eric7/EricNetwork/EricGoogleMailHelpers.py",
       "eric7/EricNetwork/EricJsonClient.py",
       "eric7/EricNetwork/EricJsonServer.py",
+      "eric7/EricNetwork/EricJsonStreamReader.py",
+      "eric7/EricNetwork/EricJsonStreamWriter.py",
       "eric7/EricNetwork/EricNetworkIcon.py",
       "eric7/EricNetwork/EricNetworkProxyFactory.py",
       "eric7/EricNetwork/EricSslCertificateSelectionDialog.py",
@@ -1816,8 +1820,6 @@
       "eric7/Project/UserProjectFile.py",
       "eric7/Project/UserPropertiesDialog.py",
       "eric7/Project/__init__.py",
-      "eric7/PyUnit/UnittestDialog.py",
-      "eric7/PyUnit/__init__.py",
       "eric7/QScintilla/APIsManager.py",
       "eric7/QScintilla/DocstringGenerator/BaseDocstringGenerator.py",
       "eric7/QScintilla/DocstringGenerator/EricdocGenerator.py",
@@ -1937,6 +1939,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",
@@ -2307,14 +2319,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/CondaInterface/Conda.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/CondaInterface/Conda.py	Thu May 19 14:40:15 2022 +0200
@@ -705,7 +705,7 @@
                 process.kill()
                 process.waitForFinished(3000)
                 return False, self.tr("conda did not finish within"
-                                      " 30 seconds.")
+                                      " 3 seconds.")
         
         return False, self.tr("conda could not be started.")
     
--- a/eric7/DataViews/PyCoverageDialog.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/DataViews/PyCoverageDialog.py	Thu May 19 14:40:15 2022 +0200
@@ -7,11 +7,11 @@
 Module implementing a Python code coverage dialog.
 """
 
-import contextlib
 import os
 import time
 
-from PyQt6.QtCore import pyqtSlot, Qt
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
+from PyQt6.QtGui import QDesktopServices
 from PyQt6.QtWidgets import (
     QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem,
     QApplication
@@ -19,7 +19,6 @@
 
 from EricWidgets import EricMessageBox
 from EricWidgets.EricApplication import ericApp
-from EricWidgets.EricProgressDialog import EricProgressDialog
 
 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
 
@@ -31,12 +30,17 @@
 class PyCoverageDialog(QDialog, Ui_PyCoverageDialog):
     """
     Class implementing a dialog to display the collected code coverage data.
+    
+    @signal openFile(str) emitted to open the given file in an editor
     """
+    openFile = pyqtSignal(str)
+    
     def __init__(self, parent=None):
         """
         Constructor
         
-        @param parent parent widget (QWidget)
+        @param parent parent widget
+        @type QWidget
         """
         super().__init__(parent)
         self.setupUi(self)
@@ -57,16 +61,17 @@
         
         self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]']
         
+        self.__reportsMenu = QMenu(self.tr("Create Report"), self)
+        self.__reportsMenu.addAction(self.tr("HTML Report"), self.__htmlReport)
+        self.__reportsMenu.addAction(self.tr("JSON Report"), self.__jsonReport)
+        self.__reportsMenu.addAction(self.tr("LCOV Report"), self.__lcovReport)
+        
         self.__menu = QMenu(self)
         self.__menu.addSeparator()
         self.openAct = self.__menu.addAction(
             self.tr("Open"), self.__openFile)
         self.__menu.addSeparator()
-        self.annotate = self.__menu.addAction(
-            self.tr('Annotate'), self.__annotate)
-        self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll)
-        self.__menu.addAction(
-            self.tr('Delete annotated files'), self.__deleteAnnotated)
+        self.__menu.addMenu(self.__reportsMenu)
         self.__menu.addSeparator()
         self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase)
         self.resultList.setContextMenuPolicy(
@@ -80,7 +85,9 @@
         groups.
         
         @param lines list of integers
+        @type list of int
         @return string representing the list
+        @rtype str
         """
         pairs = []
         lines.sort()
@@ -110,6 +117,9 @@
             pair.
             
             @param pair pair of integers
+            @type tuple of (int, int
+            @return representation of the pair
+            @rtype str
             """
             start, end = pair
             if start == end:
@@ -124,12 +134,18 @@
         """
         Private method to create an entry in the result list.
         
-        @param file filename of file (string)
-        @param statements amount of statements (integer)
-        @param executed amount of executed statements (integer)
-        @param coverage percent of coverage (integer)
-        @param excluded list of excluded lines (string)
-        @param missing list of lines without coverage (string)
+        @param file filename of file
+        @type str
+        @param statements number of statements
+        @type int
+        @param executed number of executed statements
+        @type int
+        @param coverage percent of coverage
+        @type int
+        @param excluded list of excluded lines
+        @type str
+        @param missing list of lines without coverage
+        @type str
         """
         itm = QTreeWidgetItem(self.resultList, [
             file,
@@ -146,14 +162,15 @@
             font.setBold(True)
             for col in range(itm.columnCount()):
                 itm.setFont(col, font)
-        
+    
     def start(self, cfn, fn):
         """
         Public slot to start the coverage data evaluation.
         
-        @param cfn basename of the coverage file (string)
+        @param cfn basename of the coverage file
+        @type str
         @param fn file or list of files or directory to be checked
-                (string or list of strings)
+        @type str or list of str
         """
         self.__cfn = cfn
         self.__fn = fn
@@ -251,7 +268,7 @@
                         total_exceptions))
         
         self.__finish()
-        
+    
     def __finish(self):
         """
         Private slot called when the action finished or the user pressed the
@@ -271,12 +288,13 @@
         self.summaryList.header().resizeSections(
             QHeaderView.ResizeMode.ResizeToContents)
         self.summaryList.header().setStretchLastSection(True)
-        
+    
     def on_buttonBox_clicked(self, button):
         """
         Private slot called by a button of the button box clicked.
         
-        @param button button that was clicked (QAbstractButton)
+        @param button button that was clicked
+        @type QAbstractButton
         """
         if button == self.buttonBox.button(
             QDialogButtonBox.StandardButton.Close
@@ -286,88 +304,122 @@
             QDialogButtonBox.StandardButton.Cancel
         ):
             self.__finish()
-        
+    
     def __showContextMenu(self, coord):
         """
         Private slot to show the context menu of the listview.
         
-        @param coord the position of the mouse pointer (QPoint)
+        @param coord position of the mouse pointer
+        @type QPoint
         """
         itm = self.resultList.itemAt(coord)
         if itm:
-            self.annotate.setEnabled(True)
             self.openAct.setEnabled(True)
         else:
-            self.annotate.setEnabled(False)
             self.openAct.setEnabled(False)
+        self.__reportsMenu.setEnabled(
+            bool(self.resultList.topLevelItemCount()))
         self.__menu.popup(self.mapToGlobal(coord))
-        
+    
     def __openFile(self, itm=None):
         """
         Private slot to open the selected file.
         
-        @param itm reference to the item to be opened (QTreeWidgetItem)
+        @param itm reference to the item to be opened
+        @type QTreeWidgetItem
         """
         if itm is None:
             itm = self.resultList.currentItem()
         fn = itm.text(0)
         
-        vm = ericApp().getObject("ViewManager")
-        vm.openSourceFile(fn)
-        editor = vm.getOpenEditor(fn)
-        editor.codeCoverageShowAnnotations()
-        
-    def __annotate(self):
+        try:
+            vm = ericApp().getObject("ViewManager")
+            vm.openSourceFile(fn)
+            editor = vm.getOpenEditor(fn)
+            editor.codeCoverageShowAnnotations(coverageFile=self.cfn)
+        except KeyError:
+            self.openFile.emit(fn)
+    
+    def __prepareReportGeneration(self):
         """
-        Private slot to handle the annotate context menu action.
-        
-        This method produce an annotated coverage file of the
-        selected file.
-        """
-        itm = self.resultList.currentItem()
-        fn = itm.text(0)
+        Private method to prepare a report generation.
         
-        cover = Coverage(data_file=self.cfn)
-        cover.exclude(self.excludeList[0])
-        cover.load()
-        cover.annotate([fn], None, True)
-        
-    def __annotateAll(self):
+        @return tuple containing a reference to the Coverage object and the
+            list of files to report
+        @rtype tuple of (Coverage, list of str)
         """
-        Private slot to handle the annotate all context menu action.
-        
-        This method produce an annotated coverage file of every
-        file listed in the listview.
-        """
-        amount = self.resultList.topLevelItemCount()
-        if amount == 0:
-            return
+        count = self.resultList.topLevelItemCount()
+        if count == 0:
+            return None, []
         
         # get list of all filenames
-        files = []
-        for index in range(amount):
-            itm = self.resultList.topLevelItem(index)
-            files.append(itm.text(0))
+        files = [
+            self.resultList.topLevelItem(index).text(0)
+            for index in range(count)
+        ]
         
         cover = Coverage(data_file=self.cfn)
         cover.exclude(self.excludeList[0])
         cover.load()
         
-        # now process them
-        progress = EricProgressDialog(
-            self.tr("Annotating files..."), self.tr("Abort"),
-            0, len(files), self.tr("%v/%m Files"), self)
-        progress.setMinimumDuration(0)
-        progress.setWindowTitle(self.tr("Coverage"))
+        return cover, files
+    
+    @pyqtSlot()
+    def __htmlReport(self):
+        """
+        Private slot to generate a HTML report of the shown data.
+        """
+        from .PyCoverageHtmlReportDialog import PyCoverageHtmlReportDialog
+        
+        dlg = PyCoverageHtmlReportDialog(os.path.dirname(self.cfn), self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            title, outputDirectory, extraCSS, openReport = dlg.getData()
+            
+            cover, files = self.__prepareReportGeneration()
+            cover.html_report(morfs=files, directory=outputDirectory,
+                              ignore_errors=True, extra_css=extraCSS,
+                              title=title)
+            
+            if openReport:
+                QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.join(
+                    outputDirectory, "index.html")))
+    
+    @pyqtSlot()
+    def __jsonReport(self):
+        """
+        Private slot to generate a JSON report of the shown data.
+        """
+        from .PyCoverageJsonReportDialog import PyCoverageJsonReportDialog
         
-        for count, file in enumerate(files):
-            progress.setValue(count)
-            if progress.wasCanceled():
-                break
-            cover.annotate([file], None)  # , True)
+        dlg = PyCoverageJsonReportDialog(os.path.dirname(self.cfn), self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            filename, compact = dlg.getData()
+            cover, files = self.__prepareReportGeneration()
+            cover.json_report(morfs=files, outfile=filename,
+                              ignore_errors=True, pretty_print=not compact)
+    
+    @pyqtSlot()
+    def __lcovReport(self):
+        """
+        Private slot to generate a LCOV report of the shown data.
+        """
+        from EricWidgets import EricPathPickerDialog
+        from EricWidgets.EricPathPicker import EricPathPickerModes
         
-        progress.setValue(len(files))
-        
+        filename, ok = EricPathPickerDialog.getPath(
+            self,
+            self.tr("LCOV Report"),
+            self.tr("Enter the path of the output file:"),
+            mode=EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE,
+            path=os.path.join(os.path.dirname(self.cfn), "coverage.lcov"),
+            defaultDirectory=os.path.dirname(self.cfn),
+            filters=self.tr("LCOV Files (*.lcov);;All Files (*)")
+        )
+        if ok:
+            cover, files = self.__prepareReportGeneration()
+            cover.lcov_report(morfs=files, outfile=filename,
+                              ignore_errors=True)
+    
     def __erase(self):
         """
         Private slot to handle the erase context menu action.
@@ -382,19 +434,7 @@
         self.reloadButton.setEnabled(False)
         self.resultList.clear()
         self.summaryList.clear()
-        
-    def __deleteAnnotated(self):
-        """
-        Private slot to handle the delete annotated context menu action.
-        
-        This method deletes all annotated files. These are files
-        ending with ',cover'.
-        """
-        files = Utilities.direntries(self.path, True, '*,cover', False)
-        for file in files:
-            with contextlib.suppress(OSError):
-                os.remove(file)
-
+    
     @pyqtSlot()
     def on_reloadButton_clicked(self):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DataViews/PyCoverageHtmlReportDialog.py	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to enter the parameters for a coverage HTML
+report.
+"""
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from EricWidgets.EricPathPicker import EricPathPickerModes
+
+from .Ui_PyCoverageHtmlReportDialog import Ui_PyCoverageHtmlReportDialog
+
+
+class PyCoverageHtmlReportDialog(QDialog, Ui_PyCoverageHtmlReportDialog):
+    """
+    Class implementing a dialog to enter the parameters for a coverage HTML
+    report.
+    """
+    def __init__(self, defaultDirectory, parent=None):
+        """
+        Constructor
+        
+        @param defaultDirectory default directory for selecting the output
+            directory
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.outputDirectoryPicker.setMode(
+            EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE)
+        self.outputDirectoryPicker.setDefaultDirectory(defaultDirectory)
+        
+        self.extraCssPicker.setMode(
+            EricPathPickerModes.OPEN_FILE_MODE)
+        
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Ok).setEnabled(False)
+        
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+    
+    @pyqtSlot(str)
+    def on_outputDirectoryPicker_textChanged(self, directory):
+        """
+        Private slot handling a change of the output directory.
+        
+        @param directory current text of the directory picker
+        @type str
+        """
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Ok).setEnabled(bool(directory))
+    
+    def getData(self):
+        """
+        Public method to get the entered data.
+        
+        @return tuple containing the report title, the output directory, the
+            path of a file containing extra CSS and a flag indicating to open
+            the generated report in a browser
+        
+        @rtype tuple of (str, str, str, bool)
+        """
+        title = self.titleEdit.text()
+        return (
+            title if bool(title) else None,
+            self.outputDirectoryPicker.currentText(),
+            self.extraCssPicker.currentText(),
+            self.openReportCheckBox.isChecked(),
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DataViews/PyCoverageHtmlReportDialog.ui	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PyCoverageHtmlReportDialog</class>
+ <widget class="QDialog" name="PyCoverageHtmlReportDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>500</width>
+    <height>154</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>HTML Report</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Title:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="EricPathPicker" name="extraCssPicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the path of a file containing additional CSS definitions</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_3">
+     <property name="text">
+      <string>Extra CSS:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLineEdit" name="titleEdit">
+     <property name="toolTip">
+      <string>Enter the title for the HTML report</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Output Directory:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="EricPathPicker" name="outputDirectoryPicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the path of the output directory</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="2">
+    <widget class="QCheckBox" name="openReportCheckBox">
+     <property name="toolTip">
+      <string>Select to open the generated report</string>
+     </property>
+     <property name="text">
+      <string>Open Report</string>
+     </property>
+     <property name="checked">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>EricPathPicker</class>
+   <extends>QWidget</extends>
+   <header>EricWidgets/EricPathPicker.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>titleEdit</tabstop>
+  <tabstop>outputDirectoryPicker</tabstop>
+  <tabstop>extraCssPicker</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>PyCoverageHtmlReportDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>PyCoverageHtmlReportDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DataViews/PyCoverageJsonReportDialog.py	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to enter the parameters for a coverage JSON
+report.
+"""
+
+import os
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QDialogButtonBox
+
+from EricWidgets.EricPathPicker import EricPathPickerModes
+
+from .Ui_PyCoverageJsonReportDialog import Ui_PyCoverageJsonReportDialog
+
+
+class PyCoverageJsonReportDialog(QDialog, Ui_PyCoverageJsonReportDialog):
+    """
+    Class implementing a dialog to enter the parameters for a coverage JSON
+    report.
+    """
+    def __init__(self, defaultDirectory, parent=None):
+        """
+        Constructor
+        
+        @param defaultDirectory default directory for selecting the output
+            directory
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.outputFilePicker.setMode(
+            EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE)
+        self.outputFilePicker.setDefaultDirectory(defaultDirectory)
+        self.outputFilePicker.setFilters(
+            self.tr("JSON Files (*.json);;All Files (*)"))
+        self.outputFilePicker.setText(
+            os.path.join(defaultDirectory, "coverage.json"))
+        
+        msh = self.minimumSizeHint()
+        self.resize(max(self.width(), msh.width()), msh.height())
+    
+    @pyqtSlot(str)
+    def on_outputFilePicker_textChanged(self, filename):
+        """
+        Private slot handling a change of the output file.
+        
+        @param filename current text of the file picker
+        @type str
+        """
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Ok).setEnabled(bool(filename))
+    
+    def getData(self):
+        """
+        Public method to get the entered data.
+        
+        @return tuple containing the output file and a flag indicating the
+            creation of a compact JSON file
+        
+        @rtype tuple of (str, bool)
+        """
+        return (
+            self.outputFilePicker.currentText(),
+            self.compactCheckBox.isChecked(),
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DataViews/PyCoverageJsonReportDialog.ui	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PyCoverageJsonReportDialog</class>
+ <widget class="QDialog" name="PyCoverageJsonReportDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>500</width>
+    <height>98</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>JSON Report</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="1" column="0" colspan="2">
+    <widget class="QCheckBox" name="compactCheckBox">
+     <property name="toolTip">
+      <string>Select to open the generated report</string>
+     </property>
+     <property name="text">
+      <string>Compact Format</string>
+     </property>
+     <property name="checked">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Output File:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="EricPathPicker" name="outputFilePicker" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="focusPolicy">
+      <enum>Qt::StrongFocus</enum>
+     </property>
+     <property name="toolTip">
+      <string>Enter the path of the output file</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="0" colspan="2">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>EricPathPicker</class>
+   <extends>QWidget</extends>
+   <header>EricWidgets/EricPathPicker.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>outputFilePicker</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>PyCoverageJsonReportDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>PyCoverageJsonReportDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- a/eric7/DebugClients/Python/DCTestResult.py	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2003 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing a TestResult derivative for the eric debugger.
-"""
-
-import select
-from unittest import TestResult
-
-
-class DCTestResult(TestResult):
-    """
-    A TestResult derivative to work with eric's debug client.
-    
-    For more details see unittest.py of the standard python distribution.
-    """
-    def __init__(self, dbgClient, failfast):
-        """
-        Constructor
-        
-        @param dbgClient reference to the debug client
-        @type DebugClientBase
-        @param failfast flag indicating to stop at the first error
-        @type bool
-        """
-        TestResult.__init__(self)
-        self.__dbgClient = dbgClient
-        self.failfast = failfast
-    
-    def addFailure(self, test, err):
-        """
-        Public method called if a test failed.
-        
-        @param test Reference to the test object
-        @param err The error traceback
-        """
-        TestResult.addFailure(self, test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTTestFailed", {
-            "testname": str(test),
-            "traceback": tracebackLines,
-            "id": test.id(),
-        })
-    
-    def addError(self, test, err):
-        """
-        Public method called if a test errored.
-        
-        @param test Reference to the test object
-        @param err The error traceback
-        """
-        TestResult.addError(self, test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTTestErrored", {
-            "testname": str(test),
-            "traceback": tracebackLines,
-            "id": test.id(),
-        })
-    
-    def addSubTest(self, test, subtest, err):
-        """
-        Public method called for each subtest to record its result.
-        
-        @param test reference to the test object
-        @param subtest reference to the subtest object
-        @param err error traceback
-        """
-        if err is not None:
-            TestResult.addSubTest(self, test, subtest, err)
-            tracebackLines = self._exc_info_to_string(err, test)
-            if issubclass(err[0], test.failureException):
-                self.__dbgClient.sendJsonCommand("ResponseUTTestFailed", {
-                    "testname": str(subtest),
-                    "traceback": tracebackLines,
-                    "id": test.id(),
-                })
-            else:
-                self.__dbgClient.sendJsonCommand("ResponseUTTestErrored", {
-                    "testname": str(subtest),
-                    "traceback": tracebackLines,
-                    "id": test.id(),
-                })
-    
-    def addSkip(self, test, reason):
-        """
-        Public method called if a test was skipped.
-        
-        @param test reference to the test object
-        @param reason reason for skipping the test (string)
-        """
-        TestResult.addSkip(self, test, reason)
-        self.__dbgClient.sendJsonCommand("ResponseUTTestSkipped", {
-            "testname": str(test),
-            "reason": reason,
-            "id": test.id(),
-        })
-    
-    def addExpectedFailure(self, test, err):
-        """
-        Public method called if a test failed expected.
-        
-        @param test reference to the test object
-        @param err error traceback
-        """
-        TestResult.addExpectedFailure(self, test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTTestFailedExpected", {
-            "testname": str(test),
-            "traceback": tracebackLines,
-            "id": test.id(),
-        })
-    
-    def addUnexpectedSuccess(self, test):
-        """
-        Public method called if a test succeeded expectedly.
-        
-        @param test reference to the test object
-        """
-        TestResult.addUnexpectedSuccess(self, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTTestSucceededUnexpected", {
-            "testname": str(test),
-            "id": test.id(),
-        })
-    
-    def startTest(self, test):
-        """
-        Public method called at the start of a test.
-        
-        @param test Reference to the test object
-        """
-        TestResult.startTest(self, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTStartTest", {
-            "testname": str(test),
-            "description": test.shortDescription(),
-        })
-    
-    def stopTest(self, test):
-        """
-        Public method called at the end of a test.
-        
-        @param test Reference to the test object
-        """
-        TestResult.stopTest(self, test)
-        self.__dbgClient.sendJsonCommand("ResponseUTStopTest", {})
-        
-        # ensure that pending input is processed
-        rrdy, wrdy, xrdy = select.select(
-            [self.__dbgClient.readstream], [], [], 0.01)
-
-        if self.__dbgClient.readstream in rrdy:
-            self.__dbgClient.readReady(self.__dbgClient.readstream)
--- a/eric7/DebugClients/Python/DebugClientBase.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/DebugClients/Python/DebugClientBase.py	Thu May 19 14:40:15 2022 +0200
@@ -799,194 +799,6 @@
         
         elif method == "RequestCompletion":
             self.__completionList(params["text"])
-        
-        elif method == "RequestUTDiscover":
-            if params["syspath"]:
-                sys.path = params["syspath"] + sys.path
-            
-            discoveryStart = params["discoverystart"]
-            if not discoveryStart:
-                discoveryStart = params["workdir"]
-            
-            top_level_dir = params["workdir"]
-
-            os.chdir(params["discoverystart"])
-            
-            # set the system exception handling function to ensure, that
-            # we report on all unhandled exceptions
-            sys.excepthook = self.__unhandled_exception
-            self.__interceptSignals()
-            
-            try:
-                import unittest
-                testLoader = unittest.TestLoader()
-                test = testLoader.discover(
-                    discoveryStart, top_level_dir=top_level_dir)
-                if (hasattr(testLoader, "errors") and
-                        bool(testLoader.errors)):
-                    self.sendJsonCommand("ResponseUTDiscover", {
-                        "testCasesList": [],
-                        "exception": "DiscoveryError",
-                        "message": "\n\n".join(testLoader.errors),
-                    })
-                else:
-                    testsList = self.__assembleTestCasesList(test,
-                                                             discoveryStart)
-                    self.sendJsonCommand("ResponseUTDiscover", {
-                        "testCasesList": testsList,
-                        "exception": "",
-                        "message": "",
-                    })
-            except Exception:
-                exc_type, exc_value, exc_tb = sys.exc_info()
-                self.sendJsonCommand("ResponseUTDiscover", {
-                    "testCasesList": [],
-                    "exception": exc_type.__name__,
-                    "message": str(exc_value),
-                })
-        
-        elif method == "RequestUTPrepare":
-            if params["syspath"]:
-                sys.path = params["syspath"] + sys.path
-            top_level_dir = None
-            if params["workdir"]:
-                os.chdir(params["workdir"])
-                top_level_dir = params["workdir"]
-            
-            # set the system exception handling function to ensure, that
-            # we report on all unhandled exceptions
-            sys.excepthook = self.__unhandled_exception
-            self.__interceptSignals()
-            
-            try:
-                import unittest
-                testLoader = unittest.TestLoader()
-                if params["discover"]:
-                    discoveryStart = params["discoverystart"]
-                    if not discoveryStart:
-                        discoveryStart = params["workdir"]
-                    sys.path.insert(
-                        0, os.path.abspath(discoveryStart))
-                    if params["testcases"]:
-                        self.test = testLoader.loadTestsFromNames(
-                            params["testcases"])
-                    else:
-                        self.test = testLoader.discover(
-                            discoveryStart, top_level_dir=top_level_dir)
-                else:
-                    sys.path.insert(
-                        0,
-                        os.path.dirname(os.path.abspath(params["filename"]))
-                    )
-                    if params["filename"]:
-                        utModule = __import__(params["testname"])
-                    else:
-                        utModule = None
-                    if params["failed"]:
-                        if utModule:
-                            failed = [t.split(".", 1)[1]
-                                      for t in params["failed"]]
-                        else:
-                            failed = params["failed"][:]
-                        self.test = testLoader.loadTestsFromNames(
-                            failed, utModule)
-                    else:
-                        self.test = testLoader.loadTestsFromName(
-                            params["testfunctionname"], utModule)
-            except Exception:
-                exc_type, exc_value, exc_tb = sys.exc_info()
-                self.sendJsonCommand("ResponseUTPrepared", {
-                    "count": 0,
-                    "exception": exc_type.__name__,
-                    "message": str(exc_value),
-                })
-                return
-            
-            # generate a coverage object
-            if params["coverage"]:
-                from coverage import Coverage
-                self.cover = Coverage(
-                    auto_data=True,
-                    data_file="{0}.coverage".format(
-                        os.path.splitext(params["coveragefile"])[0]))
-                if params["coverageerase"]:
-                    self.cover.erase()
-            else:
-                self.cover = None
-            
-            if params["debug"]:
-                Breakpoint.clear_all_breaks()
-                Watch.clear_all_watches()
-            
-            self.sendJsonCommand("ResponseUTPrepared", {
-                "count": self.test.countTestCases(),
-                "exception": "",
-                "message": "",
-            })
-        
-        elif method == "RequestUTRun":
-            from DCTestResult import DCTestResult
-            self.disassembly = None
-            self.testResult = DCTestResult(self, params["failfast"])
-            if self.cover:
-                self.cover.start()
-            self.debugging = params["debug"]
-            if params["debug"]:
-                self.multiprocessSupport = False
-                locals_ = locals()
-                self.threads.clear()
-                self.attachThread(mainThread=True)
-                sys.setprofile(None)
-                self.running = sys.argv[0]
-                self.mainThread.run(
-                    "result = self.test.run(self.testResult)\n",
-                    self.debugMod.__dict__,
-                    localsDict=locals_,
-                    debug=True,
-                    closeSession=False)
-                result = locals_["result"]
-            else:
-                result = self.test.run(self.testResult)
-            if self.cover:
-                self.cover.stop()
-                self.cover.save()
-            self.sendJsonCommand("ResponseUTFinished", {
-                "status": 0 if result.wasSuccessful() else 1,
-            })
-        
-        elif method == "RequestUTStop":
-            self.testResult.stop()
-    
-    def __assembleTestCasesList(self, suite, start):
-        """
-        Private method to assemble a list of test cases included in a test
-        suite.
-        
-        @param suite test suite to be inspected
-        @type unittest.TestSuite
-        @param start name of directory discovery was started at
-        @type str
-        @return list of tuples containing the test case ID, a short description
-            and the path of the test file name
-        @rtype list of tuples of (str, str, str)
-        """
-        import unittest
-        testCases = []
-        for test in suite:
-            if isinstance(test, unittest.TestSuite):
-                testCases.extend(self.__assembleTestCasesList(test, start))
-            else:
-                testId = test.id()
-                if ("ModuleImportFailure" not in testId and
-                    "LoadTestsFailure" not in testId and
-                        "_FailedTest" not in testId):
-                    filename = os.path.join(
-                        start,
-                        test.__module__.replace(".", os.sep) + ".py")
-                    testCases.append(
-                        (test.id(), test.shortDescription(), filename)
-                    )
-        return testCases
     
     def setDisassembly(self, disassembly):
         """
--- a/eric7/DebugClients/Python/DebugClientCapabilities.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/DebugClients/Python/DebugClientCapabilities.py	Thu May 19 14:40:15 2022 +0200
@@ -12,8 +12,7 @@
 HasProfiler = 0x0004
 HasCoverage = 0x0008
 HasCompleter = 0x0010
-HasUnittest = 0x0020
-HasShell = 0x0040
+HasShell = 0x0020
 
-HasAll = (HasDebugger | HasInterpreter | HasProfiler |
-          HasCoverage | HasCompleter | HasUnittest | HasShell)
+HasAll = (HasDebugger | HasInterpreter | HasProfiler | HasCoverage |
+          HasCompleter | HasShell)
--- a/eric7/Debugger/DebugClientCapabilities.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Debugger/DebugClientCapabilities.py	Thu May 19 14:40:15 2022 +0200
@@ -12,8 +12,7 @@
 HasProfiler = 0x0004
 HasCoverage = 0x0008
 HasCompleter = 0x0010
-HasUnittest = 0x0020
-HasShell = 0x0040
+HasShell = 0x0020
 
-HasAll = (HasDebugger | HasInterpreter | HasProfiler |
-          HasCoverage | HasCompleter | HasUnittest | HasShell)
+HasAll = (HasDebugger | HasInterpreter | HasProfiler | HasCoverage |
+          HasCompleter | HasShell)
--- a/eric7/Debugger/DebugServer.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Debugger/DebugServer.py	Thu May 19 14:40:15 2022 +0200
@@ -101,25 +101,6 @@
         unplanned)
     @signal clientInterpreterChanged(str) emitted to signal a change of the
         client interpreter
-    @signal utDiscovered(testCases, exc_type, exc_value) emitted after the
-        client has performed a test case discovery action
-    @signal utPrepared(nrTests, exc_type, exc_value) emitted after the client
-        has loaded a unittest suite
-    @signal utFinished() emitted after the client signalled the end of the
-        unittest
-    @signal utStartTest(testname, testdocu) emitted after the client has
-        started a test
-    @signal utStopTest() emitted after the client has finished a test
-    @signal utTestFailed(testname, exc_info, id) emitted after the client
-        reported a failed test
-    @signal utTestErrored(testname, exc_info, id) emitted after the client
-        reported an errored test
-    @signal utTestSkipped(testname, reason, id) emitted after the client
-        reported a skipped test
-    @signal utTestFailedExpected(testname, exc_info, id) emitted after the
-        client reported an expected test failure
-    @signal utTestSucceededUnexpected(testname, id) emitted after the client
-        reported an unexpected test success
     @signal callTraceInfo emitted after the client reported the call trace
         data (isCall, fromFile, fromLine, fromFunction, toFile, toLine,
         toFunction, debuggerId)
@@ -158,16 +139,6 @@
     clientCompletionList = pyqtSignal(list, str)
     clientInterpreterChanged = pyqtSignal(str)
     clientDebuggerId = pyqtSignal(str)
-    utDiscovered = pyqtSignal(list, str, str)
-    utPrepared = pyqtSignal(int, str, str)
-    utStartTest = pyqtSignal(str, str)
-    utStopTest = pyqtSignal()
-    utTestFailed = pyqtSignal(str, str, str)
-    utTestErrored = pyqtSignal(str, str, str)
-    utTestSkipped = pyqtSignal(str, str, str)
-    utTestFailedExpected = pyqtSignal(str, str, str)
-    utTestSucceededUnexpected = pyqtSignal(str, str)
-    utFinished = pyqtSignal()
     passiveDebugStarted = pyqtSignal(str, bool)
     callTraceInfo = pyqtSignal(bool, str, str, str, str, str, str, str)
     appendStdout = pyqtSignal(str)
@@ -1539,137 +1510,6 @@
         """
         self.debuggerInterface.remoteCompletion(debuggerId, text)
     
-    def remoteUTDiscover(self, clientType, forProject, venvName, syspath,
-                         workdir, discoveryStart):
-        """
-        Public method to perform a test case discovery.
-        
-        @param clientType client type to be used
-        @type str
-        @param forProject flag indicating a project related action
-        @type bool
-        @param venvName name of a virtual environment
-        @type str
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        """
-        if clientType and clientType not in self.getSupportedLanguages():
-            # a not supported client language was requested
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr(
-                    """<p>The debugger type <b>{0}</b> is not supported"""
-                    """ or not configured.</p>""").format(clientType)
-            )
-            return
-        
-        # Restart the client if there is already a program loaded.
-        try:
-            if clientType:
-                self.__setClientType(clientType)
-        except KeyError:
-            self.__setClientType('Python3')    # assume it is a Python3 file
-        self.startClient(False, forProject=forProject, venvName=venvName)
-        
-        self.debuggerInterface.remoteUTDiscover(
-            syspath, workdir, discoveryStart)
-    
-    def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase,
-                        clientType="", forProject=False, venvName="",
-                        syspath=None, workdir="", discover=False,
-                        discoveryStart="", testCases=None, debug=False):
-        """
-        Public method to prepare a new unittest run.
-        
-        @param fn the filename to load
-        @type str
-        @param tn the testname to load
-        @type str
-        @param tfn the test function name to load tests from
-        @type str
-        @param failed list of failed test, if only failed test should be run
-        @type list of str
-        @param cov flag indicating collection of coverage data is requested
-        @type bool
-        @param covname filename to be used to assemble the coverage caches
-            filename
-        @type str
-        @param coverase flag indicating erasure of coverage data is requested
-        @type bool
-        @param clientType client type to be used
-        @type str
-        @param forProject flag indicating a project related action
-        @type bool
-        @param venvName name of a virtual environment
-        @type str
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discover flag indicating to discover the tests automatically
-        @type bool
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        @param testCases list of test cases to be loaded
-        @type list of str
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        """
-        if clientType and clientType not in self.getSupportedLanguages():
-            # a not supported client language was requested
-            EricMessageBox.critical(
-                None,
-                self.tr("Start Debugger"),
-                self.tr(
-                    """<p>The debugger type <b>{0}</b> is not supported"""
-                    """ or not configured.</p>""").format(clientType)
-            )
-            return
-        
-        # Restart the client if there is already a program loaded.
-        try:
-            if clientType:
-                self.__setClientType(clientType)
-            else:
-                self.__setClientType(
-                    self.__findLanguageForExtension(os.path.splitext(fn)[1]))
-        except KeyError:
-            self.__setClientType('Python3')    # assume it is a Python3 file
-        self.startClient(False, forProject=forProject, venvName=venvName)
-        
-        self.debuggerInterface.remoteUTPrepare(
-            fn, tn, tfn, failed, cov, covname, coverase, syspath, workdir,
-            discover, discoveryStart, testCases, debug)
-        self.running = True
-        self.debugging = debug
-        if debug:
-            self.__restoreBreakpoints()
-            self.__restoreWatchpoints()
-    
-    def remoteUTRun(self, debug=False, failfast=False):
-        """
-        Public method to start a unittest run.
-        
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        @param failfast flag indicating to stop at the first error
-        @type bool
-        """
-        self.debuggerInterface.remoteUTRun(debug, failfast)
-    
-    def remoteUTStop(self):
-        """
-        public method to stop a unittest run.
-        """
-        self.debuggerInterface.remoteUTStop()
-    
     def signalClientOutput(self, line, debuggerId):
         """
         Public method to process a line of client output.
@@ -2036,125 +1876,6 @@
             isCall, fromFile, fromLine, fromFunction,
             toFile, toLine, toFunction, debuggerId)
     
-    def clientUtDiscovered(self, testCases, exceptionType, exceptionValue):
-        """
-        Public method to process the client unittest discover info.
-        
-        @param testCases list of detected test cases
-        @type str
-        @param exceptionType exception type
-        @type str
-        @param exceptionValue exception message
-        @type str
-        """
-        self.utDiscovered.emit(testCases, exceptionType, exceptionValue)
-    
-    def clientUtPrepared(self, result, exceptionType, exceptionValue):
-        """
-        Public method to process the client unittest prepared info.
-        
-        @param result number of test cases (0 = error)
-        @type int
-        @param exceptionType exception type
-        @type str
-        @param exceptionValue exception message
-        @type str
-        """
-        self.utPrepared.emit(result, exceptionType, exceptionValue)
-    
-    def clientUtStartTest(self, testname, doc):
-        """
-        Public method to process the client start test info.
-        
-        @param testname name of the test
-        @type str
-        @param doc short description of the test
-        @type str
-        """
-        self.utStartTest.emit(testname, doc)
-    
-    def clientUtStopTest(self):
-        """
-        Public method to process the client stop test info.
-        """
-        self.utStopTest.emit()
-    
-    def clientUtTestFailed(self, testname, traceback, testId):
-        """
-        Public method to process the client test failed info.
-        
-        @param testname name of the test
-        @type str
-        @param traceback lines of traceback info
-        @type list of str
-        @param testId id of the test
-        @type str
-        """
-        self.utTestFailed.emit(testname, traceback, testId)
-    
-    def clientUtTestErrored(self, testname, traceback, testId):
-        """
-        Public method to process the client test errored info.
-        
-        @param testname name of the test
-        @type str
-        @param traceback lines of traceback info
-        @type list of str
-        @param testId id of the test
-        @type str
-        """
-        self.utTestErrored.emit(testname, traceback, testId)
-    
-    def clientUtTestSkipped(self, testname, reason, testId):
-        """
-        Public method to process the client test skipped info.
-        
-        @param testname name of the test
-        @type str
-        @param reason reason for skipping the test
-        @type str
-        @param testId id of the test
-        @type str
-        """
-        self.utTestSkipped.emit(testname, reason, testId)
-    
-    def clientUtTestFailedExpected(self, testname, traceback, testId):
-        """
-        Public method to process the client test failed expected info.
-        
-        @param testname name of the test
-        @type str
-        @param traceback lines of traceback info
-        @type list of str
-        @param testId id of the test
-        @type str
-        """
-        self.utTestFailedExpected.emit(testname, traceback, testId)
-    
-    def clientUtTestSucceededUnexpected(self, testname, testId):
-        """
-        Public method to process the client test succeeded unexpected info.
-        
-        @param testname name of the test
-        @type str
-        @param testId id of the test
-        @type str
-        """
-        self.utTestSucceededUnexpected.emit(testname, testId)
-    
-    def clientUtFinished(self, status):
-        """
-        Public method to process the client unit test finished info.
-        
-        @param status exit status of the unit test
-        @type int
-        """
-        self.utFinished.emit()
-        
-        self.clientExit.emit("", int(status), "", True, "")
-        self.debugging = False
-        self.running = False
-    
     def passiveStartUp(self, fn, exc, debuggerId):
         """
         Public method to handle a passive debug connection.
--- a/eric7/Debugger/DebuggerInterfaceNone.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Debugger/DebuggerInterfaceNone.py	Thu May 19 14:40:15 2022 +0200
@@ -543,75 +543,7 @@
         @type str
         """
         return
-        
-    def remoteUTDiscover(self, syspath, workdir, discoveryStart):
-        """
-        Public method to perform a test case discovery.
-        
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        """
-        return
-    
-    def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase,
-                        syspath, workdir, discover, discoveryStart, testCases,
-                        debug):
-        """
-        Public method to prepare a new unittest run.
-        
-        @param fn name of file to load
-        @type str
-        @param tn name of test to load
-        @type str
-        @param tfn test function name to load tests from
-        @type str
-        @param failed list of failed test, if only failed test should be run
-        @type list of str
-        @param cov flag indicating collection of coverage data is requested
-        @type bool
-        @param covname name of file to be used to assemble the coverage caches
-            filename
-        @type str
-        @param coverase flag indicating erasure of coverage data is requested
-        @type bool
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discover flag indicating to discover the tests automatically
-        @type bool
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        @param testCases list of test cases to be loaded
-        @type list of str
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        """
-        return
-        
-    def remoteUTRun(self, debug, failfast):
-        """
-        Public method to start a unittest run.
-        
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        @param failfast flag indicating to stop at the first error
-        @type bool
-        """
-        return
-        
-    def remoteUTStop(self):
-        """
-        public method to stop a unittest run.
-        """
-        return
-    
+
 
 def createDebuggerInterfaceNone(debugServer, passive):
     """
--- a/eric7/Debugger/DebuggerInterfacePython.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Debugger/DebuggerInterfacePython.py	Thu May 19 14:40:15 2022 +0200
@@ -366,10 +366,6 @@
         
         # start debugger with project specific settings
         debugClient = project.getDebugProperty("DEBUGCLIENT")
-        if not venvName:
-            venvName = project.getDebugProperty("VIRTUALENV")
-        if not venvName and project.getProjectLanguage() == "Python3":
-            venvName = Preferences.getDebugger("Python3VirtualEnv")
         
         redirect = (
             str(configOverride["redirect"])
@@ -384,14 +380,8 @@
             else ''
         )
         
-        venvManager = ericApp().getObject("VirtualEnvManager")
-        interpreter = venvManager.getVirtualenvInterpreter(venvName)
-        execPath = venvManager.getVirtualenvExecPath(venvName)
-        if (
-            interpreter == "" and
-            project.getProjectLanguage().startswith("Python")
-        ):
-            interpreter = Globals.getPythonExecutable()
+        execPath = project.getProjectExecPath()
+        interpreter = project.getProjectInterpreter()
         if interpreter == "":
             EricMessageBox.critical(
                 None,
@@ -1215,104 +1205,6 @@
             "text": text,
         }, debuggerId)
     
-    def remoteUTDiscover(self, syspath, workdir, discoveryStart):
-        """
-        Public method to perform a test case discovery.
-        
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        """
-        self.__sendJsonCommand("RequestUTDiscover", {
-            "syspath": [] if syspath is None else syspath,
-            "workdir": workdir,
-            "discoverystart": discoveryStart,
-        })
-    
-    def remoteUTPrepare(self, fn, tn, tfn, failed, cov, covname, coverase,
-                        syspath, workdir, discover, discoveryStart, testCases,
-                        debug):
-        """
-        Public method to prepare a new unittest run.
-        
-        @param fn name of file to load
-        @type str
-        @param tn name of test to load
-        @type str
-        @param tfn test function name to load tests from
-        @type str
-        @param failed list of failed test, if only failed test should be run
-        @type list of str
-        @param cov flag indicating collection of coverage data is requested
-        @type bool
-        @param covname name of file to be used to assemble the coverage caches
-            filename
-        @type str
-        @param coverase flag indicating erasure of coverage data is requested
-        @type bool
-        @param syspath list of directories to be added to sys.path on the
-            remote side
-        @type list of str
-        @param workdir path name of the working directory
-        @type str
-        @param discover flag indicating to discover the tests automatically
-        @type bool
-        @param discoveryStart directory to start auto-discovery at
-        @type str
-        @param testCases list of test cases to be loaded
-        @type list of str
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        """
-        if fn:
-            self.__scriptName = os.path.abspath(fn)
-            
-            fn = self.translate(os.path.abspath(fn), False)
-        else:
-            self.__scriptName = "unittest discover"
-        
-        self.__sendJsonCommand("RequestUTPrepare", {
-            "filename": fn,
-            "testname": tn,
-            "testfunctionname": tfn,
-            "failed": failed,
-            "coverage": cov,
-            "coveragefile": covname,
-            "coverageerase": coverase,
-            "syspath": [] if syspath is None else syspath,
-            "workdir": workdir,
-            "discover": discover,
-            "discoverystart": discoveryStart,
-            "testcases": [] if testCases is None else testCases,
-            "debug": debug,
-        })
-    
-    def remoteUTRun(self, debug, failfast):
-        """
-        Public method to start a unittest run.
-        
-        @param debug flag indicating to run unittest with debugging
-        @type bool
-        @param failfast flag indicating to stop at the first error
-        @type bool
-        """
-        if debug:
-            self.__autoContinue = True
-        self.__sendJsonCommand("RequestUTRun", {
-            "debug": debug,
-            "failfast": failfast,
-        })
-    
-    def remoteUTStop(self):
-        """
-        Public method to stop a unittest run.
-        """
-        self.__sendJsonCommand("RequestUTStop", {})
-    
     def __parseClientLine(self, sock):
         """
         Private method to handle data from the client.
@@ -1519,49 +1411,6 @@
         elif method == "ResponseCompletion":
             self.debugServer.signalClientCompletionList(
                 params["completions"], params["text"], params["debuggerId"])
-        
-        ###################################################################
-        ## Unit test related stuff is not done with multi processing
-        ###################################################################
-        
-        elif method == "ResponseUTDiscover":
-            self.debugServer.clientUtDiscovered(
-                params["testCasesList"], params["exception"],
-                params["message"])
-        
-        elif method == "ResponseUTPrepared":
-            self.debugServer.clientUtPrepared(
-                params["count"], params["exception"], params["message"])
-        
-        elif method == "ResponseUTFinished":
-            self.debugServer.clientUtFinished(params["status"])
-        
-        elif method == "ResponseUTStartTest":
-            self.debugServer.clientUtStartTest(
-                params["testname"], params["description"])
-        
-        elif method == "ResponseUTStopTest":
-            self.debugServer.clientUtStopTest()
-        
-        elif method == "ResponseUTTestFailed":
-            self.debugServer.clientUtTestFailed(
-                params["testname"], params["traceback"], params["id"])
-        
-        elif method == "ResponseUTTestErrored":
-            self.debugServer.clientUtTestErrored(
-                params["testname"], params["traceback"], params["id"])
-        
-        elif method == "ResponseUTTestSkipped":
-            self.debugServer.clientUtTestSkipped(
-                params["testname"], params["reason"], params["id"])
-        
-        elif method == "ResponseUTTestFailedExpected":
-            self.debugServer.clientUtTestFailedExpected(
-                params["testname"], params["traceback"], params["id"])
-        
-        elif method == "ResponseUTTestSucceededUnexpected":
-            self.debugServer.clientUtTestSucceededUnexpected(
-                params["testname"], params["id"])
     
     def __sendJsonCommand(self, command, params, debuggerId="", sock=None):
         """
--- a/eric7/Documentation/Source/eric7.DebugClients.Python.DCTestResult.html	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,259 +0,0 @@
-<!DOCTYPE html>
-<html><head>
-<title>eric7.DebugClients.Python.DCTestResult</title>
-<meta charset="UTF-8">
-<link rel="stylesheet" href="styles.css">
-</head>
-<body>
-<a NAME="top" ID="top"></a>
-<h1>eric7.DebugClients.Python.DCTestResult</h1>
-
-<p>
-Module implementing a TestResult derivative for the eric debugger.
-</p>
-<h3>Global Attributes</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Classes</h3>
-
-<table>
-
-<tr>
-<td><a href="#DCTestResult">DCTestResult</a></td>
-<td>A TestResult derivative to work with eric's debug client.</td>
-</tr>
-</table>
-<h3>Functions</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<hr />
-<hr />
-<a NAME="DCTestResult" ID="DCTestResult"></a>
-<h2>DCTestResult</h2>
-
-<p>
-    A TestResult derivative to work with eric's debug client.
-</p>
-<p>
-    For more details see unittest.py of the standard python distribution.
-</p>
-<h3>Derived from</h3>
-TestResult
-<h3>Class Attributes</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Class Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Methods</h3>
-
-<table>
-
-<tr>
-<td><a href="#DCTestResult.__init__">DCTestResult</a></td>
-<td>Constructor</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addError">addError</a></td>
-<td>Public method called if a test errored.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addExpectedFailure">addExpectedFailure</a></td>
-<td>Public method called if a test failed expected.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addFailure">addFailure</a></td>
-<td>Public method called if a test failed.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addSkip">addSkip</a></td>
-<td>Public method called if a test was skipped.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addSubTest">addSubTest</a></td>
-<td>Public method called for each subtest to record its result.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.addUnexpectedSuccess">addUnexpectedSuccess</a></td>
-<td>Public method called if a test succeeded expectedly.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.startTest">startTest</a></td>
-<td>Public method called at the start of a test.</td>
-</tr>
-<tr>
-<td><a href="#DCTestResult.stopTest">stopTest</a></td>
-<td>Public method called at the end of a test.</td>
-</tr>
-</table>
-<h3>Static Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-
-<a NAME="DCTestResult.__init__" ID="DCTestResult.__init__"></a>
-<h4>DCTestResult (Constructor)</h4>
-<b>DCTestResult</b>(<i>dbgClient, failfast</i>)
-
-<p>
-        Constructor
-</p>
-<dl>
-
-<dt><i>dbgClient</i> (DebugClientBase)</dt>
-<dd>
-reference to the debug client
-</dd>
-<dt><i>failfast</i> (bool)</dt>
-<dd>
-flag indicating to stop at the first error
-</dd>
-</dl>
-<a NAME="DCTestResult.addError" ID="DCTestResult.addError"></a>
-<h4>DCTestResult.addError</h4>
-<b>addError</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test errored.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-The error traceback
-</dd>
-</dl>
-<a NAME="DCTestResult.addExpectedFailure" ID="DCTestResult.addExpectedFailure"></a>
-<h4>DCTestResult.addExpectedFailure</h4>
-<b>addExpectedFailure</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test failed expected.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="DCTestResult.addFailure" ID="DCTestResult.addFailure"></a>
-<h4>DCTestResult.addFailure</h4>
-<b>addFailure</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test failed.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-The error traceback
-</dd>
-</dl>
-<a NAME="DCTestResult.addSkip" ID="DCTestResult.addSkip"></a>
-<h4>DCTestResult.addSkip</h4>
-<b>addSkip</b>(<i>test, reason</i>)
-
-<p>
-        Public method called if a test was skipped.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>reason</i></dt>
-<dd>
-reason for skipping the test (string)
-</dd>
-</dl>
-<a NAME="DCTestResult.addSubTest" ID="DCTestResult.addSubTest"></a>
-<h4>DCTestResult.addSubTest</h4>
-<b>addSubTest</b>(<i>test, subtest, err</i>)
-
-<p>
-        Public method called for each subtest to record its result.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>subtest</i></dt>
-<dd>
-reference to the subtest object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="DCTestResult.addUnexpectedSuccess" ID="DCTestResult.addUnexpectedSuccess"></a>
-<h4>DCTestResult.addUnexpectedSuccess</h4>
-<b>addUnexpectedSuccess</b>(<i>test</i>)
-
-<p>
-        Public method called if a test succeeded expectedly.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-</dl>
-<a NAME="DCTestResult.startTest" ID="DCTestResult.startTest"></a>
-<h4>DCTestResult.startTest</h4>
-<b>startTest</b>(<i>test</i>)
-
-<p>
-        Public method called at the start of a test.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-</dl>
-<a NAME="DCTestResult.stopTest" ID="DCTestResult.stopTest"></a>
-<h4>DCTestResult.stopTest</h4>
-<b>stopTest</b>(<i>test</i>)
-
-<p>
-        Public method called at the end of a test.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-</dl>
-<div align="right"><a href="#top">Up</a></div>
-<hr />
-</body></html>
\ No newline at end of file
--- a/eric7/Documentation/Source/eric7.PyUnit.UnittestDialog.html	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1236 +0,0 @@
-<!DOCTYPE html>
-<html><head>
-<title>eric7.PyUnit.UnittestDialog</title>
-<meta charset="UTF-8">
-<link rel="stylesheet" href="styles.css">
-</head>
-<body>
-<a NAME="top" ID="top"></a>
-<h1>eric7.PyUnit.UnittestDialog</h1>
-
-<p>
-Module implementing the UI to the pyunit package.
-</p>
-<h3>Global Attributes</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Classes</h3>
-
-<table>
-
-<tr>
-<td><a href="#QtTestResult">QtTestResult</a></td>
-<td>A TestResult derivative to work with a graphical GUI.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog">UnittestDialog</a></td>
-<td>Class implementing the UI to the pyunit package.</td>
-</tr>
-<tr>
-<td><a href="#UnittestWindow">UnittestWindow</a></td>
-<td>Main window class for the standalone dialog.</td>
-</tr>
-</table>
-<h3>Functions</h3>
-
-<table>
-
-<tr>
-<td><a href="#clearSavedHistories">clearSavedHistories</a></td>
-<td>Function to clear the saved history lists.</td>
-</tr>
-</table>
-<hr />
-<hr />
-<a NAME="QtTestResult" ID="QtTestResult"></a>
-<h2>QtTestResult</h2>
-
-<p>
-    A TestResult derivative to work with a graphical GUI.
-</p>
-<p>
-    For more details see pyunit.py of the standard Python distribution.
-</p>
-<h3>Derived from</h3>
-unittest.TestResult
-<h3>Class Attributes</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Class Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Methods</h3>
-
-<table>
-
-<tr>
-<td><a href="#QtTestResult.__init__">QtTestResult</a></td>
-<td>Constructor</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addError">addError</a></td>
-<td>Public method called if a test errored.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addExpectedFailure">addExpectedFailure</a></td>
-<td>Public method called if a test failed expected.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addFailure">addFailure</a></td>
-<td>Public method called if a test failed.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addSkip">addSkip</a></td>
-<td>Public method called if a test was skipped.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addSubTest">addSubTest</a></td>
-<td>Public method called for each subtest to record its result.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.addUnexpectedSuccess">addUnexpectedSuccess</a></td>
-<td>Public method called if a test succeeded expectedly.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.startTest">startTest</a></td>
-<td>Public method called at the start of a test.</td>
-</tr>
-<tr>
-<td><a href="#QtTestResult.stopTest">stopTest</a></td>
-<td>Public method called at the end of a test.</td>
-</tr>
-</table>
-<h3>Static Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-
-<a NAME="QtTestResult.__init__" ID="QtTestResult.__init__"></a>
-<h4>QtTestResult (Constructor)</h4>
-<b>QtTestResult</b>(<i>parent, failfast</i>)
-
-<p>
-        Constructor
-</p>
-<dl>
-
-<dt><i>parent</i> (UnittestDialog)</dt>
-<dd>
-reference to the parent widget
-</dd>
-<dt><i>failfast</i> (bool)</dt>
-<dd>
-flag indicating to stop at the first error
-</dd>
-</dl>
-<a NAME="QtTestResult.addError" ID="QtTestResult.addError"></a>
-<h4>QtTestResult.addError</h4>
-<b>addError</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test errored.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="QtTestResult.addExpectedFailure" ID="QtTestResult.addExpectedFailure"></a>
-<h4>QtTestResult.addExpectedFailure</h4>
-<b>addExpectedFailure</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test failed expected.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="QtTestResult.addFailure" ID="QtTestResult.addFailure"></a>
-<h4>QtTestResult.addFailure</h4>
-<b>addFailure</b>(<i>test, err</i>)
-
-<p>
-        Public method called if a test failed.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="QtTestResult.addSkip" ID="QtTestResult.addSkip"></a>
-<h4>QtTestResult.addSkip</h4>
-<b>addSkip</b>(<i>test, reason</i>)
-
-<p>
-        Public method called if a test was skipped.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>reason</i></dt>
-<dd>
-reason for skipping the test (string)
-</dd>
-</dl>
-<a NAME="QtTestResult.addSubTest" ID="QtTestResult.addSubTest"></a>
-<h4>QtTestResult.addSubTest</h4>
-<b>addSubTest</b>(<i>test, subtest, err</i>)
-
-<p>
-        Public method called for each subtest to record its result.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-<dt><i>subtest</i></dt>
-<dd>
-reference to the subtest object
-</dd>
-<dt><i>err</i></dt>
-<dd>
-error traceback
-</dd>
-</dl>
-<a NAME="QtTestResult.addUnexpectedSuccess" ID="QtTestResult.addUnexpectedSuccess"></a>
-<h4>QtTestResult.addUnexpectedSuccess</h4>
-<b>addUnexpectedSuccess</b>(<i>test</i>)
-
-<p>
-        Public method called if a test succeeded expectedly.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-reference to the test object
-</dd>
-</dl>
-<a NAME="QtTestResult.startTest" ID="QtTestResult.startTest"></a>
-<h4>QtTestResult.startTest</h4>
-<b>startTest</b>(<i>test</i>)
-
-<p>
-        Public method called at the start of a test.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-</dl>
-<a NAME="QtTestResult.stopTest" ID="QtTestResult.stopTest"></a>
-<h4>QtTestResult.stopTest</h4>
-<b>stopTest</b>(<i>test</i>)
-
-<p>
-        Public method called at the end of a test.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-Reference to the test object
-</dd>
-</dl>
-<div align="right"><a href="#top">Up</a></div>
-<hr />
-<hr />
-<a NAME="UnittestDialog" ID="UnittestDialog"></a>
-<h2>UnittestDialog</h2>
-
-<p>
-    Class implementing the UI to the pyunit package.
-</p>
-<h3>Signals</h3>
-<dl>
-
-<dt>unittestFile(str, int, bool)</dt>
-<dd>
-emitted to show the source of a
-        unittest file
-</dd>
-<dt>unittestStopped()</dt>
-<dd>
-emitted after a unit test was run
-</dd>
-</dl>
-<h3>Derived from</h3>
-QWidget, Ui_UnittestDialog
-<h3>Class Attributes</h3>
-
-<table>
-<tr><td>ErrorsInfoRole</td></tr><tr><td>FailedExpectedColorDarkTheme</td></tr><tr><td>FailedExpectedColorLightTheme</td></tr><tr><td>SkippedColorDarkTheme</td></tr><tr><td>SkippedColorLightTheme</td></tr><tr><td>SucceededUnexpectedColorDarkTheme</td></tr><tr><td>SucceededUnexpectedColorLightTheme</td></tr><tr><td>TestCaseFileRole</td></tr><tr><td>TestCaseNameRole</td></tr>
-</table>
-<h3>Class Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Methods</h3>
-
-<table>
-
-<tr>
-<td><a href="#UnittestDialog.__init__">UnittestDialog</a></td>
-<td>Constructor</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__UTDiscovered">__UTDiscovered</a></td>
-<td>Private slot to handle the utDiscovered signal.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__UTPrepared">__UTPrepared</a></td>
-<td>Private slot to handle the utPrepared signal.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__assembleTestCasesList">__assembleTestCasesList</a></td>
-<td>Private method to assemble a list of test cases included in a test suite.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__discover">__discover</a></td>
-<td>Private slot to discover unit test but don't run them.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__findDiscoveryItem">__findDiscoveryItem</a></td>
-<td>Private method to find an item given the module path.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__loadRecent">__loadRecent</a></td>
-<td>Private method to load the most recently used lists.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__openEditor">__openEditor</a></td>
-<td>Private method to open an editor window for the given file.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__populateDiscoveryResults">__populateDiscoveryResults</a></td>
-<td>Private method to populate the test discovery results list.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__populateVenvComboBox">__populateVenvComboBox</a></td>
-<td>Private method to (re-)populate the virtual environments selector.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__saveRecent">__saveRecent</a></td>
-<td>Private method to save the most recently used lists.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__selectedTestCases">__selectedTestCases</a></td>
-<td>Private method to assemble the list of selected test cases and suites.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__setProgressColor">__setProgressColor</a></td>
-<td>Private method to set the color of the progress color label.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__setRunningMode">__setRunningMode</a></td>
-<td>Private method to set the GUI in running mode.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__setStoppedMode">__setStoppedMode</a></td>
-<td>Private method to set the GUI in stopped mode.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__showSource">__showSource</a></td>
-<td>Private slot to show the source of a traceback in an eric editor.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.__stopTests">__stopTests</a></td>
-<td>Private slot to stop the test.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.clearRecent">clearRecent</a></td>
-<td>Public slot to clear the recently used lists.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.closeEvent">closeEvent</a></td>
-<td>Protected method to handle the close event.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.hasFailedTests">hasFailedTests</a></td>
-<td>Public method to check, if there are failed tests from the last run.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.insertDiscovery">insertDiscovery</a></td>
-<td>Public slot to insert the discovery start directory into the discoveryPicker object.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.insertProg">insertProg</a></td>
-<td>Public slot to insert the filename prog into the testsuitePicker object.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.insertTestName">insertTestName</a></td>
-<td>Public slot to insert a test name into the testComboBox object.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.keyPressEvent">keyPressEvent</a></td>
-<td>Protected slot to handle key press events.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_buttonBox_clicked">on_buttonBox_clicked</a></td>
-<td>Private slot called by a button of the button box clicked.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_discoverCheckBox_toggled">on_discoverCheckBox_toggled</a></td>
-<td>Private slot handling state changes of the 'discover' checkbox.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_discoveryList_itemChanged">on_discoveryList_itemChanged</a></td>
-<td>Private slot handling the user checking or unchecking an item.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_discoveryList_itemDoubleClicked">on_discoveryList_itemDoubleClicked</a></td>
-<td>Private slot handling the user double clicking an item.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_errorsListWidget_currentTextChanged">on_errorsListWidget_currentTextChanged</a></td>
-<td>Private slot to handle the highlighted signal.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_errorsListWidget_itemDoubleClicked">on_errorsListWidget_itemDoubleClicked</a></td>
-<td>Private slot called by doubleclicking an errorlist entry.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_testsuitePicker_aboutToShowPathPickerDialog">on_testsuitePicker_aboutToShowPathPickerDialog</a></td>
-<td>Private slot called before the test suite selection dialog is shown.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_testsuitePicker_editTextChanged">on_testsuitePicker_editTextChanged</a></td>
-<td>Private slot handling changes of the test suite path.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.on_testsuitePicker_pathSelected">on_testsuitePicker_pathSelected</a></td>
-<td>Private slot called after a test suite has been selected.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.setProjectMode">setProjectMode</a></td>
-<td>Public method to set the project mode of the dialog.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.startTests">startTests</a></td>
-<td>Public slot to start the test.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testErrored">testErrored</a></td>
-<td>Public method called if a test errors.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testFailed">testFailed</a></td>
-<td>Public method called if a test fails.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testFailedExpected">testFailedExpected</a></td>
-<td>Public method called if a test fails as expected.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testFinished">testFinished</a></td>
-<td>Public method called if a test has finished.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testSkipped">testSkipped</a></td>
-<td>Public method called if a test was skipped.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testStarted">testStarted</a></td>
-<td>Public method called if a test is about to be run.</td>
-</tr>
-<tr>
-<td><a href="#UnittestDialog.testSucceededUnexpected">testSucceededUnexpected</a></td>
-<td>Public method called if a test succeeds unexpectedly.</td>
-</tr>
-</table>
-<h3>Static Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-
-<a NAME="UnittestDialog.__init__" ID="UnittestDialog.__init__"></a>
-<h4>UnittestDialog (Constructor)</h4>
-<b>UnittestDialog</b>(<i>prog=None, dbs=None, ui=None, parent=None, name=None</i>)
-
-<p>
-        Constructor
-</p>
-<dl>
-
-<dt><i>prog</i> (str)</dt>
-<dd>
-filename of the program to open
-</dd>
-<dt><i>dbs</i> (DebugServer)</dt>
-<dd>
-reference to the debug server object. It is an indication
-            whether we were called from within the eric IDE.
-</dd>
-<dt><i>ui</i> (UserInterface)</dt>
-<dd>
-reference to the UI object
-</dd>
-<dt><i>parent</i> (QWidget)</dt>
-<dd>
-parent widget of this dialog
-</dd>
-<dt><i>name</i> (str)</dt>
-<dd>
-name of this dialog
-</dd>
-</dl>
-<a NAME="UnittestDialog.__UTDiscovered" ID="UnittestDialog.__UTDiscovered"></a>
-<h4>UnittestDialog.__UTDiscovered</h4>
-<b>__UTDiscovered</b>(<i>testCases, exc_type, exc_value</i>)
-
-<p>
-        Private slot to handle the utDiscovered signal.
-</p>
-<p>
-        If the unittest suite was loaded successfully, we ask the
-        client to run the test suite.
-</p>
-<dl>
-
-<dt><i>testCases</i> (str)</dt>
-<dd>
-list of detected test cases
-</dd>
-<dt><i>exc_type</i> (str)</dt>
-<dd>
-exception type occured during discovery
-</dd>
-<dt><i>exc_value</i> (str)</dt>
-<dd>
-value of exception occured during discovery
-</dd>
-</dl>
-<a NAME="UnittestDialog.__UTPrepared" ID="UnittestDialog.__UTPrepared"></a>
-<h4>UnittestDialog.__UTPrepared</h4>
-<b>__UTPrepared</b>(<i>nrTests, exc_type, exc_value</i>)
-
-<p>
-        Private slot to handle the utPrepared signal.
-</p>
-<p>
-        If the unittest suite was loaded successfully, we ask the
-        client to run the test suite.
-</p>
-<dl>
-
-<dt><i>nrTests</i></dt>
-<dd>
-number of tests contained in the test suite (integer)
-</dd>
-<dt><i>exc_type</i></dt>
-<dd>
-type of exception occured during preparation (string)
-</dd>
-<dt><i>exc_value</i></dt>
-<dd>
-value of exception occured during preparation (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.__assembleTestCasesList" ID="UnittestDialog.__assembleTestCasesList"></a>
-<h4>UnittestDialog.__assembleTestCasesList</h4>
-<b>__assembleTestCasesList</b>(<i>suite, start</i>)
-
-<p>
-        Private method to assemble a list of test cases included in a test
-        suite.
-</p>
-<dl>
-
-<dt><i>suite</i> (unittest.TestSuite)</dt>
-<dd>
-test suite to be inspected
-</dd>
-<dt><i>start</i> (str)</dt>
-<dd>
-name of directory discovery was started at
-</dd>
-</dl>
-<dl>
-<dt>Return:</dt>
-<dd>
-list of tuples containing the test case ID, a short description
-            and the path of the test file name
-</dd>
-</dl>
-<dl>
-<dt>Return Type:</dt>
-<dd>
-list of tuples of (str, str, str)
-</dd>
-</dl>
-<a NAME="UnittestDialog.__discover" ID="UnittestDialog.__discover"></a>
-<h4>UnittestDialog.__discover</h4>
-<b>__discover</b>(<i></i>)
-
-<p>
-        Private slot to discover unit test but don't run them.
-</p>
-<a NAME="UnittestDialog.__findDiscoveryItem" ID="UnittestDialog.__findDiscoveryItem"></a>
-<h4>UnittestDialog.__findDiscoveryItem</h4>
-<b>__findDiscoveryItem</b>(<i>modulePath</i>)
-
-<p>
-        Private method to find an item given the module path.
-</p>
-<dl>
-
-<dt><i>modulePath</i> (str)</dt>
-<dd>
-path of the module in dotted notation
-</dd>
-</dl>
-<dl>
-<dt>Return:</dt>
-<dd>
-reference to the item or None
-</dd>
-</dl>
-<dl>
-<dt>Return Type:</dt>
-<dd>
-QTreeWidgetItem or None
-</dd>
-</dl>
-<a NAME="UnittestDialog.__loadRecent" ID="UnittestDialog.__loadRecent"></a>
-<h4>UnittestDialog.__loadRecent</h4>
-<b>__loadRecent</b>(<i></i>)
-
-<p>
-        Private method to load the most recently used lists.
-</p>
-<a NAME="UnittestDialog.__openEditor" ID="UnittestDialog.__openEditor"></a>
-<h4>UnittestDialog.__openEditor</h4>
-<b>__openEditor</b>(<i>filename, linenumber</i>)
-
-<p>
-        Private method to open an editor window for the given file.
-</p>
-<p>
-        Note: This method opens an editor window when the unittest dialog
-        is called as a standalone application.
-</p>
-<dl>
-
-<dt><i>filename</i> (str)</dt>
-<dd>
-path of the file to be opened
-</dd>
-<dt><i>linenumber</i> (int)</dt>
-<dd>
-line number to place the cursor at
-</dd>
-</dl>
-<a NAME="UnittestDialog.__populateDiscoveryResults" ID="UnittestDialog.__populateDiscoveryResults"></a>
-<h4>UnittestDialog.__populateDiscoveryResults</h4>
-<b>__populateDiscoveryResults</b>(<i>tests</i>)
-
-<p>
-        Private method to populate the test discovery results list.
-</p>
-<dl>
-
-<dt><i>tests</i> (list of tuples of (str, str, str))</dt>
-<dd>
-list of tuples containing the discovery results
-</dd>
-</dl>
-<a NAME="UnittestDialog.__populateVenvComboBox" ID="UnittestDialog.__populateVenvComboBox"></a>
-<h4>UnittestDialog.__populateVenvComboBox</h4>
-<b>__populateVenvComboBox</b>(<i></i>)
-
-<p>
-        Private method to (re-)populate the virtual environments selector.
-</p>
-<a NAME="UnittestDialog.__saveRecent" ID="UnittestDialog.__saveRecent"></a>
-<h4>UnittestDialog.__saveRecent</h4>
-<b>__saveRecent</b>(<i></i>)
-
-<p>
-        Private method to save the most recently used lists.
-</p>
-<a NAME="UnittestDialog.__selectedTestCases" ID="UnittestDialog.__selectedTestCases"></a>
-<h4>UnittestDialog.__selectedTestCases</h4>
-<b>__selectedTestCases</b>(<i>parent=None</i>)
-
-<p>
-        Private method to assemble the list of selected test cases and suites.
-</p>
-<dl>
-
-<dt><i>parent</i> (QTreeWidgetItem)</dt>
-<dd>
-reference to the parent item
-</dd>
-</dl>
-<dl>
-<dt>Return:</dt>
-<dd>
-list of selected test cases
-</dd>
-</dl>
-<dl>
-<dt>Return Type:</dt>
-<dd>
-list of str
-</dd>
-</dl>
-<a NAME="UnittestDialog.__setProgressColor" ID="UnittestDialog.__setProgressColor"></a>
-<h4>UnittestDialog.__setProgressColor</h4>
-<b>__setProgressColor</b>(<i>color</i>)
-
-<p>
-        Private method to set the color of the progress color label.
-</p>
-<dl>
-
-<dt><i>color</i></dt>
-<dd>
-colour to be shown (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.__setRunningMode" ID="UnittestDialog.__setRunningMode"></a>
-<h4>UnittestDialog.__setRunningMode</h4>
-<b>__setRunningMode</b>(<i></i>)
-
-<p>
-        Private method to set the GUI in running mode.
-</p>
-<a NAME="UnittestDialog.__setStoppedMode" ID="UnittestDialog.__setStoppedMode"></a>
-<h4>UnittestDialog.__setStoppedMode</h4>
-<b>__setStoppedMode</b>(<i></i>)
-
-<p>
-        Private method to set the GUI in stopped mode.
-</p>
-<a NAME="UnittestDialog.__showSource" ID="UnittestDialog.__showSource"></a>
-<h4>UnittestDialog.__showSource</h4>
-<b>__showSource</b>(<i></i>)
-
-<p>
-        Private slot to show the source of a traceback in an eric editor.
-</p>
-<a NAME="UnittestDialog.__stopTests" ID="UnittestDialog.__stopTests"></a>
-<h4>UnittestDialog.__stopTests</h4>
-<b>__stopTests</b>(<i></i>)
-
-<p>
-        Private slot to stop the test.
-</p>
-<a NAME="UnittestDialog.clearRecent" ID="UnittestDialog.clearRecent"></a>
-<h4>UnittestDialog.clearRecent</h4>
-<b>clearRecent</b>(<i></i>)
-
-<p>
-        Public slot to clear the recently used lists.
-</p>
-<a NAME="UnittestDialog.closeEvent" ID="UnittestDialog.closeEvent"></a>
-<h4>UnittestDialog.closeEvent</h4>
-<b>closeEvent</b>(<i>event</i>)
-
-<p>
-        Protected method to handle the close event.
-</p>
-<dl>
-
-<dt><i>event</i> (QCloseEvent)</dt>
-<dd>
-close event
-</dd>
-</dl>
-<a NAME="UnittestDialog.hasFailedTests" ID="UnittestDialog.hasFailedTests"></a>
-<h4>UnittestDialog.hasFailedTests</h4>
-<b>hasFailedTests</b>(<i></i>)
-
-<p>
-        Public method to check, if there are failed tests from the last run.
-</p>
-<dl>
-<dt>Return:</dt>
-<dd>
-flag indicating the presence of failed tests (boolean)
-</dd>
-</dl>
-<a NAME="UnittestDialog.insertDiscovery" ID="UnittestDialog.insertDiscovery"></a>
-<h4>UnittestDialog.insertDiscovery</h4>
-<b>insertDiscovery</b>(<i>start</i>)
-
-<p>
-        Public slot to insert the discovery start directory into the
-        discoveryPicker object.
-</p>
-<dl>
-
-<dt><i>start</i> (str)</dt>
-<dd>
-start directory name to be inserted
-</dd>
-</dl>
-<a NAME="UnittestDialog.insertProg" ID="UnittestDialog.insertProg"></a>
-<h4>UnittestDialog.insertProg</h4>
-<b>insertProg</b>(<i>prog</i>)
-
-<p>
-        Public slot to insert the filename prog into the testsuitePicker
-        object.
-</p>
-<dl>
-
-<dt><i>prog</i></dt>
-<dd>
-filename to be inserted (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.insertTestName" ID="UnittestDialog.insertTestName"></a>
-<h4>UnittestDialog.insertTestName</h4>
-<b>insertTestName</b>(<i>testName</i>)
-
-<p>
-        Public slot to insert a test name into the testComboBox object.
-</p>
-<dl>
-
-<dt><i>testName</i></dt>
-<dd>
-name of the test to be inserted (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.keyPressEvent" ID="UnittestDialog.keyPressEvent"></a>
-<h4>UnittestDialog.keyPressEvent</h4>
-<b>keyPressEvent</b>(<i>evt</i>)
-
-<p>
-        Protected slot to handle key press events.
-</p>
-<dl>
-
-<dt><i>evt</i></dt>
-<dd>
-key press event to handle (QKeyEvent)
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_buttonBox_clicked" ID="UnittestDialog.on_buttonBox_clicked"></a>
-<h4>UnittestDialog.on_buttonBox_clicked</h4>
-<b>on_buttonBox_clicked</b>(<i>button</i>)
-
-<p>
-        Private slot called by a button of the button box clicked.
-</p>
-<dl>
-
-<dt><i>button</i></dt>
-<dd>
-button that was clicked (QAbstractButton)
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_discoverCheckBox_toggled" ID="UnittestDialog.on_discoverCheckBox_toggled"></a>
-<h4>UnittestDialog.on_discoverCheckBox_toggled</h4>
-<b>on_discoverCheckBox_toggled</b>(<i>checked</i>)
-
-<p>
-        Private slot handling state changes of the 'discover' checkbox.
-</p>
-<dl>
-
-<dt><i>checked</i> (bool)</dt>
-<dd>
-state of the checkbox
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_discoveryList_itemChanged" ID="UnittestDialog.on_discoveryList_itemChanged"></a>
-<h4>UnittestDialog.on_discoveryList_itemChanged</h4>
-<b>on_discoveryList_itemChanged</b>(<i>item, column</i>)
-
-<p>
-        Private slot handling the user checking or unchecking an item.
-</p>
-<dl>
-
-<dt><i>item</i> (QTreeWidgetItem)</dt>
-<dd>
-reference to the item
-</dd>
-<dt><i>column</i> (int)</dt>
-<dd>
-changed column
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_discoveryList_itemDoubleClicked" ID="UnittestDialog.on_discoveryList_itemDoubleClicked"></a>
-<h4>UnittestDialog.on_discoveryList_itemDoubleClicked</h4>
-<b>on_discoveryList_itemDoubleClicked</b>(<i>item, column</i>)
-
-<p>
-        Private slot handling the user double clicking an item.
-</p>
-<dl>
-
-<dt><i>item</i> (QTreeWidgetItem)</dt>
-<dd>
-reference to the item
-</dd>
-<dt><i>column</i> (int)</dt>
-<dd>
-column of the double click
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_errorsListWidget_currentTextChanged" ID="UnittestDialog.on_errorsListWidget_currentTextChanged"></a>
-<h4>UnittestDialog.on_errorsListWidget_currentTextChanged</h4>
-<b>on_errorsListWidget_currentTextChanged</b>(<i>text</i>)
-
-<p>
-        Private slot to handle the highlighted signal.
-</p>
-<dl>
-
-<dt><i>text</i></dt>
-<dd>
-current text (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_errorsListWidget_itemDoubleClicked" ID="UnittestDialog.on_errorsListWidget_itemDoubleClicked"></a>
-<h4>UnittestDialog.on_errorsListWidget_itemDoubleClicked</h4>
-<b>on_errorsListWidget_itemDoubleClicked</b>(<i>lbitem</i>)
-
-<p>
-        Private slot called by doubleclicking an errorlist entry.
-</p>
-<p>
-        It will popup a dialog showing the stacktrace.
-        If called from eric, an additional button is displayed
-        to show the python source in an eric source viewer (in
-        erics main window.
-</p>
-<dl>
-
-<dt><i>lbitem</i></dt>
-<dd>
-the listbox item that was double clicked
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_testsuitePicker_aboutToShowPathPickerDialog" ID="UnittestDialog.on_testsuitePicker_aboutToShowPathPickerDialog"></a>
-<h4>UnittestDialog.on_testsuitePicker_aboutToShowPathPickerDialog</h4>
-<b>on_testsuitePicker_aboutToShowPathPickerDialog</b>(<i></i>)
-
-<p>
-        Private slot called before the test suite selection dialog is shown.
-</p>
-<a NAME="UnittestDialog.on_testsuitePicker_editTextChanged" ID="UnittestDialog.on_testsuitePicker_editTextChanged"></a>
-<h4>UnittestDialog.on_testsuitePicker_editTextChanged</h4>
-<b>on_testsuitePicker_editTextChanged</b>(<i>path</i>)
-
-<p>
-        Private slot handling changes of the test suite path.
-</p>
-<dl>
-
-<dt><i>path</i> (str)</dt>
-<dd>
-path of the test suite file
-</dd>
-</dl>
-<a NAME="UnittestDialog.on_testsuitePicker_pathSelected" ID="UnittestDialog.on_testsuitePicker_pathSelected"></a>
-<h4>UnittestDialog.on_testsuitePicker_pathSelected</h4>
-<b>on_testsuitePicker_pathSelected</b>(<i>suite</i>)
-
-<p>
-        Private slot called after a test suite has been selected.
-</p>
-<dl>
-
-<dt><i>suite</i> (str)</dt>
-<dd>
-file name of the test suite
-</dd>
-</dl>
-<a NAME="UnittestDialog.setProjectMode" ID="UnittestDialog.setProjectMode"></a>
-<h4>UnittestDialog.setProjectMode</h4>
-<b>setProjectMode</b>(<i>forProject</i>)
-
-<p>
-        Public method to set the project mode of the dialog.
-</p>
-<dl>
-
-<dt><i>forProject</i> (bool)</dt>
-<dd>
-flag indicating to run for the open project
-</dd>
-</dl>
-<a NAME="UnittestDialog.startTests" ID="UnittestDialog.startTests"></a>
-<h4>UnittestDialog.startTests</h4>
-<b>startTests</b>(<i>failedOnly=False</i>)
-
-<p>
-        Public slot to start the test.
-</p>
-<dl>
-
-<dt><i>failedOnly</i></dt>
-<dd>
-flag indicating to run only failed tests (boolean)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testErrored" ID="UnittestDialog.testErrored"></a>
-<h4>UnittestDialog.testErrored</h4>
-<b>testErrored</b>(<i>test, exc, testId</i>)
-
-<p>
-        Public method called if a test errors.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the test (string)
-</dd>
-<dt><i>exc</i></dt>
-<dd>
-string representation of the exception (string)
-</dd>
-<dt><i>testId</i></dt>
-<dd>
-id of the test (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testFailed" ID="UnittestDialog.testFailed"></a>
-<h4>UnittestDialog.testFailed</h4>
-<b>testFailed</b>(<i>test, exc, testId</i>)
-
-<p>
-        Public method called if a test fails.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the test (string)
-</dd>
-<dt><i>exc</i></dt>
-<dd>
-string representation of the exception (string)
-</dd>
-<dt><i>testId</i></dt>
-<dd>
-id of the test (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testFailedExpected" ID="UnittestDialog.testFailedExpected"></a>
-<h4>UnittestDialog.testFailedExpected</h4>
-<b>testFailedExpected</b>(<i>test, exc, testId</i>)
-
-<p>
-        Public method called if a test fails as expected.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the test (string)
-</dd>
-<dt><i>exc</i></dt>
-<dd>
-string representation of the exception (string)
-</dd>
-<dt><i>testId</i></dt>
-<dd>
-id of the test (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testFinished" ID="UnittestDialog.testFinished"></a>
-<h4>UnittestDialog.testFinished</h4>
-<b>testFinished</b>(<i></i>)
-
-<p>
-        Public method called if a test has finished.
-</p>
-<p>
-        <b>Note</b>: It is also called if it has already failed or errored.
-</p>
-<a NAME="UnittestDialog.testSkipped" ID="UnittestDialog.testSkipped"></a>
-<h4>UnittestDialog.testSkipped</h4>
-<b>testSkipped</b>(<i>test, reason, testId</i>)
-
-<p>
-        Public method called if a test was skipped.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the test (string)
-</dd>
-<dt><i>reason</i></dt>
-<dd>
-reason for skipping the test (string)
-</dd>
-<dt><i>testId</i></dt>
-<dd>
-id of the test (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testStarted" ID="UnittestDialog.testStarted"></a>
-<h4>UnittestDialog.testStarted</h4>
-<b>testStarted</b>(<i>test, doc</i>)
-
-<p>
-        Public method called if a test is about to be run.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the started test (string)
-</dd>
-<dt><i>doc</i></dt>
-<dd>
-documentation of the started test (string)
-</dd>
-</dl>
-<a NAME="UnittestDialog.testSucceededUnexpected" ID="UnittestDialog.testSucceededUnexpected"></a>
-<h4>UnittestDialog.testSucceededUnexpected</h4>
-<b>testSucceededUnexpected</b>(<i>test, testId</i>)
-
-<p>
-        Public method called if a test succeeds unexpectedly.
-</p>
-<dl>
-
-<dt><i>test</i></dt>
-<dd>
-name of the test (string)
-</dd>
-<dt><i>testId</i></dt>
-<dd>
-id of the test (string)
-</dd>
-</dl>
-<div align="right"><a href="#top">Up</a></div>
-<hr />
-<hr />
-<a NAME="UnittestWindow" ID="UnittestWindow"></a>
-<h2>UnittestWindow</h2>
-
-<p>
-    Main window class for the standalone dialog.
-</p>
-<h3>Derived from</h3>
-EricMainWindow
-<h3>Class Attributes</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Class Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-<h3>Methods</h3>
-
-<table>
-
-<tr>
-<td><a href="#UnittestWindow.__init__">UnittestWindow</a></td>
-<td>Constructor</td>
-</tr>
-<tr>
-<td><a href="#UnittestWindow.eventFilter">eventFilter</a></td>
-<td>Public method to filter events.</td>
-</tr>
-</table>
-<h3>Static Methods</h3>
-
-<table>
-<tr><td>None</td></tr>
-</table>
-
-<a NAME="UnittestWindow.__init__" ID="UnittestWindow.__init__"></a>
-<h4>UnittestWindow (Constructor)</h4>
-<b>UnittestWindow</b>(<i>prog=None, parent=None</i>)
-
-<p>
-        Constructor
-</p>
-<dl>
-
-<dt><i>prog</i></dt>
-<dd>
-filename of the program to open
-</dd>
-<dt><i>parent</i></dt>
-<dd>
-reference to the parent widget (QWidget)
-</dd>
-</dl>
-<a NAME="UnittestWindow.eventFilter" ID="UnittestWindow.eventFilter"></a>
-<h4>UnittestWindow.eventFilter</h4>
-<b>eventFilter</b>(<i>obj, event</i>)
-
-<p>
-        Public method to filter events.
-</p>
-<dl>
-
-<dt><i>obj</i></dt>
-<dd>
-reference to the object the event is meant for (QObject)
-</dd>
-<dt><i>event</i></dt>
-<dd>
-reference to the event object (QEvent)
-</dd>
-</dl>
-<dl>
-<dt>Return:</dt>
-<dd>
-flag indicating, whether the event was handled (boolean)
-</dd>
-</dl>
-<div align="right"><a href="#top">Up</a></div>
-<hr />
-<hr />
-<a NAME="clearSavedHistories" ID="clearSavedHistories"></a>
-<h2>clearSavedHistories</h2>
-<b>clearSavedHistories</b>(<i>self</i>)
-
-<p>
-    Function to clear the saved history lists.
-</p>
-<div align="right"><a href="#top">Up</a></div>
-<hr />
-</body></html>
\ No newline at end of file
--- a/eric7/Documentation/Source/index-eric7.PyUnit.html	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<html><head>
-<title>eric7.PyUnit</title>
-<meta charset="UTF-8">
-<link rel="stylesheet" href="styles.css">
-</head>
-<body>
-<h1>eric7.PyUnit</h1>
-
-<p>
-Package implementing an interface to the pyunit unittest package.
-</p>
-<p>
-The package consist of a single dialog, which may be called as a
-standalone version using the eric7_unittest script or from within the eric
-IDE. If it is called from within eric, it has the additional function to
-open a source file that failed a test.
-</p>
-
-
-<h3>Modules</h3>
-<table>
-
-<tr>
-<td><a href="eric7.PyUnit.UnittestDialog.html">UnittestDialog</a></td>
-<td>Module implementing the UI to the pyunit package.</td>
-</tr>
-</table>
-</body></html>
\ No newline at end of file
--- a/eric7/EricNetwork/EricJsonClient.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/EricNetwork/EricJsonClient.py	Thu May 19 14:40:15 2022 +0200
@@ -24,7 +24,7 @@
         """
         Constructor
         
-        @param host ip address the background service is listening
+        @param host IP address the background service is listening
         @type str
         @param port port of the background service
         @type int
--- a/eric7/EricNetwork/EricJsonServer.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/EricNetwork/EricJsonServer.py	Thu May 19 14:40:15 2022 +0200
@@ -60,10 +60,9 @@
 
         self.newConnection.connect(self.handleNewConnection)
         
-        port = self.serverPort()
         ## Note: Need the port if client is started external in debugger.
         print('JSON server ({1}) listening on: {0:d}'   # __IGNORE_WARNING__
-              .format(port, self.__name))
+              .format(self.serverPort(), self.__name))
     
     @pyqtSlot()
     def handleNewConnection(self):
@@ -131,7 +130,7 @@
         """
         Private slot handling received data from the client.
         
-        @param idString id of the connection been disconnected
+        @param idString id of the connection
         @type str
         """
         if idString:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/EricNetwork/EricJsonStreamReader.py	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,142 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a JSON based reader class.
+"""
+
+import json
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot
+from PyQt6.QtNetwork import QTcpServer, QHostAddress
+
+from EricWidgets import EricMessageBox
+
+import Preferences
+import Utilities
+
+
+class EricJsonReader(QTcpServer):
+    """
+    Class implementing a JSON based reader class.
+    
+    The reader is responsible for opening a socket to listen for writer
+    connections.
+    
+    @signal dataReceived(object) emitted after a data object was received
+    """
+    dataReceived = pyqtSignal(object)
+    
+    def __init__(self, name="", ip=None, parent=None):
+        """
+        Constructor
+        
+        @param name name of the server (used for output only)
+        @type str
+        @param ip IP address to listen at
+        @type str
+        @param parent parent object
+        @type QObject
+        """
+        super().__init__(parent)
+        
+        self.__name = name
+        self.__connection = None
+        
+        # setup the network interface
+        if ip is None:
+            networkInterface = Preferences.getDebugger("NetworkInterface")
+            if networkInterface == "all" or '.' in networkInterface:
+                # IPv4
+                self.__hostAddress = '127.0.0.1'
+            else:
+                # IPv6
+                self.__hostAddress = '::1'
+        else:
+            self.__hostAddress = ip
+        self.listen(QHostAddress(self.__hostAddress))
+
+        self.newConnection.connect(self.handleNewConnection)
+        
+        ## Note: Need the port if writer is started external in debugger.
+        print('JSON reader ({1}) listening on: {0:d}'   # __IGNORE_WARNING__
+              .format(self.serverPort(), self.__name))
+    
+    def address(self):
+        """
+        Public method to get the host address.
+        
+        @return host address
+        @rtype str
+        """
+        return self.__hostAddress
+    
+    def port(self):
+        """
+        Public method to get the port number to connect to.
+        
+        @return port number
+        @rtype int
+        """
+        return self.serverPort()
+    
+    @pyqtSlot()
+    def handleNewConnection(self):
+        """
+        Public slot for new incoming connections from a writer.
+        """
+        connection = self.nextPendingConnection()
+        if not connection.isValid():
+            return
+        
+        if self.__connection is not None:
+            self.__connection.close()
+        
+        self.__connection = connection
+        
+        connection.readyRead.connect(self.__receiveJson)
+        connection.disconnected.connect(self.__handleDisconnect)
+    
+    @pyqtSlot()
+    def __handleDisconnect(self):
+        """
+        Private slot handling a disconnect of the writer.
+        """
+        if self.__connection is not None:
+            self.__connection.close()
+        
+        self.__connection = None
+    
+    @pyqtSlot()
+    def __receiveJson(self):
+        """
+        Private slot handling received data from the writer.
+        """
+        connection = self.__connection
+        
+        while connection and connection.canReadLine():
+            dataStr = connection.readLine()
+            jsonLine = bytes(dataStr).decode("utf-8", 'backslashreplace')
+            
+            #- print("JSON Reader ({0}): {1}".format(self.__name, jsonLine))
+            #- this is for debugging only
+            
+            try:
+                data = json.loads(jsonLine.strip())
+            except (TypeError, ValueError) as err:
+                EricMessageBox.critical(
+                    None,
+                    self.tr("JSON Protocol Error"),
+                    self.tr("""<p>The data received from the writer"""
+                            """ could not be decoded. Please report"""
+                            """ this issue with the received data to the"""
+                            """ eric bugs email address.</p>"""
+                            """<p>Error: {0}</p>"""
+                            """<p>Data:<br/>{1}</p>""").format(
+                        str(err), Utilities.html_encode(jsonLine.strip())),
+                    EricMessageBox.Ok)
+                return
+            
+            self.dataReceived.emit(data)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/EricNetwork/EricJsonStreamWriter.py	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a JSON based writer class.
+"""
+
+import json
+import socket
+
+
+class EricJsonWriter:
+    """
+    Class implementing a JSON based writer class.
+    """
+    def __init__(self, host, port):
+        """
+        Constructor
+        
+        @param host IP address the reader is listening on
+        @type str
+        @param port port the reader is listening on
+        @type int
+        """
+        self.__connection = socket.create_connection((host, port))
+    
+    def write(self, data):
+        """
+        Public method to send JSON serializable data.
+        
+        @param data JSON serializable object to be sent
+        @type object
+        """
+        dataStr = json.dumps(data) + '\n'
+        self.__connection.sendall(dataStr.encode('utf8', 'backslashreplace'))
+    
+    def close(self):
+        """
+        Public method to close the stream.
+        """
+        self.__connection.close()
--- a/eric7/Globals/__init__.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Globals/__init__.py	Thu May 19 14:40:15 2022 +0200
@@ -35,9 +35,11 @@
 recentNameHosts = "Hosts"
 recentNameBreakpointFiles = "BreakPointFiles"
 recentNameBreakpointConditions = "BreakPointConditions"
-recentNameUnittestDiscoverHistory = "UTDiscoverHistory"
-recentNameUnittestFileHistory = "UTFileHistory"
-recentNameUnittestTestnameHistory = "UTTestnameHistory"
+recentNameTestDiscoverHistory = "UTDiscoverHistory"
+recentNameTestFileHistory = "UTFileHistory"
+recentNameTestNameHistory = "UTTestnameHistory"
+recentNameTestFramework = "UTTestFramework"
+recentNameTestEnvironment = "UTEnvironmentName"
 
 configDir = None
 
--- a/eric7/JediInterface/JediServer.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/JediInterface/JediServer.py	Thu May 19 14:40:15 2022 +0200
@@ -854,25 +854,11 @@
         if "PATH" in clientEnv:
             clientEnv["PATH"] = self.__ui.getOriginalPathString()
         
-        if (projectLanguage.startswith("Python") or
-                projectLanguage == "MicroPython"):
-            # new code using virtual environments
-            venvManager = ericApp().getObject("VirtualEnvManager")
-            
-            # get virtual environment from project first
-            venvName = self.__ericProject.getDebugProperty("VIRTUALENV")
-            if not venvName:
-                # get it from debugger settings next
-                if projectLanguage in ("Python3", "MicroPython", "Cython"):
-                    venvName = Preferences.getDebugger("Python3VirtualEnv")
-                    if not venvName:
-                        venvName, _ = venvManager.getDefaultEnvironment()
-                else:
-                    venvName = ""
-            if venvName:
-                interpreter = venvManager.getVirtualenvInterpreter(
-                    venvName)
-                execPath = venvManager.getVirtualenvExecPath(venvName)
+        if projectLanguage in ("Python3", "MicroPython", "Cython"):
+            interpreter = self.__ericProject.getProjectInterpreter(
+                resolveGlobal=False)
+            if interpreter:
+                execPath = self.__ericProject.getProjectExecPath()
                 
                 # build a suitable environment
                 if execPath:
--- a/eric7/PipInterface/Pip.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/PipInterface/Pip.py	Thu May 19 14:40:15 2022 +0200
@@ -203,16 +203,9 @@
         @return interpreter path
         @rtype str
         """
-        if venvName == self.getProjectEnvironmentString():
-            venvName = (
-                ericApp().getObject("Project")
-                .getDebugProperty("VIRTUALENV")
-            )
-            if not venvName:
-                # fall back to interpreter used to run eric7
-                return Globals.getPythonExecutable()
-        
         interpreter = (
+            ericApp().getObject("Project").getProjectInterpreter()
+            if venvName == self.getProjectEnvironmentString() else
             ericApp().getObject("VirtualEnvManager")
             .getVirtualenvInterpreter(venvName)
         )
--- a/eric7/Project/CreateDialogCodeDialog.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Project/CreateDialogCodeDialog.py	Thu May 19 14:40:15 2022 +0200
@@ -161,7 +161,7 @@
         venvManager = ericApp().getObject("VirtualEnvManager")
         projectType = self.project.getProjectType()
         
-        venvName = self.project.getDebugProperty("VIRTUALENV")
+        venvName = self.project.getProjectVenv(resolveDebugger=False)
         if not venvName:
             # no project specific environment, try a type specific one
             if projectType in ("PyQt5", "PySide2"):
--- a/eric7/Project/Project.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Project/Project.py	Thu May 19 14:40:15 2022 +0200
@@ -39,6 +39,7 @@
 
 from EricGui.EricAction import EricAction, createActionGroup
 
+import Globals
 import Preferences
 import Utilities
 
@@ -524,6 +525,7 @@
             },
             "EOL": -1,
             "DOCSTRING": "",
+            "TESTING_FRAMEWORK": "",
         }
         
         self.__initDebugProperties()
@@ -3361,6 +3363,9 @@
         """
         Public method to return the main script filename.
         
+        The normalized name is the name of the main script prepended with
+        the project path.
+        
         @param normalized flag indicating a normalized filename is wanted
             (boolean)
         @return filename of the projects main script (string)
@@ -3704,6 +3709,79 @@
         """
         return self.pdata["DESCRIPTION"]
     
+    def getProjectVenv(self, resolveDebugger=True):
+        """
+        Public method to get the name of the virtual environment used by the
+        project.
+        
+        @param resolveDebugger flag indicating to resolve the virtual
+            environment name via the debugger settings if none was configured
+        @type bool
+        @return name of the project's virtual environment
+        @rtype str
+        """
+        venvName = self.getDebugProperty("VIRTUALENV")
+        if (
+            not venvName and
+            resolveDebugger and
+            self.getProjectLanguage() in ("Python3", "MicroPython", "Cython")
+        ):
+            venvName = Preferences.getDebugger("Python3VirtualEnv")
+        
+        return venvName
+    
+    def getProjectInterpreter(self, resolveGlobal=True):
+        """
+        Public method to get the path of the interpreter used by the project.
+        
+        @param resolveGlobal flag indicating to resolve the interpreter using
+            the global interpreter if no project of debugger specific
+            environment was configured
+        @type bool
+        @return path of the project's interpreter
+        @rtype str
+        """
+        interpreter = ""
+        venvName = self.getProjectVenv()
+        if venvName:
+            interpreter = (
+                ericApp().getObject("VirtualEnvManager")
+                .getVirtualenvInterpreter(venvName)
+            )
+        if not interpreter and resolveGlobal:
+            interpreter = Globals.getPythonExecutable()
+        
+        return interpreter
+    
+    def getProjectExecPath(self):
+        """
+        Public method to get the executable search path prefix of the project.
+        
+        @return executable search path prefix
+        @rtype str
+        """
+        execPath = ""
+        venvName = self.getProjectVenv()
+        if venvName:
+            execPath = (
+                ericApp().getObject("VirtualEnvManager")
+                .getVirtualenvExecPath(venvName)
+            )
+        
+        return execPath
+    
+    def getProjectTestingFramework(self):
+        """
+        Public method to get the testing framework name of the project.
+        
+        @return testing framework name of the project
+        @rtype str
+        """
+        try:
+            return self.pdata["TESTING_FRAMEWORK"]
+        except KeyError:
+            return ""
+    
     def __isInPdata(self, fn):
         """
         Private method used to check, if the passed in filename is project
@@ -4990,18 +5068,13 @@
                     " current project. Aborting"))
             return
         
-        tfn = Utilities.getTestFileName(fn)
-        basename = os.path.splitext(fn)[0]
-        tbasename = os.path.splitext(tfn)[0]
-        
         # determine name of coverage file to be used
         files = []
-        f = "{0}.coverage".format(basename)
-        tf = "{0}.coverage".format(tbasename)
-        if os.path.isfile(f):
-            files.append(f)
-        if os.path.isfile(tf):
-            files.append(tf)
+        for filename in [fn] + Utilities.getTestFileNames(fn):
+            basename = os.path.splitext(filename)[0]
+            f = "{0}.coverage".format(basename)
+            if os.path.isfile(f):
+                files.append(f)
         
         if files:
             if len(files) > 1:
@@ -5040,18 +5113,13 @@
                     " current project. Aborting"))
             return
         
-        tfn = Utilities.getTestFileName(fn)
-        basename = os.path.splitext(fn)[0]
-        tbasename = os.path.splitext(tfn)[0]
-        
         # determine name of profile file to be used
         files = []
-        f = "{0}.profile".format(basename)
-        tf = "{0}.profile".format(tbasename)
-        if os.path.isfile(f):
-            files.append(f)
-        if os.path.isfile(tf):
-            files.append(tf)
+        for filename in [fn] + Utilities.getTestFileNames(fn):
+            basename = os.path.splitext(filename)[0]
+            f = "{0}.profile".format(basename)
+            if os.path.isfile(f):
+                files.append(f)
         
         if files:
             if len(files) > 1:
@@ -5079,16 +5147,18 @@
         """
         fn = self.getMainScript(True)
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            self.codeProfileAct.setEnabled(
-                os.path.isfile("{0}.profile".format(basename)) or
-                os.path.isfile("{0}.profile".format(tbasename)))
+            filenames = [os.path.splitext(f)[0]
+                         for f in [fn] + Utilities.getTestFileNames(fn)]
+            self.codeProfileAct.setEnabled(any([
+                os.path.isfile("{0}.profile".format(f))
+                for f in filenames
+            ]))
             self.codeCoverageAct.setEnabled(
-                self.isPy3Project() and
-                (os.path.isfile("{0}.coverage".format(basename)) or
-                 os.path.isfile("{0}.coverage".format(tbasename))))
+                self.isPy3Project() and any([
+                    os.path.isfile("{0}.coverage".format(f))
+                    for f in filenames
+                ])
+            )
         else:
             self.codeProfileAct.setEnabled(False)
             self.codeCoverageAct.setEnabled(False)
--- a/eric7/Project/ProjectSourcesBrowser.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Project/ProjectSourcesBrowser.py	Thu May 19 14:40:15 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))
@@ -755,19 +755,18 @@
         # a project coverage file
         fn = self.project.getMainScript(True)
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            prEnable = (
-                prEnable or
-                os.path.isfile("{0}.profile".format(basename)) or
-                os.path.isfile("{0}.profile".format(tbasename))
-            )
+            filenames = [os.path.splitext(f)[0]
+                         for f in [fn] + Utilities.getTestFileNames(fn)]
+            prEnable = any([
+                os.path.isfile("{0}.profile".format(f))
+                for f in filenames
+            ])
             coEnable = (
-                (coEnable or
-                 os.path.isfile("{0}.coverage".format(basename)) or
-                 os.path.isfile("{0}.coverage".format(tbasename))) and
-                self.project.isPy3Project()
+                self.project.isPy3Project() and
+                any([
+                    os.path.isfile("{0}.coverage".format(f))
+                    for f in filenames
+                ])
             )
         
         # now check the selected item
@@ -989,28 +988,18 @@
         files = []
         
         if pfn is not None:
-            tpfn = Utilities.getTestFileName(pfn)
-            basename = os.path.splitext(pfn)[0]
-            tbasename = os.path.splitext(tpfn)[0]
-            
-            f = "{0}.coverage".format(basename)
-            tf = "{0}.coverage".format(tbasename)
-            if os.path.isfile(f):
-                files.append(f)
-            if os.path.isfile(tf):
-                files.append(tf)
+            for filename in [pfn] + Utilities.getTestFileNames(pfn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.coverage".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
         
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            
-            f = "{0}.coverage".format(basename)
-            tf = "{0}.coverage".format(tbasename)
-            if os.path.isfile(f) and f not in files:
-                files.append(f)
-            if os.path.isfile(tf) and tf not in files:
-                files.append(tf)
+            for filename in [fn] + Utilities.getTestFileNames(fn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.coverage".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
         
         if files:
             if len(files) > 1:
@@ -1043,28 +1032,18 @@
         files = []
         
         if pfn is not None:
-            tpfn = Utilities.getTestFileName(pfn)
-            basename = os.path.splitext(pfn)[0]
-            tbasename = os.path.splitext(tpfn)[0]
-            
-            f = "{0}.profile".format(basename)
-            tf = "{0}.profile".format(tbasename)
-            if os.path.isfile(f):
-                files.append(f)
-            if os.path.isfile(tf):
-                files.append(tf)
+            for filename in [pfn] + Utilities.getTestFileNames(pfn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.profile".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
         
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            
-            f = "{0}.profile".format(basename)
-            tf = "{0}.profile".format(tbasename)
-            if os.path.isfile(f) and f not in files:
-                files.append(f)
-            if os.path.isfile(tf) and tf not in files:
-                files.append(tf)
+            for filename in [fn] + Utilities.getTestFileNames(fn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.profile".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
                 
         if files:
             if len(files) > 1:
--- a/eric7/Project/PropertiesDialog.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Project/PropertiesDialog.py	Thu May 19 14:40:15 2022 +0200
@@ -7,6 +7,7 @@
 Module implementing the project properties dialog.
 """
 
+import contextlib
 import os
 
 from PyQt6.QtCore import QDir, pyqtSlot
@@ -19,6 +20,8 @@
 
 from QScintilla.DocstringGenerator import getSupportedDocstringTypes
 
+from Testing.Interfaces import FrameworkNames
+
 import Utilities
 import Preferences
 import UI.PixmapCache
@@ -52,6 +55,10 @@
             getSupportedDocstringTypes()
         ):
             self.docstringStyleComboBox.addItem(docstringStyle, docstringType)
+##        
+##        self.testingFrameworkComboBox.addItem(self.tr("None"), "")
+##        for framework in sorted(FrameworkNames):
+##            self.testingFrameworkComboBox.addItem(framework, framework)
         
         self.project = project
         self.newProject = new
@@ -133,6 +140,10 @@
             cindex = self.docstringStyleComboBox.findData(
                 self.project.pdata["DOCSTRING"])
             self.docstringStyleComboBox.setCurrentIndex(cindex)
+            with contextlib.suppress(KeyError):
+                cindex = self.testingFrameworkComboBox.findData(
+                    self.project.pdata["TESTING_FRAMEWORK"])
+                self.testingFrameworkComboBox.setCurrentIndex(cindex)
         else:
             self.languageComboBox.setCurrentIndex(
                 self.languageComboBox.findText("Python3"))
@@ -149,29 +160,36 @@
             bool(self.dirPicker.text()) and
             self.dirPicker.text() not in self.__initPaths)
     
-    @pyqtSlot(int)
-    def on_languageComboBox_currentIndexChanged(self, index):
+    @pyqtSlot(str)
+    def on_languageComboBox_currentTextChanged(self, language):
         """
         Private slot handling the selection of a programming language.
         
-        @param index index of the current item
-        @type int
+        @param language text of the current item
+        @type str
         """
-        language = self.languageComboBox.itemText(index)
         curProjectType = self.getProjectType()
         
-        projectTypes = []
-        for projectTypeItem in self.project.getProjectTypes(language).items():
-            projectTypes.append((projectTypeItem[1], projectTypeItem[0]))
         self.projectTypeComboBox.clear()
-        for projectType in sorted(projectTypes):
+        for projectType in sorted(
+            self.project.getProjectTypes(language).items(),
+            key=lambda k: k[1]
+        ):
             self.projectTypeComboBox.addItem(
-                projectType[0], projectType[1])
+                projectType[1], projectType[0])
         
         index = self.projectTypeComboBox.findData(curProjectType)
         if index == -1:
             index = 0
         self.projectTypeComboBox.setCurrentIndex(index)
+        
+        curTestingFramework = self.testingFrameworkComboBox.currentText()
+        self.testingFrameworkComboBox.clear()
+        self.testingFrameworkComboBox.addItem(self.tr("None"), "")
+        with contextlib.suppress(KeyError):
+            for framework in sorted(FrameworkNames[language]):
+                self.testingFrameworkComboBox.addItem(framework, framework)
+        self.testingFrameworkComboBox.setCurrentText(curTestingFramework)
     
     @pyqtSlot(str)
     def on_dirPicker_textChanged(self, txt):
@@ -335,3 +353,7 @@
         self.project.pdata["DOCSTRING"] = (
             self.docstringStyleComboBox.currentData()
         )
+        
+        self.project.pdata["TESTING_FRAMEWORK"] = (
+            self.testingFrameworkComboBox.currentData()
+        )
--- a/eric7/Project/PropertiesDialog.ui	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/Project/PropertiesDialog.ui	Thu May 19 14:40:15 2022 +0200
@@ -51,87 +51,37 @@
        </property>
       </widget>
      </item>
-     <item row="1" column="1">
-      <widget class="QPushButton" name="spellPropertiesButton">
-       <property name="toolTip">
-        <string>Press to edit the spell checking properties</string>
-       </property>
-       <property name="text">
-        <string>Spell Checking Properties...</string>
-       </property>
-      </widget>
-     </item>
-     <item row="2" column="0">
+     <item row="1" column="0">
       <widget class="QLabel" name="textLabel1">
        <property name="text">
-        <string>&amp;Progr. Language:</string>
+        <string>&amp;Programming Language:</string>
        </property>
        <property name="buddy">
         <cstring>languageComboBox</cstring>
        </property>
       </widget>
      </item>
-     <item row="2" column="1">
+     <item row="1" column="1">
       <widget class="QComboBox" name="languageComboBox">
        <property name="toolTip">
         <string>Select the project's programming language</string>
        </property>
       </widget>
      </item>
-     <item row="3" column="1">
-      <layout class="QHBoxLayout" name="horizontalLayout">
-       <item>
-        <widget class="QCheckBox" name="mixedLanguageCheckBox">
-         <property name="toolTip">
-          <string>Select, if the project uses other programming languages as well</string>
-         </property>
-         <property name="text">
-          <string>Mi&amp;xed programming languages</string>
-         </property>
-         <property name="shortcut">
-          <string>Alt+X</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>
-       <item>
-        <widget class="QCheckBox" name="makeCheckBox">
-         <property name="toolTip">
-          <string>Select to activate the 'make' support</string>
-         </property>
-         <property name="text">
-          <string>Enable 'make' Support</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QToolButton" name="makeButton">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <property name="toolTip">
-          <string>Press to open a dialog to enter the 'make' parameters</string>
-         </property>
-         <property name="text">
-          <string/>
-         </property>
-        </widget>
-       </item>
-      </layout>
+     <item row="2" column="1">
+      <widget class="QCheckBox" name="mixedLanguageCheckBox">
+       <property name="toolTip">
+        <string>Select, if the project uses other programming languages as well</string>
+       </property>
+       <property name="text">
+        <string>Mi&amp;xed programming languages</string>
+       </property>
+       <property name="shortcut">
+        <string>Alt+X</string>
+       </property>
+      </widget>
      </item>
-     <item row="4" column="0">
+     <item row="3" column="0">
       <widget class="QLabel" name="textLabel1_2">
        <property name="text">
         <string>Project &amp;Type:</string>
@@ -141,14 +91,14 @@
        </property>
       </widget>
      </item>
-     <item row="4" column="1">
+     <item row="3" column="1">
       <widget class="QComboBox" name="projectTypeComboBox">
        <property name="toolTip">
         <string>Select the type of the project</string>
        </property>
       </widget>
      </item>
-     <item row="5" column="0">
+     <item row="4" column="0">
       <widget class="QLabel" name="dirLabel">
        <property name="text">
         <string>Project &amp;Directory:</string>
@@ -158,7 +108,7 @@
        </property>
       </widget>
      </item>
-     <item row="5" column="1">
+     <item row="4" column="1">
       <widget class="EricPathPicker" name="dirPicker" native="true">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -179,7 +129,7 @@
        </property>
       </widget>
      </item>
-     <item row="6" column="0">
+     <item row="5" column="0">
       <widget class="QLabel" name="versionLabel">
        <property name="text">
         <string>&amp;Version No.:</string>
@@ -189,7 +139,7 @@
        </property>
       </widget>
      </item>
-     <item row="6" column="1">
+     <item row="5" column="1">
       <widget class="QLineEdit" name="versionEdit">
        <property name="toolTip">
         <string>Enter the version number</string>
@@ -200,7 +150,7 @@
        </property>
       </widget>
      </item>
-     <item row="7" column="0">
+     <item row="6" column="0">
       <widget class="QLabel" name="mainscriptLabel">
        <property name="text">
         <string>&amp;Main Script:</string>
@@ -210,7 +160,7 @@
        </property>
       </widget>
      </item>
-     <item row="7" column="1">
+     <item row="6" column="1">
       <widget class="EricPathPicker" name="mainscriptPicker" native="true">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -231,17 +181,7 @@
        </property>
       </widget>
      </item>
-     <item row="8" column="1">
-      <widget class="QPushButton" name="transPropertiesButton">
-       <property name="toolTip">
-        <string>Press to edit the translations properties</string>
-       </property>
-       <property name="text">
-        <string>Translations Properties...</string>
-       </property>
-      </widget>
-     </item>
-     <item row="9" column="0">
+     <item row="7" column="0">
       <widget class="QLabel" name="eolLabel">
        <property name="text">
         <string>End of &amp;Line Character:</string>
@@ -251,7 +191,7 @@
        </property>
       </widget>
      </item>
-     <item row="9" column="1">
+     <item row="7" column="1">
       <widget class="QComboBox" name="eolComboBox">
        <property name="toolTip">
         <string>Select the end of line character to be used by the project</string>
@@ -278,21 +218,21 @@
        </item>
       </widget>
      </item>
-     <item row="10" column="0">
+     <item row="8" column="0">
       <widget class="QLabel" name="label">
        <property name="text">
         <string>Docstring Style:</string>
        </property>
       </widget>
      </item>
-     <item row="10" column="1">
+     <item row="8" column="1">
       <widget class="QComboBox" name="docstringStyleComboBox">
        <property name="toolTip">
         <string>Select the docstring style for the project</string>
        </property>
       </widget>
      </item>
-     <item row="11" column="0">
+     <item row="9" column="0">
       <widget class="QLabel" name="authorLabel">
        <property name="text">
         <string>&amp;Author:</string>
@@ -302,7 +242,7 @@
        </property>
       </widget>
      </item>
-     <item row="11" column="1">
+     <item row="9" column="1">
       <widget class="QLineEdit" name="authorEdit">
        <property name="toolTip">
         <string>Enter authors name</string>
@@ -313,7 +253,7 @@
        </property>
       </widget>
      </item>
-     <item row="12" column="0">
+     <item row="10" column="0">
       <widget class="QLabel" name="emailLabel">
        <property name="text">
         <string>&amp;Email:</string>
@@ -323,7 +263,7 @@
        </property>
       </widget>
      </item>
-     <item row="12" column="1">
+     <item row="10" column="1">
       <widget class="QLineEdit" name="emailEdit">
        <property name="toolTip">
         <string>Enter authors email</string>
@@ -334,7 +274,7 @@
        </property>
       </widget>
      </item>
-     <item row="13" column="0">
+     <item row="11" column="0">
       <widget class="QLabel" name="descriptionLabel">
        <property name="text">
         <string>&amp;Description:</string>
@@ -347,7 +287,7 @@
        </property>
       </widget>
      </item>
-     <item row="13" column="1">
+     <item row="11" column="1">
       <widget class="EricSpellCheckedTextEdit" name="descriptionEdit">
        <property name="toolTip">
         <string>Enter description</string>
@@ -364,6 +304,110 @@
        </property>
       </widget>
      </item>
+     <item row="12" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Testing Framework:</string>
+       </property>
+      </widget>
+     </item>
+     <item row="12" column="1">
+      <widget class="QComboBox" name="testingFrameworkComboBox">
+       <property name="toolTip">
+        <string>Select the testing framework used by the project</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <spacer name="horizontalSpacer_3">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="spellPropertiesButton">
+       <property name="toolTip">
+        <string>Press to edit the spell checking properties</string>
+       </property>
+       <property name="text">
+        <string>Spell Checking Properties...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="transPropertiesButton">
+       <property name="toolTip">
+        <string>Press to edit the translations properties</string>
+       </property>
+       <property name="text">
+        <string>Translations Properties...</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_4">
+       <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>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QCheckBox" name="makeCheckBox">
+       <property name="toolTip">
+        <string>Select to activate the 'make' support</string>
+       </property>
+       <property name="text">
+        <string>Enable 'make' Support</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="makeButton">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
+       <property name="toolTip">
+        <string>Press to open a dialog to enter the 'make' parameters</string>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_6">
+       <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>
    <item>
@@ -434,21 +478,22 @@
  </customwidgets>
  <tabstops>
   <tabstop>nameEdit</tabstop>
-  <tabstop>spellPropertiesButton</tabstop>
   <tabstop>languageComboBox</tabstop>
   <tabstop>mixedLanguageCheckBox</tabstop>
-  <tabstop>makeCheckBox</tabstop>
-  <tabstop>makeButton</tabstop>
   <tabstop>projectTypeComboBox</tabstop>
   <tabstop>dirPicker</tabstop>
   <tabstop>versionEdit</tabstop>
   <tabstop>mainscriptPicker</tabstop>
-  <tabstop>transPropertiesButton</tabstop>
   <tabstop>eolComboBox</tabstop>
   <tabstop>docstringStyleComboBox</tabstop>
   <tabstop>authorEdit</tabstop>
   <tabstop>emailEdit</tabstop>
   <tabstop>descriptionEdit</tabstop>
+  <tabstop>testingFrameworkComboBox</tabstop>
+  <tabstop>spellPropertiesButton</tabstop>
+  <tabstop>transPropertiesButton</tabstop>
+  <tabstop>makeCheckBox</tabstop>
+  <tabstop>makeButton</tabstop>
   <tabstop>vcsCheckBox</tabstop>
   <tabstop>vcsInfoButton</tabstop>
  </tabstops>
--- a/eric7/PyUnit/UnittestDialog.py	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1506 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Module implementing the UI to the pyunit package.
-"""
-
-import unittest
-import sys
-import time
-import re
-import os
-import contextlib
-
-from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QFileInfo
-from PyQt6.QtGui import QColor
-from PyQt6.QtWidgets import (
-    QWidget, QDialog, QApplication, QDialogButtonBox, QListWidgetItem,
-    QComboBox, QTreeWidgetItem
-)
-
-from EricWidgets.EricApplication import ericApp
-from EricWidgets import EricMessageBox
-from EricWidgets.EricMainWindow import EricMainWindow
-from EricWidgets.EricPathPicker import EricPathPickerModes
-
-from .Ui_UnittestDialog import Ui_UnittestDialog
-
-import UI.PixmapCache
-
-import Preferences
-
-from Globals import (
-    recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory,
-    recentNameUnittestTestnameHistory
-)
-
-
-class UnittestDialog(QWidget, Ui_UnittestDialog):
-    """
-    Class implementing the UI to the pyunit package.
-    
-    @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()
-    
-    TestCaseNameRole = Qt.ItemDataRole.UserRole
-    TestCaseFileRole = Qt.ItemDataRole.UserRole + 1
-    
-    ErrorsInfoRole = Qt.ItemDataRole.UserRole
-    
-    SkippedColorDarkTheme = QColor("#00aaff")
-    FailedExpectedColorDarkTheme = QColor("#ccaaff")
-    SucceededUnexpectedColorDarkTheme = QColor("#ff99dd")
-    SkippedColorLightTheme = QColor("#0000ff")
-    FailedExpectedColorLightTheme = QColor("#7700bb")
-    SucceededUnexpectedColorLightTheme = QColor("#ff0000")
-    
-    def __init__(self, prog=None, dbs=None, ui=None, parent=None, name=None):
-        """
-        Constructor
-        
-        @param prog filename of the program to open
-        @type str
-        @param dbs reference to the debug server object. It is an indication
-            whether we were called from within the eric IDE.
-        @type DebugServer
-        @param ui reference to the UI object
-        @type UserInterface
-        @param parent parent widget of this dialog
-        @type QWidget
-        @param name name of this dialog
-        @type str
-        """
-        super().__init__(parent)
-        if name:
-            self.setObjectName(name)
-        self.setupUi(self)
-        
-        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)
-        
-        self.discoverButton = self.buttonBox.addButton(
-            self.tr("Discover"), QDialogButtonBox.ButtonRole.ActionRole)
-        self.discoverButton.setToolTip(self.tr(
-            "Discover tests"))
-        self.discoverButton.setWhatsThis(self.tr(
-            """<b>Discover</b>"""
-            """<p>This button starts a discovery of available tests.</p>"""))
-        self.startButton = self.buttonBox.addButton(
-            self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
-        
-        self.startButton.setToolTip(self.tr(
-            "Start the selected testsuite"))
-        self.startButton.setWhatsThis(self.tr(
-            """<b>Start Test</b>"""
-            """<p>This button starts the selected testsuite.</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 selected"""
-            """ testsuite.</p>"""))
-        
-        self.stopButton = self.buttonBox.addButton(
-            self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
-        self.stopButton.setToolTip(self.tr("Stop the running unittest"))
-        self.stopButton.setWhatsThis(self.tr(
-            """<b>Stop Test</b>"""
-            """<p>This button stops a running unittest.</p>"""))
-        
-        self.discoverButton.setEnabled(False)
-        self.stopButton.setEnabled(False)
-        self.startButton.setDefault(True)
-        self.startFailedButton.setEnabled(False)
-        
-        self.__dbs = dbs
-        self.__forProject = False
-        
-        self.setWindowFlags(
-            self.windowFlags() |
-            Qt.WindowType.WindowContextHelpButtonHint
-        )
-        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
-        self.setWindowTitle(self.tr("Unittest"))
-        if dbs:
-            self.ui = ui
-            
-            self.debuggerCheckBox.setChecked(True)
-            
-            # virtual environment manager is only used in the integrated
-            # variant
-            self.__venvManager = ericApp().getObject("VirtualEnvManager")
-            self.__populateVenvComboBox()
-            self.__venvManager.virtualEnvironmentAdded.connect(
-                self.__populateVenvComboBox)
-            self.__venvManager.virtualEnvironmentRemoved.connect(
-                self.__populateVenvComboBox)
-            self.__venvManager.virtualEnvironmentChanged.connect(
-                self.__populateVenvComboBox)
-        else:
-            self.__venvManager = None
-            self.debuggerCheckBox.setVisible(False)
-        self.venvComboBox.setVisible(bool(self.__venvManager))
-        self.venvLabel.setVisible(bool(self.__venvManager))
-        
-        self.__setProgressColor("green")
-        self.progressLed.setDarkFactor(150)
-        self.progressLed.off()
-        
-        self.discoverHistory = []
-        self.fileHistory = []
-        self.testNameHistory = []
-        self.running = False
-        self.savedModulelist = None
-        self.savedSysPath = sys.path
-        self.savedCwd = os.getcwd()
-        
-        self.rxPatterns = [
-            self.tr("^Failure: "),
-            self.tr("^Error: "),
-            # These are for untranslated/partially translated situations
-            "^Failure: ",
-            "^Error: ",
-        ]
-        
-        self.__failedTests = set()
-        
-        # now connect the debug server signals if called from the eric IDE
-        if self.__dbs:
-            self.__dbs.utDiscovered.connect(self.__UTDiscovered)
-            self.__dbs.utPrepared.connect(self.__UTPrepared)
-            self.__dbs.utFinished.connect(self.__setStoppedMode)
-            self.__dbs.utStartTest.connect(self.testStarted)
-            self.__dbs.utStopTest.connect(self.testFinished)
-            self.__dbs.utTestFailed.connect(self.testFailed)
-            self.__dbs.utTestErrored.connect(self.testErrored)
-            self.__dbs.utTestSkipped.connect(self.testSkipped)
-            self.__dbs.utTestFailedExpected.connect(self.testFailedExpected)
-            self.__dbs.utTestSucceededUnexpected.connect(
-                self.testSucceededUnexpected)
-        
-        self.__editors = []
-        
-        self.__loadRecent()
-        
-        self.insertProg(prog)
-        self.insertTestName("")
-        
-        self.clearHistoriesButton.clicked.connect(self.clearRecent)
-    
-    def keyPressEvent(self, evt):
-        """
-        Protected slot to handle key press events.
-        
-        @param evt key press event to handle (QKeyEvent)
-        """
-        if evt.key() == Qt.Key.Key_Escape and self.__dbs:
-            self.close()
-    
-    def __populateVenvComboBox(self):
-        """
-        Private method to (re-)populate the virtual environments selector.
-        """
-        currentText = self.venvComboBox.currentText()
-        self.venvComboBox.clear()
-        self.venvComboBox.addItem("")
-        self.venvComboBox.addItems(
-            sorted(self.__venvManager.getVirtualenvNames()))
-        index = self.venvComboBox.findText(currentText)
-        if index < 0:
-            index = 0
-        self.venvComboBox.setCurrentIndex(index)
-    
-    def __setProgressColor(self, color):
-        """
-        Private method to set the color of the progress color label.
-        
-        @param color colour to be shown (string)
-        """
-        self.progressLed.setColor(QColor(color))
-    
-    def setProjectMode(self, forProject):
-        """
-        Public method to set the project mode of the dialog.
-        
-        @param forProject flag indicating to run for the open project
-        @type bool
-        """
-        self.__forProject = forProject
-        if forProject:
-            project = ericApp().getObject("Project")
-            if project.isOpen():
-                self.insertDiscovery(project.getProjectPath())
-            else:
-                self.insertDiscovery("")
-        else:
-            self.insertDiscovery("")
-        
-        self.discoveryList.clear()
-        self.tabWidget.setCurrentIndex(0)
-    
-    def insertDiscovery(self, start):
-        """
-        Public slot to insert the discovery start directory into the
-        discoveryPicker object.
-        
-        @param start start directory name to be inserted
-        @type str
-        """
-        current = self.discoveryPicker.currentText()
-        
-        # prepend the given directory to the discovery picker
-        if start is None:
-            start = ""
-        if start in self.discoverHistory:
-            self.discoverHistory.remove(start)
-        self.discoverHistory.insert(0, start)
-        self.discoveryPicker.clear()
-        self.discoveryPicker.addItems(self.discoverHistory)
-        
-        if current:
-            self.discoveryPicker.setText(current)
-    
-    def insertProg(self, prog):
-        """
-        Public slot to insert the filename prog into the testsuitePicker
-        object.
-        
-        @param prog filename to be inserted (string)
-        """
-        current = self.testsuitePicker.currentText()
-        
-        # prepend the selected file to the testsuite picker
-        if prog is None:
-            prog = ""
-        if prog in self.fileHistory:
-            self.fileHistory.remove(prog)
-        self.fileHistory.insert(0, prog)
-        self.testsuitePicker.clear()
-        self.testsuitePicker.addItems(self.fileHistory)
-        
-        if current:
-            self.testsuitePicker.setText(current)
-    
-    def insertTestName(self, testName):
-        """
-        Public slot to insert a test name into the testComboBox object.
-        
-        @param testName name of the test to be inserted (string)
-        """
-        current = self.testComboBox.currentText()
-        
-        # prepend the selected file to the testsuite combobox
-        if testName is None:
-            testName = ""
-        if testName in self.testNameHistory:
-            self.testNameHistory.remove(testName)
-        self.testNameHistory.insert(0, testName)
-        self.testComboBox.clear()
-        self.testComboBox.addItems(self.testNameHistory)
-        
-        if current:
-            self.testComboBox.setCurrentText(current)
-    
-    @pyqtSlot()
-    def on_testsuitePicker_aboutToShowPathPickerDialog(self):
-        """
-        Private slot called before the test suite selection dialog is shown.
-        """
-        if self.__dbs:
-            py3Extensions = ' '.join(
-                ["*{0}".format(ext)
-                 for ext in self.__dbs.getExtensions('Python3')]
-            )
-            fileFilter = self.tr(
-                "Python3 Files ({0});;All Files (*)"
-            ).format(py3Extensions)
-        else:
-            fileFilter = self.tr("Python Files (*.py);;All Files (*)")
-        self.testsuitePicker.setFilters(fileFilter)
-        
-        defaultDirectory = Preferences.getMultiProject("Workspace")
-        if not defaultDirectory:
-            defaultDirectory = os.path.expanduser("~")
-        if self.__dbs:
-            project = ericApp().getObject("Project")
-            if self.__forProject and project.isOpen():
-                defaultDirectory = project.getProjectPath()
-        self.testsuitePicker.setDefaultDirectory(defaultDirectory)
-    
-    @pyqtSlot(str)
-    def on_testsuitePicker_pathSelected(self, suite):
-        """
-        Private slot called after a test suite has been selected.
-        
-        @param suite file name of the test suite
-        @type str
-        """
-        self.insertProg(suite)
-    
-    @pyqtSlot(str)
-    def on_testsuitePicker_editTextChanged(self, path):
-        """
-        Private slot handling changes of the test suite path.
-        
-        @param path path of the test suite file
-        @type str
-        """
-        self.startFailedButton.setEnabled(False)
-    
-    @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
-        """
-        self.discoverButton.setEnabled(checked)
-        self.discoveryList.clear()
-        
-        if not bool(self.discoveryPicker.currentText()):
-            if self.__forProject:
-                project = ericApp().getObject("Project")
-                if project.isOpen():
-                    self.insertDiscovery(project.getProjectPath())
-                    return
-            
-            self.insertDiscovery(Preferences.getMultiProject("Workspace"))
-    
-    def on_buttonBox_clicked(self, button):
-        """
-        Private slot called by a button of the button box clicked.
-        
-        @param button button that was clicked (QAbstractButton)
-        """
-        if button == self.discoverButton:
-            self.__discover()
-            self.__saveRecent()
-        elif button == self.startButton:
-            self.startTests()
-            self.__saveRecent()
-        elif button == self.stopButton:
-            self.__stopTests()
-        elif button == self.startFailedButton:
-            self.startTests(failedOnly=True)
-    
-    def __loadRecent(self):
-        """
-        Private method to load the most recently used lists.
-        """
-        Preferences.Prefs.rsettings.sync()
-        
-        # 1. discovery history
-        self.discoverHistory = []
-        rs = Preferences.Prefs.rsettings.value(
-            recentNameUnittestDiscoverHistory)
-        if rs is not None:
-            recent = [f
-                      for f in Preferences.toList(rs)
-                      if QFileInfo(f).exists()]
-            self.discoverHistory = recent[
-                :Preferences.getDebugger("RecentNumber")]
-        
-        # 2. 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 QFileInfo(f).exists()]
-            self.fileHistory = recent[
-                :Preferences.getDebugger("RecentNumber")]
-        
-        # 3. 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(
-            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 __discover(self):
-        """
-        Private slot to discover unit test but don't run them.
-        """
-        if self.running:
-            return
-        
-        self.discoveryList.clear()
-        
-        discoveryStart = self.discoveryPicker.currentText()
-        self.insertDiscovery(discoveryStart)
-        self.sbLabel.setText(self.tr("Discovering Tests"))
-        QApplication.processEvents()
-        
-        self.testName = self.tr("Unittest with auto-discovery")
-        if self.__dbs:
-            venvName = self.venvComboBox.currentText()
-            
-            # we are cooperating with the eric IDE
-            project = ericApp().getObject("Project")
-            if self.__forProject:
-                mainScript = project.getMainScript(True)
-                clientType = project.getProjectLanguage()
-                if mainScript:
-                    workdir = os.path.dirname(os.path.abspath(mainScript))
-                else:
-                    workdir = project.getProjectPath()
-                sysPath = [workdir]
-                if not discoveryStart:
-                    discoveryStart = workdir
-            else:
-                if not discoveryStart:
-                    EricMessageBox.critical(
-                        self,
-                        self.tr("Unittest"),
-                        self.tr("You must enter a start directory for"
-                                " auto-discovery."))
-                    return
-                
-                workdir = ""
-                clientType = "Python3"
-                sysPath = []
-            self.__dbs.remoteUTDiscover(clientType, self.__forProject,
-                                        venvName, sysPath, workdir,
-                                        discoveryStart)
-        else:
-            # we are running as an application
-            if not discoveryStart:
-                EricMessageBox.critical(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr("You must enter a start directory for"
-                            " auto-discovery."))
-                return
-            
-            if discoveryStart:
-                sys.path = (
-                    [os.path.abspath(discoveryStart)] +
-                    self.savedSysPath
-                )
-            
-            # clean up list of imported modules to force a reimport upon
-            # running the test
-            if self.savedModulelist:
-                for modname in list(sys.modules.keys()):
-                    if modname not in self.savedModulelist:
-                        # delete it
-                        del(sys.modules[modname])
-            self.savedModulelist = sys.modules.copy()
-            
-            # now try to discover the testsuite
-            os.chdir(discoveryStart)
-            try:
-                testLoader = unittest.TestLoader()
-                test = testLoader.discover(discoveryStart)
-                if hasattr(testLoader, "errors") and bool(testLoader.errors):
-                    EricMessageBox.critical(
-                        self,
-                        self.tr("Unittest"),
-                        self.tr(
-                            "<p>Unable to discover tests.</p>"
-                            "<p>{0}</p>"
-                        ).format("<br/>".join(testLoader.errors)
-                                 .replace("\n", "<br/>"))
-                    )
-                    self.sbLabel.clear()
-                else:
-                    testsList = self.__assembleTestCasesList(
-                        test, discoveryStart)
-                    self.__populateDiscoveryResults(testsList)
-                    self.sbLabel.setText(
-                        self.tr("Discovered %n Test(s)", "",
-                                len(testsList)))
-                    self.tabWidget.setCurrentIndex(0)
-            except Exception:
-                exc_type, exc_value, exc_tb = sys.exc_info()
-                EricMessageBox.critical(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr(
-                        "<p>Unable to discover tests.</p>"
-                        "<p>{0}<br/>{1}</p>")
-                    .format(str(exc_type),
-                            str(exc_value).replace("\n", "<br/>"))
-                )
-                self.sbLabel.clear()
-            
-            sys.path = self.savedSysPath
-    
-    def __assembleTestCasesList(self, suite, start):
-        """
-        Private method to assemble a list of test cases included in a test
-        suite.
-        
-        @param suite test suite to be inspected
-        @type unittest.TestSuite
-        @param start name of directory discovery was started at
-        @type str
-        @return list of tuples containing the test case ID, a short description
-            and the path of the test file name
-        @rtype list of tuples of (str, str, str)
-        """
-        testCases = []
-        for test in suite:
-            if isinstance(test, unittest.TestSuite):
-                testCases.extend(self.__assembleTestCasesList(test, start))
-            else:
-                testId = test.id()
-                if (
-                    "ModuleImportFailure" not in testId and
-                    "LoadTestsFailure" not in testId and
-                    "_FailedTest" not in testId
-                ):
-                    filename = os.path.join(
-                        start,
-                        test.__module__.replace(".", os.sep) + ".py")
-                    testCases.append(
-                        (test.id(), test.shortDescription(), filename)
-                    )
-        return testCases
-    
-    def __findDiscoveryItem(self, modulePath):
-        """
-        Private method to find an item given the module path.
-        
-        @param modulePath path of the module in dotted notation
-        @type str
-        @return reference to the item or None
-        @rtype QTreeWidgetItem or None
-        """
-        itm = self.discoveryList.topLevelItem(0)
-        while itm is not None:
-            if itm.data(0, UnittestDialog.TestCaseNameRole) == modulePath:
-                return itm
-            itm = self.discoveryList.itemBelow(itm)
-        
-        return None
-    
-    def __populateDiscoveryResults(self, tests):
-        """
-        Private method to populate the test discovery results list.
-        
-        @param tests list of tuples containing the discovery results
-        @type list of tuples of (str, str, str)
-        """
-        for test, _testDescription, filename in tests:
-            testPath = test.split(".")
-            pitm = None
-            for index in range(1, len(testPath) + 1):
-                modulePath = ".".join(testPath[:index])
-                itm = self.__findDiscoveryItem(modulePath)
-                if itm is not None:
-                    pitm = itm
-                else:
-                    if pitm is None:
-                        itm = QTreeWidgetItem(self.discoveryList,
-                                              [testPath[index - 1]])
-                    else:
-                        itm = QTreeWidgetItem(pitm,
-                                              [testPath[index - 1]])
-                        pitm.setExpanded(True)
-                    itm.setFlags(Qt.ItemFlag.ItemIsUserCheckable |
-                                 Qt.ItemFlag.ItemIsEnabled)
-                    itm.setCheckState(0, Qt.CheckState.Unchecked)
-                    itm.setData(0, UnittestDialog.TestCaseNameRole, modulePath)
-                    if (
-                        os.path.splitext(os.path.basename(filename))[0] ==
-                        itm.text(0)
-                    ):
-                        itm.setData(0, UnittestDialog.TestCaseFileRole,
-                                    filename)
-                    elif pitm:
-                        fn = pitm.data(0, UnittestDialog.TestCaseFileRole)
-                        if fn:
-                            itm.setData(0, UnittestDialog.TestCaseFileRole, fn)
-                    pitm = itm
-    
-    def __selectedTestCases(self, parent=None):
-        """
-        Private method to assemble the list of selected test cases and suites.
-        
-        @param parent reference to the parent item
-        @type QTreeWidgetItem
-        @return list of selected test cases
-        @rtype list of str
-        """
-        selectedTests = []
-        itemsList = [
-            self.discoveryList.topLevelItem(index)
-            for index in range(self.discoveryList.topLevelItemCount())
-        ] if parent is None else [
-            parent.child(index)
-            for index in range(parent.childCount())
-        ]
-        
-        for itm in itemsList:
-            if (
-                itm.checkState(0) == Qt.CheckState.Checked and
-                itm.childCount() == 0
-            ):
-                selectedTests.append(
-                    itm.data(0, UnittestDialog.TestCaseNameRole))
-            if itm.childCount():
-                # recursively check children
-                selectedTests.extend(self.__selectedTestCases(itm))
-        
-        return selectedTests
-    
-    def __UTDiscovered(self, testCases, exc_type, exc_value):
-        """
-        Private slot to handle the utDiscovered signal.
-        
-        If the unittest suite was loaded successfully, we ask the
-        client to run the test suite.
-        
-        @param testCases list of detected test cases
-        @type str
-        @param exc_type exception type occured during discovery
-        @type str
-        @param exc_value value of exception occured during discovery
-        @type str
-        """
-        if testCases:
-            self.__populateDiscoveryResults(testCases)
-            self.sbLabel.setText(
-                self.tr("Discovered %n Test(s)", "",
-                        len(testCases)))
-            self.tabWidget.setCurrentIndex(0)
-        else:
-            EricMessageBox.critical(
-                self,
-                self.tr("Unittest"),
-                self.tr("<p>Unable to discover tests.</p>"
-                        "<p>{0}<br/>{1}</p>")
-                .format(exc_type, exc_value.replace("\n", "<br/>"))
-            )
-    
-    @pyqtSlot(QTreeWidgetItem, int)
-    def on_discoveryList_itemChanged(self, item, column):
-        """
-        Private slot handling the user checking or unchecking an item.
-
-        @param item reference to the item
-        @type QTreeWidgetItem
-        @param column changed column
-        @type int
-        """
-        if column == 0:
-            for index in range(item.childCount()):
-                item.child(index).setCheckState(0, item.checkState(0))
-
-    @pyqtSlot(QTreeWidgetItem, int)
-    def on_discoveryList_itemDoubleClicked(self, item, column):
-        """
-        Private slot handling the user double clicking an item.
-        
-        @param item reference to the item
-        @type QTreeWidgetItem
-        @param column column of the double click
-        @type int
-        """
-        if item:
-            filename = item.data(0, UnittestDialog.TestCaseFileRole)
-            if filename:
-                if self.__dbs:
-                    # running as part of eric IDE
-                    self.unittestFile.emit(filename, 1, False)
-                else:
-                    self.__openEditor(filename, 1)
-    
-    @pyqtSlot()
-    def startTests(self, failedOnly=False):
-        """
-        Public slot to start the test.
-        
-        @param failedOnly flag indicating to run only failed tests (boolean)
-        """
-        if self.running:
-            return
-        
-        discover = self.discoverCheckBox.isChecked()
-        if discover:
-            discoveryStart = self.discoveryPicker.currentText()
-            testFileName = ""
-            testName = ""
-            
-            if discoveryStart:
-                self.insertDiscovery(discoveryStart)
-        else:
-            discoveryStart = ""
-            testFileName = self.testsuitePicker.currentText()
-            testName = self.testComboBox.currentText()
-            if testName:
-                self.insertTestName(testName)
-            if testFileName and not testName:
-                testName = "suite"
-        
-        if not discover and not testFileName and not testName:
-            EricMessageBox.critical(
-                self,
-                self.tr("Unittest"),
-                self.tr("You must select auto-discovery or enter a test suite"
-                        " file or a dotted test name."))
-            return
-        
-        # prepend the selected file to the testsuite combobox
-        self.insertProg(testFileName)
-        self.sbLabel.setText(self.tr("Preparing Testsuite"))
-        QApplication.processEvents()
-        
-        if discover:
-            self.testName = self.tr("Unittest with auto-discovery")
-        else:
-            # build the module name from the filename without extension
-            if testFileName:
-                self.testName = os.path.splitext(
-                    os.path.basename(testFileName))[0]
-            elif testName:
-                self.testName = testName
-            else:
-                self.testName = self.tr("<Unnamed Test>")
-        
-        if failedOnly:
-            testCases = []
-        else:
-            testCases = self.__selectedTestCases()
-        
-            if not testCases and self.discoveryList.topLevelItemCount():
-                ok = EricMessageBox.yesNo(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr("""No test case has been selected. Shall all"""
-                            """ test cases be run?"""))
-                if not ok:
-                    return
-        
-        if self.__dbs:
-            venvName = self.venvComboBox.currentText()
-            
-            # we are cooperating with the eric IDE
-            project = ericApp().getObject("Project")
-            if self.__forProject:
-                mainScript = project.getMainScript(True)
-                clientType = project.getProjectLanguage()
-                if mainScript:
-                    workdir = os.path.dirname(os.path.abspath(mainScript))
-                    coverageFile = os.path.splitext(mainScript)[0]
-                else:
-                    workdir = project.getProjectPath()
-                    coverageFile = os.path.join(discoveryStart, "unittest")
-                sysPath = [workdir]
-                if discover and not discoveryStart:
-                    discoveryStart = workdir
-            else:
-                if discover:
-                    if not discoveryStart:
-                        EricMessageBox.critical(
-                            self,
-                            self.tr("Unittest"),
-                            self.tr("You must enter a start directory for"
-                                    " auto-discovery."))
-                        return
-                    
-                    coverageFile = os.path.join(discoveryStart, "unittest")
-                    workdir = ""
-                    clientType = "Python3"
-                elif testFileName:
-                    mainScript = os.path.abspath(testFileName)
-                    workdir = os.path.dirname(mainScript)
-                    clientType = "Python3"
-                    coverageFile = os.path.splitext(mainScript)[0]
-                else:
-                    coverageFile = os.path.abspath("unittest")
-                    workdir = ""
-                    clientType = "Python3"
-                sysPath = []
-            if failedOnly and self.__failedTests:
-                failed = list(self.__failedTests)
-                if discover:
-                    workdir = discoveryStart
-                    discover = False
-            else:
-                failed = []
-            self.__failedTests = set()
-            self.__dbs.remoteUTPrepare(
-                testFileName, self.testName, testName, failed,
-                self.coverageCheckBox.isChecked(), coverageFile,
-                self.coverageEraseCheckBox.isChecked(), clientType=clientType,
-                forProject=self.__forProject, workdir=workdir,
-                venvName=venvName, syspath=sysPath,
-                discover=discover, discoveryStart=discoveryStart,
-                testCases=testCases, debug=self.debuggerCheckBox.isChecked())
-        else:
-            # we are running as an application
-            if discover and not discoveryStart:
-                EricMessageBox.critical(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr("You must enter a start directory for"
-                            " auto-discovery."))
-                return
-            
-            if testFileName:
-                sys.path = (
-                    [os.path.dirname(os.path.abspath(testFileName))] +
-                    self.savedSysPath
-                )
-            elif discoveryStart:
-                sys.path = (
-                    [os.path.abspath(discoveryStart)] +
-                    self.savedSysPath
-                )
-            
-            # clean up list of imported modules to force a reimport upon
-            # running the test
-            if self.savedModulelist:
-                for modname in list(sys.modules.keys()):
-                    if modname not in self.savedModulelist:
-                        # delete it
-                        del(sys.modules[modname])
-            self.savedModulelist = sys.modules.copy()
-            
-            os.chdir(self.savedCwd)
-            
-            # now try to generate the testsuite
-            try:
-                testLoader = unittest.TestLoader()
-                if failedOnly and self.__failedTests:
-                    failed = list(self.__failedTests)
-                    if discover:
-                        os.chdir(discoveryStart)
-                        discover = False
-                else:
-                    failed = []
-                if discover:
-                    if testCases:
-                        test = testLoader.loadTestsFromNames(testCases)
-                    else:
-                        test = testLoader.discover(discoveryStart)
-                else:
-                    if testFileName:
-                        module = __import__(self.testName)
-                    else:
-                        module = None
-                    if failedOnly and self.__failedTests:
-                        if module:
-                            failed = [t.split(".", 1)[1]
-                                      for t in self.__failedTests]
-                        else:
-                            failed = list(self.__failedTests)
-                        test = testLoader.loadTestsFromNames(
-                            failed, module)
-                    else:
-                        test = testLoader.loadTestsFromName(
-                            testName, module)
-            except Exception:
-                exc_type, exc_value, exc_tb = sys.exc_info()
-                EricMessageBox.critical(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr(
-                        "<p>Unable to run test <b>{0}</b>.</p>"
-                        "<p>{1}<br/>{2}</p>")
-                    .format(self.testName, str(exc_type),
-                            str(exc_value).replace("\n", "<br/>"))
-                )
-                return
-                
-            # now set up the coverage stuff
-            if self.coverageCheckBox.isChecked():
-                if discover:
-                    covname = os.path.join(discoveryStart, "unittest")
-                elif testFileName:
-                    covname = os.path.splitext(
-                        os.path.abspath(testFileName))[0]
-                else:
-                    covname = "unittest"
-                
-                from DebugClients.Python.coverage import coverage
-                cover = coverage(data_file="{0}.coverage".format(covname))
-                if self.coverageEraseCheckBox.isChecked():
-                    cover.erase()
-            else:
-                cover = None
-            
-            self.testResult = QtTestResult(
-                self, self.failfastCheckBox.isChecked())
-            self.totalTests = test.countTestCases()
-            if self.totalTests == 0:
-                EricMessageBox.warning(
-                    self,
-                    self.tr("Unittest"),
-                    self.tr("""No unittest were found. Aborting..."""))
-            else:
-                self.__failedTests = set()
-                self.__setRunningMode()
-                if cover:
-                    cover.start()
-                test.run(self.testResult)
-                if cover:
-                    cover.stop()
-                    cover.save()
-                self.__setStoppedMode()
-                sys.path = self.savedSysPath
-    
-    def __UTPrepared(self, nrTests, exc_type, exc_value):
-        """
-        Private slot to handle the utPrepared signal.
-        
-        If the unittest suite was loaded successfully, we ask the
-        client to run the test suite.
-        
-        @param nrTests number of tests contained in the test suite (integer)
-        @param exc_type type of exception occured during preparation (string)
-        @param exc_value value of exception occured during preparation (string)
-        """
-        if nrTests == 0:
-            EricMessageBox.critical(
-                self,
-                self.tr("Unittest"),
-                self.tr(
-                    "<p>Unable to run test <b>{0}</b>.</p>"
-                    "<p>{1}<br/>{2}</p>")
-                .format(self.testName, exc_type,
-                        exc_value.replace("\n", "<br/>"))
-            )
-            return
-        
-        self.totalTests = nrTests
-        self.__setRunningMode()
-        self.__dbs.remoteUTRun(debug=self.debuggerCheckBox.isChecked(),
-                               failfast=self.failfastCheckBox.isChecked())
-    
-    @pyqtSlot()
-    def __stopTests(self):
-        """
-        Private slot to stop the test.
-        """
-        if self.__dbs:
-            self.__dbs.remoteUTStop()
-        elif self.testResult:
-            self.testResult.stop()
-    
-    def on_errorsListWidget_currentTextChanged(self, text):
-        """
-        Private slot to handle the highlighted signal.
-        
-        @param text current text (string)
-        """
-        if text:
-            for pattern in self.rxPatterns:
-                text = re.sub(pattern, "", text)
-            
-            foundItems = self.testsListWidget.findItems(
-                text, Qt.MatchFlag.MatchExactly)
-            if len(foundItems) > 0:
-                itm = foundItems[0]
-                self.testsListWidget.setCurrentItem(itm)
-                self.testsListWidget.scrollToItem(itm)
-    
-    def __setRunningMode(self):
-        """
-        Private method to set the GUI in running mode.
-        """
-        self.running = True
-        self.tabWidget.setCurrentIndex(1)
-        
-        # reset counters and error infos
-        self.runCount = 0
-        self.failCount = 0
-        self.errorCount = 0
-        self.skippedCount = 0
-        self.expectedFailureCount = 0
-        self.unexpectedSuccessCount = 0
-        self.remainingCount = self.totalTests
-        
-        # reset the GUI
-        self.progressCounterRunCount.setText(str(self.runCount))
-        self.progressCounterRemCount.setText(str(self.remainingCount))
-        self.progressCounterFailureCount.setText(str(self.failCount))
-        self.progressCounterErrorCount.setText(str(self.errorCount))
-        self.progressCounterSkippedCount.setText(str(self.skippedCount))
-        self.progressCounterExpectedFailureCount.setText(
-            str(self.expectedFailureCount))
-        self.progressCounterUnexpectedSuccessCount.setText(
-            str(self.unexpectedSuccessCount))
-        
-        self.errorsListWidget.clear()
-        self.testsListWidget.clear()
-        
-        self.progressProgressBar.setRange(0, self.totalTests)
-        self.__setProgressColor("green")
-        self.progressProgressBar.reset()
-        
-        self.stopButton.setEnabled(True)
-        self.startButton.setEnabled(False)
-        self.startFailedButton.setEnabled(False)
-        self.stopButton.setDefault(True)
-        
-        self.sbLabel.setText(self.tr("Running"))
-        self.progressLed.on()
-        QApplication.processEvents()
-        
-        self.startTime = time.time()
-    
-    def __setStoppedMode(self):
-        """
-        Private method to set the GUI in stopped mode.
-        """
-        self.stopTime = time.time()
-        self.timeTaken = float(self.stopTime - self.startTime)
-        self.running = False
-        
-        failedAvailable = bool(self.__failedTests)
-        self.startButton.setEnabled(True)
-        self.startFailedButton.setEnabled(failedAvailable)
-        self.stopButton.setEnabled(False)
-        if failedAvailable:
-            self.startFailedButton.setDefault(True)
-            self.startButton.setDefault(False)
-        else:
-            self.startFailedButton.setDefault(False)
-            self.startButton.setDefault(True)
-        self.sbLabel.setText(
-            self.tr("Ran %n test(s) in {0:.3f}s", "", self.runCount)
-            .format(self.timeTaken))
-        self.progressLed.off()
-        
-        self.unittestStopped.emit()
-        
-        self.raise_()
-        self.activateWindow()
-    
-    def testFailed(self, test, exc, testId):
-        """
-        Public method called if a test fails.
-        
-        @param test name of the test (string)
-        @param exc string representation of the exception (string)
-        @param testId id of the test (string)
-        """
-        self.failCount += 1
-        self.progressCounterFailureCount.setText(str(self.failCount))
-        itm = QListWidgetItem(self.tr("Failure: {0}").format(test))
-        itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc))
-        self.errorsListWidget.insertItem(0, itm)
-        self.__failedTests.add(testId)
-    
-    def testErrored(self, test, exc, testId):
-        """
-        Public method called if a test errors.
-        
-        @param test name of the test (string)
-        @param exc string representation of the exception (string)
-        @param testId id of the test (string)
-        """
-        self.errorCount += 1
-        self.progressCounterErrorCount.setText(str(self.errorCount))
-        itm = QListWidgetItem(self.tr("Error: {0}").format(test))
-        itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc))
-        self.errorsListWidget.insertItem(0, itm)
-        self.__failedTests.add(testId)
-    
-    def testSkipped(self, test, reason, testId):
-        """
-        Public method called if a test was skipped.
-        
-        @param test name of the test (string)
-        @param reason reason for skipping the test (string)
-        @param testId id of the test (string)
-        """
-        self.skippedCount += 1
-        self.progressCounterSkippedCount.setText(str(self.skippedCount))
-        itm = QListWidgetItem(self.tr("    Skipped: {0}").format(reason))
-        if ericApp().usesDarkPalette():
-            itm.setForeground(self.SkippedColorDarkTheme)
-        else:
-            itm.setForeground(self.SkippedColorLightTheme)
-        self.testsListWidget.insertItem(1, itm)
-    
-    def testFailedExpected(self, test, exc, testId):
-        """
-        Public method called if a test fails as expected.
-        
-        @param test name of the test (string)
-        @param exc string representation of the exception (string)
-        @param testId id of the test (string)
-        """
-        self.expectedFailureCount += 1
-        self.progressCounterExpectedFailureCount.setText(
-            str(self.expectedFailureCount))
-        itm = QListWidgetItem(self.tr("    Expected Failure"))
-        if ericApp().usesDarkPalette():
-            itm.setForeground(self.FailedExpectedColorDarkTheme)
-        else:
-            itm.setForeground(self.FailedExpectedColorLightTheme)
-        self.testsListWidget.insertItem(1, itm)
-    
-    def testSucceededUnexpected(self, test, testId):
-        """
-        Public method called if a test succeeds unexpectedly.
-        
-        @param test name of the test (string)
-        @param testId id of the test (string)
-        """
-        self.unexpectedSuccessCount += 1
-        self.progressCounterUnexpectedSuccessCount.setText(
-            str(self.unexpectedSuccessCount))
-        itm = QListWidgetItem(self.tr("    Unexpected Success"))
-        if ericApp().usesDarkPalette():
-            itm.setForeground(self.SucceededUnexpectedColorDarkTheme)
-        else:
-            itm.setForeground(self.SucceededUnexpectedColorLightTheme)
-        self.testsListWidget.insertItem(1, itm)
-    
-    def testStarted(self, test, doc):
-        """
-        Public method called if a test is about to be run.
-        
-        @param test name of the started test (string)
-        @param doc documentation of the started test (string)
-        """
-        if doc:
-            self.testsListWidget.insertItem(0, "    {0}".format(doc))
-        self.testsListWidget.insertItem(0, test)
-        if self.__dbs is None:
-            QApplication.processEvents()
-    
-    def testFinished(self):
-        """
-        Public method called if a test has finished.
-        
-        <b>Note</b>: It is also called if it has already failed or errored.
-        """
-        # update the counters
-        self.remainingCount -= 1
-        self.runCount += 1
-        self.progressCounterRunCount.setText(str(self.runCount))
-        self.progressCounterRemCount.setText(str(self.remainingCount))
-        
-        # update the progressbar
-        if self.errorCount:
-            self.__setProgressColor("red")
-        elif self.failCount:
-            self.__setProgressColor("orange")
-        self.progressProgressBar.setValue(self.runCount)
-    
-    def on_errorsListWidget_itemDoubleClicked(self, lbitem):
-        """
-        Private slot called by doubleclicking an errorlist entry.
-        
-        It will popup a dialog showing the stacktrace.
-        If called from eric, an additional button is displayed
-        to show the python source in an eric source viewer (in
-        erics main window.
-        
-        @param lbitem the listbox item that was double clicked
-        """
-        self.errListIndex = self.errorsListWidget.row(lbitem)
-        text = lbitem.text()
-        self.on_errorsListWidget_currentTextChanged(text)
-        
-        # get the error info
-        test, tracebackText = lbitem.data(UnittestDialog.ErrorsInfoRole)
-        
-        # now build the dialog
-        from .Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog
-        self.dlg = QDialog(self)
-        ui = Ui_UnittestStacktraceDialog()
-        ui.setupUi(self.dlg)
-        self.dlg.traceback = ui.traceback
-        
-        ui.showButton = ui.buttonBox.addButton(
-            self.tr("Show Source"), QDialogButtonBox.ButtonRole.ActionRole)
-        ui.showButton.clicked.connect(self.__showSource)
-        
-        ui.buttonBox.button(
-            QDialogButtonBox.StandardButton.Close).setDefault(True)
-        
-        self.dlg.setWindowTitle(text)
-        ui.testLabel.setText(test)
-        ui.traceback.setPlainText(tracebackText)
-        
-        # and now fire it up
-        self.dlg.show()
-        self.dlg.exec()
-    
-    def __showSource(self):
-        """
-        Private slot to show the source of a traceback in an eric editor.
-        """
-        # get the error info
-        tracebackLines = self.dlg.traceback.toPlainText().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:
-            fn, ln = fmatch.group(1, 2)
-            if self.__dbs:
-                # running as part of eric IDE
-                self.unittestFile.emit(fn, int(ln), True)
-            else:
-                self.__openEditor(fn, int(ln))
-    
-    def hasFailedTests(self):
-        """
-        Public method to check, if there are failed tests from the last run.
-        
-        @return flag indicating the presence of failed tests (boolean)
-        """
-        return bool(self.__failedTests)
-    
-    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 QtTestResult(unittest.TestResult):
-    """
-    A TestResult derivative to work with a graphical GUI.
-    
-    For more details see pyunit.py of the standard Python distribution.
-    """
-    def __init__(self, parent, failfast):
-        """
-        Constructor
-        
-        @param parent reference to the parent widget
-        @type UnittestDialog
-        @param failfast flag indicating to stop at the first error
-        @type bool
-        """
-        super().__init__()
-        self.parent = parent
-        self.failfast = failfast
-    
-    def addFailure(self, test, err):
-        """
-        Public method called if a test failed.
-        
-        @param test reference to the test object
-        @param err error traceback
-        """
-        super().addFailure(test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.parent.testFailed(str(test), tracebackLines, test.id())
-    
-    def addError(self, test, err):
-        """
-        Public method called if a test errored.
-        
-        @param test reference to the test object
-        @param err error traceback
-        """
-        super().addError(test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.parent.testErrored(str(test), tracebackLines, test.id())
-    
-    def addSubTest(self, test, subtest, err):
-        """
-        Public method called for each subtest to record its result.
-        
-        @param test reference to the test object
-        @param subtest reference to the subtest object
-        @param err error traceback
-        """
-        if err is not None:
-            super().addSubTest(test, subtest, err)
-            tracebackLines = self._exc_info_to_string(err, test)
-            if issubclass(err[0], test.failureException):
-                self.parent.testFailed(
-                    str(subtest), tracebackLines, test.id())
-            else:
-                self.parent.testErrored(
-                    str(subtest), tracebackLines, test.id())
-    
-    def addSkip(self, test, reason):
-        """
-        Public method called if a test was skipped.
-        
-        @param test reference to the test object
-        @param reason reason for skipping the test (string)
-        """
-        super().addSkip(test, reason)
-        self.parent.testSkipped(str(test), reason, test.id())
-    
-    def addExpectedFailure(self, test, err):
-        """
-        Public method called if a test failed expected.
-        
-        @param test reference to the test object
-        @param err error traceback
-        """
-        super().addExpectedFailure(test, err)
-        tracebackLines = self._exc_info_to_string(err, test)
-        self.parent.testFailedExpected(str(test), tracebackLines, test.id())
-    
-    def addUnexpectedSuccess(self, test):
-        """
-        Public method called if a test succeeded expectedly.
-        
-        @param test reference to the test object
-        """
-        super().addUnexpectedSuccess(test)
-        self.parent.testSucceededUnexpected(str(test), test.id())
-    
-    def startTest(self, test):
-        """
-        Public method called at the start of a test.
-        
-        @param test Reference to the test object
-        """
-        super().startTest(test)
-        self.parent.testStarted(str(test), test.shortDescription())
-
-    def stopTest(self, test):
-        """
-        Public method called at the end of a test.
-        
-        @param test Reference to the test object
-        """
-        super().stopTest(test)
-        self.parent.testFinished()
-
-
-class UnittestWindow(EricMainWindow):
-    """
-    Main window class for the standalone dialog.
-    """
-    def __init__(self, prog=None, parent=None):
-        """
-        Constructor
-        
-        @param prog filename of the program to open
-        @param parent reference to the parent widget (QWidget)
-        """
-        super().__init__(parent)
-        self.cw = UnittestDialog(prog, 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:
-            QApplication.exit()
-            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/PyUnit/UnittestDialog.ui	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,694 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>UnittestDialog</class>
- <widget class="QWidget" name="UnittestDialog">
-  <property name="geometry">
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>650</width>
-    <height>700</height>
-   </rect>
-  </property>
-  <property name="windowTitle">
-   <string>Unittest</string>
-  </property>
-  <layout class="QVBoxLayout" name="verticalLayout_5">
-   <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_6">
-       <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>&amp;Discover tests (test modules must be importable)</string>
-              </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 &amp;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="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 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 &amp;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="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">
-            <property name="text">
-             <string>&amp;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="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="QHBoxLayout" name="horizontalLayout_3">
-            <item>
-             <widget class="QLabel" name="venvLabel">
-              <property name="text">
-               <string>&amp;Virtual Environment:</string>
-              </property>
-              <property name="buddy">
-               <cstring>venvComboBox</cstring>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <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>
-           </layout>
-          </item>
-          <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>C&amp;ollect 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>
-            <item row="1" column="1">
-             <widget class="QCheckBox" name="debuggerCheckBox">
-              <property name="toolTip">
-               <string>Select to run the unittest with debugger support enabled</string>
-              </property>
-              <property name="text">
-               <string>Run with Debugger</string>
-              </property>
-             </widget>
-            </item>
-           </layout>
-          </item>
-         </layout>
-        </widget>
-       </item>
-       <item>
-        <widget class="QGroupBox" name="discoveryGroup">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="title">
-          <string>Discovery Results</string>
-         </property>
-         <layout class="QVBoxLayout" name="verticalLayout_3">
-          <item>
-           <widget class="QTreeWidget" name="discoveryList">
-            <property name="alternatingRowColors">
-             <bool>true</bool>
-            </property>
-            <property name="sortingEnabled">
-             <bool>true</bool>
-            </property>
-            <property name="headerHidden">
-             <bool>true</bool>
-            </property>
-            <property name="expandsOnDoubleClick">
-             <bool>false</bool>
-            </property>
-            <column>
-             <property name="text">
-              <string notr="true">1</string>
-             </property>
-            </column>
-           </widget>
-          </item>
-         </layout>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-     <widget class="QWidget" name="resultsTab">
-      <attribute name="title">
-       <string>Results</string>
-      </attribute>
-      <layout class="QVBoxLayout" name="verticalLayout_4">
-       <item>
-        <widget class="QGroupBox" name="progressGroupBox">
-         <property name="title">
-          <string>Progress</string>
-         </property>
-         <layout class="QVBoxLayout" name="verticalLayout">
-          <item>
-           <layout class="QHBoxLayout" name="_8">
-            <item>
-             <widget class="QLabel" name="progressTextLabel">
-              <property name="text">
-               <string>Progress:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <spacer>
-              <property name="orientation">
-               <enum>Qt::Horizontal</enum>
-              </property>
-              <property name="sizeHint" stdset="0">
-               <size>
-                <width>371</width>
-                <height>20</height>
-               </size>
-              </property>
-             </spacer>
-            </item>
-            <item>
-             <widget class="EricLed" name="progressLed"/>
-            </item>
-           </layout>
-          </item>
-          <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>
-          <item>
-           <layout class="QHBoxLayout" name="horizontalLayout">
-            <item>
-             <widget class="QLabel" name="progressCounterFailureLabel">
-              <property name="text">
-               <string>Failures:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterFailureCount">
-              <property name="toolTip">
-               <string>Number of test failures</string>
-              </property>
-              <property name="text">
-               <string notr="true">0</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterErrorLabel">
-              <property name="text">
-               <string>Errors:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterErrorCount">
-              <property name="toolTip">
-               <string>Number of test errors</string>
-              </property>
-              <property name="text">
-               <string notr="true">0</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterSkippedLabel">
-              <property name="text">
-               <string>Skipped:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterSkippedCount">
-              <property name="toolTip">
-               <string>Number of tests skipped</string>
-              </property>
-              <property name="text">
-               <string notr="true">0</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterExpectedFailureLabel">
-              <property name="text">
-               <string>Expected Failures:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterExpectedFailureCount">
-              <property name="toolTip">
-               <string>Number of tests with expected failure</string>
-              </property>
-              <property name="text">
-               <string notr="true">0</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterUnexpectedSuccessLabel">
-              <property name="text">
-               <string>Unexpected Successes:</string>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QLabel" name="progressCounterUnexpectedSuccessCount">
-              <property name="toolTip">
-               <string>Number of tests with unexpected success</string>
-              </property>
-              <property name="text">
-               <string notr="true">0</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>
-         </layout>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="textLabel1">
-         <property name="text">
-          <string>Tests performed:</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QListWidget" name="testsListWidget"/>
-       </item>
-       <item>
-        <widget class="QLabel" name="listboxLabel">
-         <property name="text">
-          <string>Failures and errors:</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QListWidget" name="errorsListWidget">
-         <property name="toolTip">
-          <string>Failures and Errors list</string>
-         </property>
-         <property name="whatsThis">
-          <string>&lt;b&gt;Failures and Errors list&lt;/b&gt;
-&lt;p&gt;This list shows all failed and errored tests.
-Double clicking on an entry will show the respective traceback.&lt;/p&gt;</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </widget>
-    </widget>
-   </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>
-   <item>
-    <layout class="QHBoxLayout" name="_3">
-     <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>
-  </layout>
- </widget>
- <customwidgets>
-  <customwidget>
-   <class>EricComboPathPicker</class>
-   <extends>QWidget</extends>
-   <header>EricWidgets/EricPathPicker.h</header>
-   <container>1</container>
-  </customwidget>
-  <customwidget>
-   <class>EricLed</class>
-   <extends>QFrame</extends>
-   <header>EricWidgets/EricLed.h</header>
-   <container>1</container>
-  </customwidget>
- </customwidgets>
- <tabstops>
-  <tabstop>tabWidget</tabstop>
-  <tabstop>discoverCheckBox</tabstop>
-  <tabstop>clearHistoriesButton</tabstop>
-  <tabstop>discoveryPicker</tabstop>
-  <tabstop>testsuitePicker</tabstop>
-  <tabstop>testComboBox</tabstop>
-  <tabstop>venvComboBox</tabstop>
-  <tabstop>coverageCheckBox</tabstop>
-  <tabstop>coverageEraseCheckBox</tabstop>
-  <tabstop>failfastCheckBox</tabstop>
-  <tabstop>debuggerCheckBox</tabstop>
-  <tabstop>discoveryList</tabstop>
-  <tabstop>testsListWidget</tabstop>
-  <tabstop>errorsListWidget</tabstop>
- </tabstops>
- <resources/>
- <connections>
-  <connection>
-   <sender>coverageCheckBox</sender>
-   <signal>toggled(bool)</signal>
-   <receiver>coverageEraseCheckBox</receiver>
-   <slot>setEnabled(bool)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>321</x>
-     <y>294</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>616</x>
-     <y>294</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>accepted()</signal>
-   <receiver>UnittestDialog</receiver>
-   <slot>close()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>67</x>
-     <y>662</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>72</x>
-     <y>667</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>rejected()</signal>
-   <receiver>UnittestDialog</receiver>
-   <slot>close()</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>157</x>
-     <y>662</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>148</x>
-     <y>668</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>discoverCheckBox</sender>
-   <signal>toggled(bool)</signal>
-   <receiver>discoveryPicker</receiver>
-   <slot>setEnabled(bool)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>209</x>
-     <y>93</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>209</x>
-     <y>120</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>discoverCheckBox</sender>
-   <signal>toggled(bool)</signal>
-   <receiver>testsuitePicker</receiver>
-   <slot>setDisabled(bool)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>96</x>
-     <y>93</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>150</x>
-     <y>143</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>discoverCheckBox</sender>
-   <signal>toggled(bool)</signal>
-   <receiver>testComboBox</receiver>
-   <slot>setDisabled(bool)</slot>
-   <hints>
-    <hint type="sourcelabel">
-     <x>321</x>
-     <y>97</y>
-    </hint>
-    <hint type="destinationlabel">
-     <x>327</x>
-     <y>166</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>
--- a/eric7/PyUnit/UnittestStacktraceDialog.ui	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-<ui version="4.0" >
- <class>UnittestStacktraceDialog</class>
- <widget class="QDialog" name="UnittestStacktraceDialog" >
-  <property name="geometry" >
-   <rect>
-    <x>0</x>
-    <y>0</y>
-    <width>600</width>
-    <height>250</height>
-   </rect>
-  </property>
-  <layout class="QVBoxLayout" >
-   <item>
-    <widget class="QLabel" name="testLabel" />
-   </item>
-   <item>
-    <widget class="QTextEdit" name="traceback" >
-     <property name="readOnly" >
-      <bool>true</bool>
-     </property>
-     <property name="acceptRichText" >
-      <bool>false</bool>
-     </property>
-    </widget>
-   </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>
- <resources/>
- <connections>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>accepted()</signal>
-   <receiver>UnittestStacktraceDialog</receiver>
-   <slot>accept()</slot>
-   <hints>
-    <hint type="sourcelabel" >
-     <x>33</x>
-     <y>235</y>
-    </hint>
-    <hint type="destinationlabel" >
-     <x>33</x>
-     <y>249</y>
-    </hint>
-   </hints>
-  </connection>
-  <connection>
-   <sender>buttonBox</sender>
-   <signal>rejected()</signal>
-   <receiver>UnittestStacktraceDialog</receiver>
-   <slot>reject()</slot>
-   <hints>
-    <hint type="sourcelabel" >
-     <x>94</x>
-     <y>226</y>
-    </hint>
-    <hint type="destinationlabel" >
-     <x>95</x>
-     <y>249</y>
-    </hint>
-   </hints>
-  </connection>
- </connections>
-</ui>
--- a/eric7/PyUnit/__init__.py	Thu May 19 10:45:41 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
-#
-
-"""
-Package implementing an interface to the pyunit unittest package.
-
-The package consist of a single dialog, which may be called as a
-standalone version using the eric7_unittest script or from within the eric
-IDE. If it is called from within eric, it has the additional function to
-open a source file that failed a test.
-"""
--- a/eric7/QScintilla/Editor.py	Thu May 19 10:45:41 2022 +0200
+++ b/eric7/QScintilla/Editor.py	Thu May 19 14:40:15 2022 +0200
@@ -522,6 +522,9 @@
         self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0
         self.autosaveManuallyDisabled = False
         
+        # code coverage related attributes
+        self.__coverageFile = ""
+        
         self.__initContextMenu()
         self.__initContextMenuMargins()
         
@@ -5629,38 +5632,38 @@
         ):
             fn = self.project.getMainScript(True)
             if fn is not None:
-                tfn = Utilities.getTestFileName(fn)
-                basename = os.path.splitext(fn)[0]
-                tbasename = os.path.splitext(tfn)[0]
-                prEnable = (
-                    prEnable or
-                    os.path.isfile("{0}.profile".format(basename)) or
-                    os.path.isfile("{0}.profile".format(tbasename))
-                )
+                filenames = [os.path.splitext(f)[0]
+                             for f in [fn] + Utilities.getTestFileNames(fn)]
+                prEnable = any([
+                    os.path.isfile("{0}.profile".format(f))
+                    for f in filenames
+                ])
                 coEnable = (
-                    (coEnable or
-                     os.path.isfile("{0}.coverage".format(basename)) or
-                     os.path.isfile("{0}.coverage".format(tbasename))) and
-                    self.project.isPy3Project()
+                    self.project.isPy3Project() and
+                    any([
+                        os.path.isfile("{0}.coverage".format(f))
+                        for f in filenames
+                    ])
                 )
         
         # now check ourselves
         fn = self.getFileName()
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            prEnable = (
-                prEnable or
-                os.path.isfile("{0}.profile".format(basename)) or
-                os.path.isfile("{0}.profile".format(tbasename))
+            filenames = [os.path.splitext(f)[0]
+                         for f in [fn] + Utilities.getTestFileNames(fn)]
+            prEnable |= any([
+                os.path.isfile("{0}.profile".format(f))
+                for f in filenames
+            ])
+            coEnable |= (
+                self.project.isPy3Project() and
+                any([
+                    os.path.isfile("{0}.coverage".format(f))
+                    for f in filenames
+                ])
             )
-            coEnable = (
-                (coEnable or
-                 os.path.isfile("{0}.coverage".format(basename)) or
-                 os.path.isfile("{0}.coverage".format(tbasename))) and
-                self.isPyFile()
-            )
+        
+        coEnable |= bool(self.__coverageFile)
         
         # now check for syntax errors
         if self.hasSyntaxErrors():
@@ -6052,6 +6055,10 @@
         """
         files = []
         
+        if bool(self.__coverageFile):
+            # return the path of a previously used coverage file
+            return self.__coverageFile
+        
         # first check if the file belongs to a project and there is
         # a project coverage file
         if (
@@ -6060,30 +6067,23 @@
         ):
             fn = self.project.getMainScript(True)
             if fn is not None:
-                tfn = Utilities.getTestFileName(fn)
-                basename = os.path.splitext(fn)[0]
-                tbasename = os.path.splitext(tfn)[0]
-                
-                f = "{0}.coverage".format(basename)
-                tf = "{0}.coverage".format(tbasename)
-                if os.path.isfile(f):
-                    files.append(f)
-                if os.path.isfile(tf):
-                    files.append(tf)
+                for filename in [fn] + Utilities.getTestFileNames(fn):
+                    basename = os.path.splitext(filename)[0]
+                    f = "{0}.coverage".format(basename)
+                    if os.path.isfile(f):
+                        files.append(f)
         
         # now check, if there are coverage files belonging to ourselves
         fn = self.getFileName()
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            
-            f = "{0}.coverage".format(basename)
-            tf = "{0}.coverage".format(tbasename)
-            if os.path.isfile(f) and f not in files:
-                files.append(f)
-            if os.path.isfile(tf) and tf not in files:
-                files.append(tf)
+            for filename in [fn] + Utilities.getTestFileNames(fn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.coverage".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
+        
+        # make the list unique
+        files = list(set(files))
         
         if files:
             if len(files) > 1:
@@ -6107,6 +6107,7 @@
         Private method to handle the code coverage context menu action.
         """
         fn = self.__getCodeCoverageFile()
+        self.__coverageFile = fn
         if fn:
             from DataViews.PyCoverageDialog import PyCoverageDialog
             self.codecoverage = PyCoverageDialog()
@@ -6120,16 +6121,27 @@
         if self.showingNotcoveredMarkers:
             self.codeCoverageShowAnnotations(silent=True)
         
-    def codeCoverageShowAnnotations(self, silent=False):
+    def codeCoverageShowAnnotations(self, silent=False, coverageFile=None):
         """
         Public method to handle the show code coverage annotations context
         menu action.
         
-        @param silent flag indicating to not show any dialog (boolean)
+        @param silent flag indicating to not show any dialog (defaults to
+            False)
+        @type bool (optional)
+        @param coverageFile path of the file containing the code coverage data
+            (defaults to None)
+        @type str (optional)
         """
         self.__codeCoverageHideAnnotations()
         
-        fn = self.__getCodeCoverageFile()
+        fn = (
+            coverageFile
+            if bool(coverageFile) else
+            self.__getCodeCoverageFile()
+        )
+        self.__coverageFile = fn
+        
         if fn:
             from coverage import Coverage
             cover = Coverage(data_file=fn)
@@ -6240,30 +6252,23 @@
         ):
             fn = self.project.getMainScript(True)
             if fn is not None:
-                tfn = Utilities.getTestFileName(fn)
-                basename = os.path.splitext(fn)[0]
-                tbasename = os.path.splitext(tfn)[0]
-                
-                f = "{0}.profile".format(basename)
-                tf = "{0}.profile".format(tbasename)
-                if os.path.isfile(f):
-                    files.append(f)
-                if os.path.isfile(tf):
-                    files.append(tf)
+                for filename in [fn] + Utilities.getTestFileNames(fn):
+                    basename = os.path.splitext(filename)[0]
+                    f = "{0}.profile".format(basename)
+                    if os.path.isfile(f):
+                        files.append(f)
         
         # now check, if there are profile files belonging to ourselves
         fn = self.getFileName()
         if fn is not None:
-            tfn = Utilities.getTestFileName(fn)
-            basename = os.path.splitext(fn)[0]
-            tbasename = os.path.splitext(tfn)[0]
-            
-            f = "{0}.profile".format(basename)
-            tf = "{0}.profile".format(tbasename)
-            if os.path.isfile(f) and f not in files:
-                files.append(f)
-            if os.path.isfile(tf) and tf not in files:
-                files.append(tf)
+            for filename in [fn] + Utilities.getTestFileNames(fn):
+                basename = os.path.splitext(filename)[0]
+                f = "{0}.profile".format(basename)
+                if os.path.isfile(f):
+                    files.append(f)
+        
+        # make the list unique
+        files = list(set(files))
         
         if files:
             if len(files) > 1:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Testing/Interfaces/PytestExecutor.py	Thu May 19 14:40:15 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	Thu May 19 14:40:15 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	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,263 @@
+# -*- 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
+    coverageFile: str               # name of the coverage data file
+
+
+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	Thu May 19 14:40:15 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	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,224 @@
+# -*- 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.coverageFile:
+                args.append("--cover-file")
+                args.append(config.coverageFile)
+        
+        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	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,435 @@
+# -*- 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")
+    
+    collectCoverage = "--cover" in argv
+    if collectCoverage:
+        argv.remove("--cover")
+    coverageErase = "--cover-erase" in argv
+    if coverageErase:
+        argv.remove("--cover-erase")
+    if "--cover-file" in argv:
+        index = argv.index("--cover-file")
+        covDataFile = argv[index + 1]
+        del argv[index:index + 2]
+    else:
+        covDataFile = ""
+    
+    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))
+    
+    # setup test coverage
+    if collectCoverage:
+        if not covDataFile:
+            if discover:
+                covname = os.path.join(discoveryStart, "test")
+            elif testFileName:
+                covname = os.path.splitext(
+                    os.path.abspath(testFileName))[0]
+            else:
+                covname = "test"
+            covDataFile = "{0}.coverage".format(covname)
+        if not os.path.isabs(covDataFile):
+            covDataFile = os.path.abspath(covDataFile)
+        
+        sys.path.insert(
+            2,
+            os.path.abspath(os.path.join(
+                os.path.dirname(__file__), "..", "..", "DebugClients", "Python"
+            ))
+        )
+        from DebugClients.Python.coverage import Coverage
+        cover = Coverage(data_file=covDataFile)
+        if coverageErase:
+            cover.erase()
+        cover.start()
+    else:
+        cover = None
+    
+    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)
+    
+    testResult = EricTestResult(writer, failfast)
+    startTestRun = getattr(testResult, 'startTestRun', None)
+    if startTestRun is not None:
+        startTestRun()
+    try:
+        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	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,27 @@
+# -*- 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 = {
+    "MicroPython": (
+        UnittestExecutor.name,
+        PytestExecutor.name,
+    ),
+    "Python3": (
+        UnittestExecutor.name,
+        PytestExecutor.name,
+    ),
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/Testing/TestResultsTree.py	Thu May 19 14:40:15 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	Thu May 19 14:40:15 2022 +0200
@@ -0,0 +1,1073 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a widget to orchestrate unit test execution.
+"""
+
+import contextlib
+import enum
+import locale
+import os
+
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QCoreApplication
+from PyQt6.QtWidgets import (
+    QAbstractButton, QComboBox, QDialogButtonBox, QWidget
+)
+
+from EricWidgets import EricMessageBox
+from EricWidgets.EricApplication import ericApp
+from EricWidgets.EricMainWindow import EricMainWindow
+from EricWidgets.EricPathPicker import EricPathPickerModes
+
+from .Ui_TestingWidget import Ui_TestingWidget
+
+from .TestResultsTree import TestResultsModel, TestResultsTreeView
+from .Interfaces import Frameworks
+from .Interfaces.TestExecutorBase import (
+    TestConfig, TestResult, TestResultCategory
+)
+from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry
+
+import Preferences
+import UI.PixmapCache
+
+from Globals import (
+    recentNameTestDiscoverHistory, recentNameTestFileHistory,
+    recentNameTestNameHistory, recentNameTestFramework,
+    recentNameTestEnvironment
+)
+
+
+class TestingWidgetModes(enum.Enum):
+    """
+    Class defining the various modes of the testing widget.
+    """
+    IDLE = 0            # idle, no test were run yet
+    RUNNING = 1         # test run being performed
+    STOPPED = 2         # test run finished
+
+
+class TestingWidget(QWidget, Ui_TestingWidget):
+    """
+    Class implementing a widget to orchestrate unit test execution.
+    
+    @signal testFile(str, int, bool) emitted to show the source of a
+       test file
+    @signal testRunStopped() emitted after a test run has finished
+    """
+    testFile = pyqtSignal(str, int, bool)
+    testRunStopped = pyqtSignal()
+    
+    def __init__(self, testfile=None, parent=None):
+        """
+        Constructor
+        
+        @param testfile file name of the test to load
+        @type str
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.__resultsModel = TestResultsModel(self)
+        self.__resultsModel.summary.connect(self.__setStatusLabel)
+        self.__resultsTree = TestResultsTreeView(self)
+        self.__resultsTree.setModel(self.__resultsModel)
+        self.__resultsTree.goto.connect(self.__showSource)
+        self.resultsGroupBox.layout().addWidget(self.__resultsTree)
+        
+        self.versionsButton.setIcon(
+            UI.PixmapCache.getIcon("info"))
+        self.clearHistoriesButton.setIcon(
+            UI.PixmapCache.getIcon("clearPrivateData"))
+        
+        self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
+        self.testsuitePicker.setInsertPolicy(
+            QComboBox.InsertPolicy.InsertAtTop)
+        self.testsuitePicker.setSizeAdjustPolicy(
+            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
+        
+        self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
+        self.discoveryPicker.setInsertPolicy(
+            QComboBox.InsertPolicy.InsertAtTop)
+        self.discoveryPicker.setSizeAdjustPolicy(
+            QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
+        
+        self.testComboBox.lineEdit().setClearButtonEnabled(True)
+        
+        # create some more dialog buttons for orchestration
+        self.__showCoverageButton = self.buttonBox.addButton(
+            self.tr("Show Coverage..."),
+            QDialogButtonBox.ButtonRole.ActionRole)
+        self.__showCoverageButton.setToolTip(
+            self.tr("Show code coverage in a new dialog"))
+        self.__showCoverageButton.setWhatsThis(self.tr(
+            """<b>Show Coverage...</b>"""
+            """<p>This button opens a dialog containing the collected code"""
+            """ coverage data.</p>"""))
+        
+        self.__startButton = self.buttonBox.addButton(
+            self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
+        
+        self.__startButton.setToolTip(self.tr(
+            "Start the selected testsuite"))
+        self.__startButton.setWhatsThis(self.tr(
+            """<b>Start Test</b>"""
+            """<p>This button starts the test run.</p>"""))
+        
+        self.__startFailedButton = self.buttonBox.addButton(
+            self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__startFailedButton.setToolTip(
+            self.tr("Reruns failed tests of the selected testsuite"))
+        self.__startFailedButton.setWhatsThis(self.tr(
+            """<b>Rerun Failed</b>"""
+            """<p>This button reruns all failed tests of the most recent"""
+            """ test run.</p>"""))
+        
+        self.__stopButton = self.buttonBox.addButton(
+            self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__stopButton.setToolTip(self.tr("Stop the running test"))
+        self.__stopButton.setWhatsThis(self.tr(
+            """<b>Stop Test</b>"""
+            """<p>This button stops a running test.</p>"""))
+        
+        self.setWindowFlags(
+            self.windowFlags() |
+            Qt.WindowType.WindowContextHelpButtonHint
+        )
+        self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
+        self.setWindowTitle(self.tr("Testing"))
+        
+        try:
+            # we are called from within the eric IDE
+            self.__venvManager = ericApp().getObject("VirtualEnvManager")
+            self.__project = ericApp().getObject("Project")
+            self.__project.projectOpened.connect(self.__projectOpened)
+            self.__project.projectClosed.connect(self.__projectClosed)
+        except KeyError:
+            # we were called as a standalone application
+            from VirtualEnv.VirtualenvManager import VirtualenvManager
+            self.__venvManager = VirtualenvManager(self)
+            self.__venvManager.virtualEnvironmentAdded.connect(
+                self.__populateVenvComboBox)
+            self.__venvManager.virtualEnvironmentRemoved.connect(
+                self.__populateVenvComboBox)
+            self.__venvManager.virtualEnvironmentChanged.connect(
+                self.__populateVenvComboBox)
+            
+            self.__project = None
+        
+        self.__discoverHistory = []
+        self.__fileHistory = []
+        self.__testNameHistory = []
+        self.__recentFramework = ""
+        self.__recentEnvironment = ""
+        self.__failedTests = []
+        
+        self.__coverageFile = ""
+        self.__coverageDialog = None
+        
+        self.__editors = []
+        self.__testExecutor = None
+        
+        # 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, forProject=False):
+        """
+        Public slot to set the given test file as the current one.
+        
+        @param testFile path of the test file
+        @type str
+        @param forProject flag indicating that this call is for a project
+            (defaults to False)
+        @type bool (optional)
+        """
+        if testFile:
+            self.__insertTestFile(testFile)
+        
+        self.discoverCheckBox.setChecked(forProject or not bool(testFile))
+        
+        self.tabWidget.setCurrentIndex(0)
+    
+    @pyqtSlot(str)
+    def __insertTestFile(self, prog):
+        """
+        Private slot to insert a test file name into the testsuitePicker
+        object.
+        
+        @param prog test file name to be inserted
+        @type str
+        """
+        self.__insertHistory(self.testsuitePicker, self.__fileHistory,
+                             prog)
+    
+    @pyqtSlot(str)
+    def __insertTestName(self, testName):
+        """
+        Private slot to insert a test name into the testComboBox object.
+        
+        @param testName name of the test to be inserted
+        @type str
+        """
+        self.__insertHistory(self.testComboBox, self.__testNameHistory,
+                             testName)
+    
+    def __loadRecent(self):
+        """
+        Private method to load the most recently used lists.
+        """
+        Preferences.Prefs.rsettings.sync()
+        
+        # 1. recently selected test framework and virtual environment
+        self.__recentEnvironment = Preferences.Prefs.rsettings.value(
+            recentNameTestEnvironment, "")
+        self.__recentFramework = Preferences.Prefs.rsettings.value(
+            recentNameTestFramework, "")
+        
+        # 2. discovery history
+        self.__discoverHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestDiscoverHistory)
+        if rs is not None:
+            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
+            self.__discoverHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+        
+        # 3. test file history
+        self.__fileHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestFileHistory)
+        if rs is not None:
+            recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
+            self.__fileHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+        
+        # 4. test name history
+        self.__testNameHistory = []
+        rs = Preferences.Prefs.rsettings.value(
+            recentNameTestNameHistory)
+        if rs is not None:
+            recent = [n for n in Preferences.toList(rs) if n]
+            self.__testNameHistory = recent[
+                :Preferences.getDebugger("RecentNumber")]
+    
+    def __saveRecent(self):
+        """
+        Private method to save the most recently used lists.
+        """
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestEnvironment, self.__recentEnvironment)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestFramework, self.__recentFramework)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestDiscoverHistory, self.__discoverHistory)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestFileHistory, self.__fileHistory)
+        Preferences.Prefs.rsettings.setValue(
+            recentNameTestNameHistory, self.__testNameHistory)
+        
+        Preferences.Prefs.rsettings.sync()
+    
+    @pyqtSlot()
+    def clearRecent(self):
+        """
+        Public slot to clear the recently used lists.
+        """
+        # clear histories
+        self.__discoverHistory = []
+        self.__fileHistory = []
+        self.__testNameHistory = []
+        
+        # clear widgets with histories
+        self.discoveryPicker.clear()
+        self.testsuitePicker.clear()
+        self.testComboBox.clear()
+        
+        # sync histories
+        self.__saveRecent()
+    
+    @pyqtSlot()
+    def __resetResults(self):
+        """
+        Private slot to reset the test results tab and data.
+        """
+        self.__totalCount = 0
+        self.__runCount = 0
+        
+        self.progressCounterRunCount.setText("0")
+        self.progressCounterRemCount.setText("0")
+        self.progressProgressBar.setMaximum(100)
+        self.progressProgressBar.setValue(0)
+        
+        self.statusLabel.clear()
+        
+        self.__resultsModel.clear()
+        self.__updateButtonBoxButtons()
+    
+    @pyqtSlot()
+    def __updateButtonBoxButtons(self):
+        """
+        Private slot to update the state of the buttons of the button box.
+        """
+        failedAvailable = bool(self.__resultsModel.getFailedTests())
+        
+        # Start button
+        if self.__mode in (
+            TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
+        ):
+            self.__startButton.setEnabled(
+                bool(self.venvComboBox.currentText()) and
+                bool(self.frameworkComboBox.currentText()) and
+                (
+                    (self.discoverCheckBox.isChecked() and
+                     bool(self.discoveryPicker.currentText())) or
+                    bool(self.testsuitePicker.currentText())
+                )
+            )
+            self.__startButton.setDefault(
+                self.__mode == TestingWidgetModes.IDLE or
+                not failedAvailable
+            )
+        else:
+            self.__startButton.setEnabled(False)
+            self.__startButton.setDefault(False)
+        
+        # Start Failed button
+        self.__startFailedButton.setEnabled(
+            self.__mode == TestingWidgetModes.STOPPED and
+            failedAvailable
+        )
+        self.__startFailedButton.setDefault(
+            self.__mode == TestingWidgetModes.STOPPED and
+            failedAvailable
+        )
+        
+        # Stop button
+        self.__stopButton.setEnabled(
+            self.__mode == TestingWidgetModes.RUNNING)
+        self.__stopButton.setDefault(
+            self.__mode == TestingWidgetModes.RUNNING)
+        
+        # Code coverage button
+        self.__showCoverageButton.setEnabled(
+            self.__mode == TestingWidgetModes.STOPPED and
+            bool(self.__coverageFile) and
+                (
+                    (self.discoverCheckBox.isChecked() and
+                     bool(self.discoveryPicker.currentText())) or
+                    bool(self.testsuitePicker.currentText())
+                )
+        )
+        
+        # Close button
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Close
+        ).setEnabled(self.__mode in (
+            TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
+        ))
+    
+    @pyqtSlot()
+    def __updateProgress(self):
+        """
+        Private slot update the progress indicators.
+        """
+        self.progressCounterRunCount.setText(
+            str(self.__runCount))
+        self.progressCounterRemCount.setText(
+            str(self.__totalCount - self.__runCount))
+        self.progressProgressBar.setMaximum(self.__totalCount)
+        self.progressProgressBar.setValue(self.__runCount)
+    
+    @pyqtSlot()
+    def __setIdleMode(self):
+        """
+        Private slot to switch the widget to idle mode.
+        """
+        self.__mode = TestingWidgetModes.IDLE
+        self.__updateButtonBoxButtons()
+        self.progressGroupBox.hide()
+        self.tabWidget.setCurrentIndex(0)
+    
+    @pyqtSlot()
+    def __setRunningMode(self):
+        """
+        Private slot to switch the widget to running mode.
+        """
+        self.__mode = TestingWidgetModes.RUNNING
+        
+        self.__totalCount = 0
+        self.__runCount = 0
+        
+        self.__coverageFile = ""
+        
+        self.sbLabel.setText(self.tr("Running"))
+        self.tabWidget.setCurrentIndex(1)
+        self.__updateButtonBoxButtons()
+        self.__updateProgress()
+        
+        self.progressGroupBox.show()
+    
+    @pyqtSlot()
+    def __setStoppedMode(self):
+        """
+        Private slot to switch the widget to stopped mode.
+        """
+        self.__mode = TestingWidgetModes.STOPPED
+        if self.__totalCount == 0:
+            self.progressProgressBar.setMaximum(100)
+        
+        self.progressGroupBox.hide()
+        
+        self.__updateButtonBoxButtons()
+        
+        self.testRunStopped.emit()
+        
+        self.raise_()
+        self.activateWindow()
+    
+    @pyqtSlot(bool)
+    def on_discoverCheckBox_toggled(self, checked):
+        """
+        Private slot handling state changes of the 'discover' checkbox.
+        
+        @param checked state of the checkbox
+        @type bool
+        """
+        if not bool(self.discoveryPicker.currentText()):
+            if self.__project and self.__project.isOpen():
+                self.__insertDiscovery(self.__project.getProjectPath())
+            else:
+                self.__insertDiscovery(
+                    Preferences.getMultiProject("Workspace"))
+        
+        self.__resetResults()
+    
+    @pyqtSlot()
+    def on_testsuitePicker_aboutToShowPathPickerDialog(self):
+        """
+        Private slot called before the test file selection dialog is shown.
+        """
+        if self.__project:
+            # we were called from within eric
+            py3Extensions = ' '.join([
+                "*{0}".format(ext)
+                for ext in
+                ericApp().getObject("DebugServer").getExtensions('Python3')
+            ])
+            fileFilter = self.tr(
+                "Python3 Files ({0});;All Files (*)"
+            ).format(py3Extensions)
+        else:
+            # standalone application
+            fileFilter = self.tr("Python Files (*.py);;All Files (*)")
+        self.testsuitePicker.setFilters(fileFilter)
+        
+        defaultDirectory = (
+            self.__project.getProjectPath()
+            if self.__project and self.__project.isOpen() else
+            Preferences.getMultiProject("Workspace")
+        )
+        if not defaultDirectory:
+            defaultDirectory = os.path.expanduser("~")
+        self.testsuitePicker.setDefaultDirectory(defaultDirectory)
+    
+    @pyqtSlot(QAbstractButton)
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+        
+        @param button button that was clicked
+        @type QAbstractButton
+        """
+        if button == self.__startButton:
+            self.startTests()
+            self.__saveRecent()
+        elif button == self.__stopButton:
+            self.__stopTests()
+        elif button == self.__startFailedButton:
+            self.startTests(failedOnly=True)
+        elif button == self.__showCoverageButton:
+            self.__showCoverageDialog()
+    
+    @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()
+        
+        if self.__project:
+            mainScript = self.__project.getMainScript(True)
+            coverageFile = os.path.splitext(mainScript)[0] + ".coverage"
+        else:
+            coverageFile = ""
+        interpreter = self.__venvManager.getVirtualenvInterpreter(
+            self.__recentEnvironment)
+        config = TestConfig(
+            interpreter=interpreter,
+            discover=discover,
+            discoveryStart=discoveryStart,
+            testFilename=testFileName,
+            testName=testName,
+            failFast=self.failfastCheckBox.isChecked(),
+            failedOnly=failedOnly,
+            collectCoverage=self.coverageCheckBox.isChecked(),
+            eraseCoverage=self.coverageEraseCheckBox.isChecked(),
+            coverageFile=coverageFile,
+        )
+        
+        self.__testExecutor = self.__frameworkRegistry.createExecutor(
+            self.__recentFramework, self)
+        self.__testExecutor.collected.connect(self.__testsCollected)
+        self.__testExecutor.collectError.connect(self.__testsCollectError)
+        self.__testExecutor.startTest.connect(self.__testStarted)
+        self.__testExecutor.testResult.connect(self.__processTestResult)
+        self.__testExecutor.testFinished.connect(self.__testProcessFinished)
+        self.__testExecutor.testRunFinished.connect(self.__testRunFinished)
+        self.__testExecutor.stop.connect(self.__testsStopped)
+        self.__testExecutor.coverageDataSaved.connect(self.__coverageData)
+        self.__testExecutor.testRunAboutToBeStarted.connect(
+            self.__testRunAboutToBeStarted)
+        
+        self.__setRunningMode()
+        self.__testExecutor.start(config, [])
+    
+    @pyqtSlot()
+    def __stopTests(self):
+        """
+        Private slot to stop the current test run.
+        """
+        self.__testExecutor.stopIfRunning()
+    
+    @pyqtSlot(list)
+    def __testsCollected(self, testNames):
+        """
+        Private slot handling the 'collected' signal of the executor.
+        
+        @param testNames list of tuples containing the test id 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.