Integrated the new testing widget into the eric IDE (compared to as a standalone app) and implemented the 'Show Source' functionality. unittest

Mon, 16 May 2022 17:22:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 16 May 2022 17:22:43 +0200
branch
unittest
changeset 9065
39405e6eba20
parent 9064
339bb8c8007d
child 9066
a219ade50f7c

Integrated the new testing widget into the eric IDE (compared to as a standalone app) and implemented the 'Show Source' functionality.

eric7/Project/Project.py file | annotate | diff | comparison | revisions
eric7/Project/PropertiesDialog.py file | annotate | diff | comparison | revisions
eric7/Project/PropertiesDialog.ui file | annotate | diff | comparison | revisions
eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/PytestExecutor.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/PytestRunner.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UnittestExecutor.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/__init__.py file | annotate | diff | comparison | revisions
eric7/Unittest/UTTestResultsTree.py file | annotate | diff | comparison | revisions
eric7/Unittest/UnittestWidget.py file | annotate | diff | comparison | revisions
--- a/eric7/Project/Project.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Project/Project.py	Mon May 16 17:22:43 2022 +0200
@@ -525,6 +525,7 @@
             },
             "EOL": -1,
             "DOCSTRING": "",
+            "TESTING_FRAMEWORK": "",
         }
         
         self.__initDebugProperties()
@@ -3766,6 +3767,18 @@
         
         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
--- a/eric7/Project/PropertiesDialog.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Project/PropertiesDialog.py	Mon May 16 17:22:43 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 Unittest.Interfaces import FrameworkNames
+
 import Utilities
 import Preferences
 import UI.PixmapCache
@@ -53,6 +56,10 @@
         ):
             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
         self.transPropertiesDlg = None
@@ -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"))
@@ -335,3 +346,7 @@
         self.project.pdata["DOCSTRING"] = (
             self.docstringStyleComboBox.currentData()
         )
+        
+        self.project.pdata["TESTING_FRAMEWORK"] = (
+            self.testingFrameworkComboBox.currentData()
+        )
--- a/eric7/Project/PropertiesDialog.ui	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Project/PropertiesDialog.ui	Mon May 16 17:22:43 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/UI/UserInterface.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/UI/UserInterface.py	Mon May 16 17:22:43 2022 +0200
@@ -341,7 +341,7 @@
         # set a few dialog members for non-modal dialogs created on demand
         self.programsDialog = None
         self.shortcutsDialog = None
-        self.unittestDialog = None
+        self.__unittestWidget = None
         self.findFileNameDialog = None
         self.diffDlg = None
         self.compareDlg = None
@@ -2520,8 +2520,9 @@
         self.utDialogAct.setStatusTip(self.tr('Start unittest dialog'))
         self.utDialogAct.setWhatsThis(self.tr(
             """<b>Unittest</b>"""
-            """<p>Perform unit tests. The dialog gives you the"""
-            """ ability to select and run a unittest suite.</p>"""
+            """<p>Perform unit tests. The dialog gives the"""
+            """ ability to select and run a unittest suite or"""
+            """auto discover them.</p>"""
         ))
         self.utDialogAct.triggered.connect(self.__unittest)
         self.actions.append(self.utDialogAct)
@@ -5332,24 +5333,24 @@
         if dlg.exec() == QDialog.DialogCode.Accepted:
             self.toolGroups, self.currentToolGroup = dlg.getToolGroups()
     
-    # TODO: adjust to new unit test framework (without debugger)
     def __createUnitTestDialog(self):
         """
         Private slot to generate the unit test dialog on demand.
         """
-        if self.unittestDialog is None:
-            from PyUnit.UnittestDialog import UnittestDialog
-            self.unittestDialog = UnittestDialog(
-                None, self.__debugServer, self)
-            self.unittestDialog.unittestFile.connect(
+        if self.__unittestWidget is None:
+            from Unittest.UnittestWidget import UnittestWidget
+            self.__unittestWidget = UnittestWidget()
+            self.__unittestWidget.unittestFile.connect(
                 self.viewmanager.setFileLine)
-            self.unittestDialog.unittestStopped.connect(self.__unittestStopped)
+            self.__unittestWidget.unittestStopped.connect(
+                self.__unittestStopped)
     
     def __unittestStopped(self):
         """
         Private slot to handle the end of a unit test run.
         """
-        self.utRerunFailedAct.setEnabled(self.unittestDialog.hasFailedTests())
+        self.utRerunFailedAct.setEnabled(
+            self.__unittestWidget.hasFailedTests())
         self.utRestartAct.setEnabled(True)
     
     def __unittest(self):
@@ -5357,50 +5358,48 @@
         Private slot for displaying the unittest dialog.
         """
         self.__createUnitTestDialog()
-        self.unittestDialog.show()
-        self.unittestDialog.raise_()
+        self.__unittestWidget.show()
+        self.__unittestWidget.raise_()
     
     @pyqtSlot()
     @pyqtSlot(str)
-    def __unittestScript(self, prog=None):
+    def __unittestScript(self, testFile=None):
         """
         Private slot for displaying the unittest dialog and run the current
         script.
         
-        @param prog the python program to be opened
-        """
-        if prog is None:
+        @param testFile file containing the unit tests to be run
+        @type str
+        """
+        if testFile is None:
             aw = self.viewmanager.activeWindow()
             fn = aw.getFileName()
             tfn = Utilities.getTestFileName(fn)
             if os.path.exists(tfn):
-                prog = tfn
+                testFile = tfn
             else:
-                prog = fn
+                testFile = fn
         
         self.__unittest()
-        self.unittestDialog.setProjectMode(False)
-        self.unittestDialog.insertProg(prog)
+        self.__unittestWidget.setTestFile(testFile)
         self.utRestartAct.setEnabled(False)
         self.utRerunFailedAct.setEnabled(False)
-        
+    
+    @pyqtSlot()
     def __unittestProject(self):
         """
         Private slot for displaying the unittest dialog and run the current
         project.
         """
-        prog = None
+        testFile = None
         fn = self.project.getMainScript(True)
         if fn:
             tfn = Utilities.getTestFileName(fn)
             if os.path.exists(tfn):
-                prog = tfn
-            else:
-                prog = fn
+                testFile = tfn
         
         self.__unittest()
-        self.unittestDialog.setProjectMode(True)
-        self.unittestDialog.insertProg(prog)
+        self.__unittestWidget.setTestFile(testFile)
         self.utRestartAct.setEnabled(False)
         self.utRerunFailedAct.setEnabled(False)
         
@@ -5410,7 +5409,7 @@
         unit test.
         """
         self.__unittest()
-        self.unittestDialog.startTests()
+        self.__unittestWidget.startTests()
         
     def __unittestRerunFailed(self):
         """
@@ -5418,7 +5417,7 @@
         of the last run.
         """
         self.__unittest()
-        self.unittestDialog.startTests(failedOnly=True)
+        self.__unittestWidget.startTests(failedOnly=True)
     
     @pyqtSlot()
     @pyqtSlot(str)
@@ -6836,11 +6835,11 @@
                 self.shell.clearAllHistories()
             if unittests:
                 # clear the unit test histories
-                if self.unittestDialog is None:
-                    from PyUnit.UnittestDialog import clearSavedHistories
+                if self.__unittestWidget is None:
+                    from Unittest.UnittestWidget import clearSavedHistories
                     clearSavedHistories()
                 else:
-                    self.unittestDialog.clearRecent()
+                    self.__unittestWidget.clearRecent()
             if vcs:
                 # clear the VCS related histories
                 self.pluginManager.clearPluginsPrivateData("version_control")
--- a/eric7/Unittest/Interfaces/PytestExecutor.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/Interfaces/PytestExecutor.py	Mon May 16 17:22:43 2022 +0200
@@ -16,6 +16,7 @@
 from .UTExecutorBase import UTExecutorBase
 
 
+# TODO: implement 'pytest' support in PytestExecutor
 class PytestExecutor(UTExecutorBase):
     """
     Class implementing the executor for the 'pytest' framework.
--- a/eric7/Unittest/Interfaces/PytestRunner.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/Interfaces/PytestRunner.py	Mon May 16 17:22:43 2022 +0200
@@ -10,6 +10,8 @@
 import json
 import sys
 
+# TODO: implement 'pytest' support in PytestRunner
+
 
 class GetPluginVersionsPlugin():
     """
--- a/eric7/Unittest/Interfaces/UnittestExecutor.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/Interfaces/UnittestExecutor.py	Mon May 16 17:22:43 2022 +0200
@@ -174,7 +174,7 @@
         
         # test result
         elif data["event"] == "result":
-            fn, ln = None, None
+            filename, lineno = None, None
             tracebackLines = []
             if "traceback" in data:
                 # get the error info
@@ -186,7 +186,8 @@
                     if fmatch:
                         break
                 if fmatch:
-                    fn, ln = fmatch.group(1, 2)
+                    filename = fmatch.group(1)
+                    lineno = int(fmatch.group(2))
                 
             if "shortmsg" in data:
                 message = data["shortmsg"]
@@ -206,8 +207,8 @@
                 duration=(
                     data["duration_ms"] if "duration_ms" in data else None
                 ),
-                filename=fn,
-                lineno=ln,
+                filename=filename,
+                lineno=lineno,
                 subtestResult=data["subtest"] if "subtest" in data else False
             ))
         
--- a/eric7/Unittest/Interfaces/__init__.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/Interfaces/__init__.py	Mon May 16 17:22:43 2022 +0200
@@ -14,3 +14,8 @@
     UnittestExecutor,
     PytestExecutor,
 )
+
+FrameworkNames = (
+    UnittestExecutor.name,
+    PytestExecutor.name,
+)
--- a/eric7/Unittest/UTTestResultsTree.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/UTTestResultsTree.py	Mon May 16 17:22:43 2022 +0200
@@ -16,10 +16,11 @@
 from operator import attrgetter
 
 from PyQt6.QtCore import (
-    pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex
+    pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication,
+    QModelIndex, QPoint
 )
 from PyQt6.QtGui import QBrush, QColor
-from PyQt6.QtWidgets import QTreeView
+from PyQt6.QtWidgets import QMenu, QTreeView
 
 from EricWidgets.EricApplication import ericApp
 
@@ -43,7 +44,7 @@
         QCoreApplication.translate("TestResultsModel", "Status"),
         QCoreApplication.translate("TestResultsModel", "Name"),
         QCoreApplication.translate("TestResultsModel", "Message"),
-        QCoreApplication.translate("TestResultsModel", "Duration (ms)"),
+        QCoreApplication.translate("TestResultsModel", "Duration [ms]"),
     ]
     
     StatusColumn = 0
@@ -142,7 +143,7 @@
                 )
         elif role == Qt.ItemDataRole.ToolTipRole:
             if idx == TopLevelId and column == TestResultsModel.NameColumn:
-                return self.testresults[row].name
+                return self.__testResults[row].name
         elif role == Qt.ItemDataRole.FontRole:
             if idx != TopLevelId:
                 return Preferences.getEditorOtherFonts("MonospacedFont")
@@ -156,7 +157,7 @@
                 return Qt.AlignmentFlag.AlignRight
         elif role == Qt.ItemDataRole.UserRole:      # __IGNORE_WARNING_Y102__
             if idx == TopLevelId:
-                testresult = self.testresults[row]
+                testresult = self.__testResults[row]
                 return (testresult.filename, testresult.lineno)
         
         return None
@@ -427,8 +428,11 @@
         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(
@@ -479,17 +483,6 @@
             bottomRight = bottomRight.parent()
         self.spanFirstColumn(topLeft.row(), bottomRight.row())
     
-    @pyqtSlot(QModelIndex)
-    def __gotoTestDefinition(self, index):
-        """
-        Private slot to show the test definition.
-        
-        @param index index for the double-clicked item
-        @type QModelIndex
-        """
-        # TODO: not implemented yet (__gotoTestDefinition)
-        pass
-    
     def resizeColumns(self):
         """
         Public method to resize the columns to their contents.
@@ -514,6 +507,105 @@
             index = model.index(row, 0)
             for i in range(model.rowCount(index)):
                 self.setFirstColumnSpanned(i, index, True)
+    
+    def __canonicalIndex(self, index):
+        """
+        Private method to create the canonical index for a given index.
+        
+        The canonical index is the index of the first column of the test
+        result entry (i.e. the top-level item). If the index is invalid,
+        None is returned.
+        
+        @param index index to determine the canonical index for
+        @type QModelIndex
+        @return index of the firt column of the associated top-level item index
+        @rtype QModelIndex
+        """
+        if not index.isValid():
+            return None
+        
+        while index.parent().isValid():  # find the top-level node
+            index = index.parent()
+        index = index.sibling(index.row(), 0)  # go to first column
+        return index
+    
+    @pyqtSlot(QModelIndex)
+    def __gotoTestDefinition(self, index):
+        """
+        Private slot to show the test definition.
+        
+        @param index index for the double-clicked item
+        @type QModelIndex
+        """
+        cindex = self.__canonicalIndex(index)
+        filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole)
+        if filename is not None:
+            if lineno is None:
+                lineno = 1
+            self.goto.emit(filename, lineno)
+    
+    @pyqtSlot(QPoint)
+    def __showContextMenu(self, pos):
+        """
+        Private slot to show the context menu.
+        
+        @param pos relative position for the context menu
+        @type QPoint
+        """
+        index = self.indexAt(pos)
+        cindex = self.__canonicalIndex(index)
+        
+        contextMenu = (
+            self.__createContextMenu(cindex)
+            if cindex else
+            self.__createBackgroundContextMenu()
+        )
+        contextMenu.exec(self.mapToGlobal(pos))
+    
+    def __createContextMenu(self, index):
+        """
+        Private method to create a context menu for the item pointed to by the
+        given index.
+        
+        @param index index of the item
+        @type QModelIndex
+        @return created context menu
+        @rtype QMenu
+        """
+        menu = QMenu(self)
+        if self.isExpanded(index):
+            menu.addAction(self.tr("Collapse"),
+                           lambda: self.collapse(index))
+        else:
+            act = menu.addAction(self.tr("Expand"),
+                                 lambda: self.expand(index))
+            act.setEnabled(self.model().hasChildren(index))
+        menu.addSeparator()
+        
+        act = menu.addAction(self.tr("Show Source"),
+                             lambda: self.__gotoTestDefinition(index))
+        act.setEnabled(
+            self.model().data(index, Qt.ItemDataRole.UserRole) is not None
+        )
+        menu.addSeparator()
+        
+        menu.addAction(self.tr("Collapse All"), self.collapseAll)
+        menu.addAction(self.tr("Expand All"), self.expandAll)
+        
+        return menu
+    
+    def __createBackgroundContextMenu(self):
+        """
+        Private method to create a context menu for the background.
+        
+        @return created context menu
+        @rtype QMenu
+        """
+        menu = QMenu(self)
+        menu.addAction(self.tr("Collapse All"), self.collapseAll)
+        menu.addAction(self.tr("Expand All"), self.expandAll)
+        
+        return menu
 
 #
 # eflag: noqa = M821, M822
--- a/eric7/Unittest/UnittestWidget.py	Sun May 15 18:08:31 2022 +0200
+++ b/eric7/Unittest/UnittestWidget.py	Mon May 16 17:22:43 2022 +0200
@@ -7,11 +7,12 @@
 Module implementing a widget to orchestrate unit test execution.
 """
 
+import contextlib
 import enum
 import locale
 import os
 
-from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QCoreApplication
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QCoreApplication
 from PyQt6.QtWidgets import (
     QAbstractButton, QComboBox, QDialogButtonBox, QWidget
 )
@@ -54,7 +55,14 @@
 class UnittestWidget(QWidget, Ui_UnittestWidget):
     """
     Class implementing a widget to orchestrate unit test execution.
+    
+    @signal unittestFile(str, int, bool) emitted to show the source of a
+        unittest file
+    @signal unittestStopped() emitted after a unit test was run
     """
+    unittestFile = pyqtSignal(str, int, bool)
+    unittestStopped = pyqtSignal()
+    
     def __init__(self, testfile=None, parent=None):
         """
         Constructor
@@ -71,6 +79,7 @@
         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(
@@ -125,17 +134,24 @@
         self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
         self.setWindowTitle(self.tr("Unittest"))
         
-        from VirtualEnv.VirtualenvManager import VirtualenvManager
-        self.__venvManager = VirtualenvManager(self)
-        self.__venvManager.virtualEnvironmentAdded.connect(
-            self.__populateVenvComboBox)
-        self.__venvManager.virtualEnvironmentRemoved.connect(
-            self.__populateVenvComboBox)
-        self.__venvManager.virtualEnvironmentChanged.connect(
-            self.__populateVenvComboBox)
-        
-        # TODO: implement project mode
-        self.__forProject = False
+        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 = []
@@ -150,8 +166,6 @@
         # connect some signals
         self.frameworkComboBox.currentIndexChanged.connect(
             self.__resetResults)
-        self.discoverCheckBox.toggled.connect(
-            self.__resetResults)
         self.discoveryPicker.editTextChanged.connect(
             self.__resetResults)
         self.testsuitePicker.editTextChanged.connect(
@@ -168,14 +182,14 @@
         self.__loadRecent()
         self.__populateVenvComboBox()
         
-        if self.__forProject:
-            project = ericApp().getObject("Project")
-            if project.isOpen():
-                self.__insertDiscovery(project.getProjectPath())
-            else:
-                self.__insertDiscovery("")
+        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("")
         
@@ -195,10 +209,7 @@
         self.venvComboBox.addItem("")
         self.venvComboBox.addItems(
             sorted(self.__venvManager.getVirtualenvNames()))
-        index = self.venvComboBox.findText(currentText)
-        if index < 0:
-            index = 0
-        self.venvComboBox.setCurrentIndex(index)
+        self.venvComboBox.setCurrentText(currentText)
     
     def __populateTestFrameworkComboBox(self):
         """
@@ -240,6 +251,15 @@
         """
         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).
@@ -261,8 +281,6 @@
         @param item item to be inserted
         @type str
         """
-        current = widget.currentText()
-        
         # prepend the given directory to the discovery picker
         if item is None:
             item = ""
@@ -271,9 +289,7 @@
         history.insert(0, item)
         widget.clear()
         widget.addItems(history)
-        
-        if current:
-            widget.setEditText(current)
+        widget.setEditText(item)
     
     @pyqtSlot(str)
     def __insertDiscovery(self, start):
@@ -288,6 +304,21 @@
                              start)
     
     @pyqtSlot(str)
+    def setTestFile(self, testFile):
+        """
+        Public slot to set the given test file as the current one.
+        
+        @param testFile path of the test file
+        @type str
+        """
+        if testFile:
+            self.__insertTestFile(testFile)
+        
+        self.discoverCheckBox.setChecked(not bool(testFile))
+        
+        self.tabWidget.setCurrentIndex(0)
+    
+    @pyqtSlot(str)
     def __insertTestFile(self, prog):
         """
         Private slot to insert a test file name into the testsuitePicker
@@ -508,34 +539,55 @@
         
         self.__updateButtonBoxButtons()
         
+        self.unittestStopped.emit()
+        
         self.raise_()
         self.activateWindow()
     
+    @pyqtSlot(bool)
+    def on_discoverCheckBox_toggled(self, checked):
+        """
+        Private slot handling state changes of the 'discover' checkbox.
+        
+        @param checked state of the checkbox
+        @type bool
+        """
+        if not bool(self.discoveryPicker.currentText()):
+            if self.__project and self.__project.isOpen():
+                self.__insertDiscovery(self.__project.getProjectPath())
+            else:
+                self.__insertDiscovery(
+                    Preferences.getMultiProject("Workspace"))
+        
+        self.__resetResults()
+    
     @pyqtSlot()
     def on_testsuitePicker_aboutToShowPathPickerDialog(self):
         """
         Private slot called before the test file selection dialog is shown.
         """
-        # TODO: implement eric-ide mode
-#        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 (*)")
+        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 = Preferences.getMultiProject("Workspace")
+        defaultDirectory = (
+            self.__project.getProjectPath()
+            if self.__project and self.__project.isOpen() else
+            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(QAbstractButton)
@@ -848,6 +900,73 @@
         @type str
         """
         self.statusLabel.setText(f"<b>{statusText}</b>")
+    
+    @pyqtSlot()
+    def __projectOpened(self):
+        """
+        Private slot to handle a project being opened.
+        """
+        self.venvComboBox.setCurrentText(self.__project.getProjectVenv())
+        self.frameworkComboBox.setCurrentText(
+            self.__project.getProjectTestingFramework())
+        self.__insertDiscovery(self.__project.getProjectPath())
+    
+    @pyqtSlot()
+    def __projectClosed(self):
+        """
+        Private slot to handle a project being closed.
+        """
+        self.venvComboBox.setCurrentText("")
+        self.frameworkComboBox.setCurrentText("")
+        self.__insertDiscovery("")
+    
+    @pyqtSlot(str, int)
+    def __showSource(self, filename, lineno):
+        """
+        Private slot to show the source of a traceback in an editor.
+        
+        @param filename file name of the file to be shown
+        @type str
+        @param lineno line number to go to in the file
+        @type int
+        """
+        if self.__project:
+            # running as part of eric IDE
+            self.unittestFile.emit(filename, lineno, True)
+        else:
+            self.__openEditor(filename, lineno)
+    
+    def __openEditor(self, filename, linenumber):
+        """
+        Private method to open an editor window for the given file.
+        
+        Note: This method opens an editor window when the unittest dialog
+        is called as a standalone application.
+        
+        @param filename path of the file to be opened
+        @type str
+        @param linenumber line number to place the cursor at
+        @type int
+        """
+        from QScintilla.MiniEditor import MiniEditor
+        editor = MiniEditor(filename, "Python3", self)
+        editor.gotoLine(linenumber)
+        editor.show()
+        
+        self.__editors.append(editor)
+    
+    def closeEvent(self, event):
+        """
+        Protected method to handle the close event.
+        
+        @param event close event
+        @type QCloseEvent
+        """
+        event.accept()
+        
+        for editor in self.__editors:
+            with contextlib.suppress(Exception):
+                editor.close()
 
 
 class UnittestWindow(EricMainWindow):

eric ide

mercurial