Implemented the "Rerun Failed" functionality for the new unit test interface. unittest

Sun, 15 May 2022 18:08:31 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 15 May 2022 18:08:31 +0200
branch
unittest
changeset 9064
339bb8c8007d
parent 9063
f1d7dd7ae471
child 9065
39405e6eba20

Implemented the "Rerun Failed" functionality for the new unit test interface.

eric7/UI/UserInterface.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UTExecutorBase.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UnittestExecutor.py file | annotate | diff | comparison | revisions
eric7/Unittest/Interfaces/UnittestRunner.py file | annotate | diff | comparison | revisions
eric7/Unittest/UTTestResultsTree.py file | annotate | diff | comparison | revisions
eric7/Unittest/UnittestWidget.py file | annotate | diff | comparison | revisions
eric7/Unittest/UnittestWidget.ui file | annotate | diff | comparison | revisions
--- a/eric7/UI/UserInterface.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/UI/UserInterface.py	Sun May 15 18:08:31 2022 +0200
@@ -5331,7 +5331,8 @@
             self.toolGroups, self.currentToolGroup, self)
         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.
--- a/eric7/Unittest/Interfaces/UTExecutorBase.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/Interfaces/UTExecutorBase.py	Sun May 15 18:08:31 2022 +0200
@@ -57,6 +57,7 @@
     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
 
@@ -77,6 +78,8 @@
     @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.
@@ -88,6 +91,7 @@
     startTest = pyqtSignal(tuple)
     testResult = pyqtSignal(UTTestResult)
     testFinished = pyqtSignal(list, str)
+    testRunAboutToBeStarted = pyqtSignal()
     testRunFinished = pyqtSignal(int, float)
     stop = pyqtSignal()
     coverageDataSaved = pyqtSignal(str)
@@ -204,6 +208,7 @@
         )
         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:
--- a/eric7/Unittest/Interfaces/UnittestExecutor.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/Interfaces/UnittestExecutor.py	Sun May 15 18:08:31 2022 +0200
@@ -54,6 +54,8 @@
             "unexpected success": self.tr("Unexpected Success"),
             "success": self.tr("Success"),
         }
+        
+        self.__testWidget = testWidget
     
     def getVersions(self, interpreter):
         """
@@ -108,7 +110,13 @@
             if config.eraseCoverage:
                 args.append("--cover-erase")
         
-        if config.testFilename and config.testName:
+        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)
         
--- a/eric7/Unittest/Interfaces/UnittestRunner.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/Interfaces/UnittestRunner.py	Sun May 15 18:08:31 2022 +0200
@@ -293,13 +293,22 @@
     if coverageErase:
         argv.remove("--cover-erase")
     
-    if not discover:
-        testFileName, testName = argv[:2]
-        del argv[:2]
+    if argv and argv[0] == "--failed-only":
+        if discover:
+            testFileName = ""
+            failed = argv[1:]
+        else:
+            testFileName = argv[1]
+            failed = argv[2:]
     else:
-        testFileName = testName = ""
-    
-    testCases = argv[:]
+        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)))
@@ -308,7 +317,7 @@
     
     try:
         testLoader = unittest.TestLoader()
-        if discover:
+        if discover and not failed:
             if testCases:
                 test = testLoader.loadTestsFromNames(testCases)
             else:
@@ -319,18 +328,15 @@
                     os.path.basename(testFileName))[0])
             else:
                 module = None
-            # TODO: implement 'failed only'
-#            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)
+            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({
--- a/eric7/Unittest/UTTestResultsTree.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/UTTestResultsTree.py	Sun May 15 18:08:31 2022 +0200
@@ -243,6 +243,8 @@
         self.beginResetModel()
         self.__testResults.clear()
         self.endResetModel()
+        
+        self.summary.emit("")
     
     def sort(self, column, order):
         """
@@ -355,6 +357,21 @@
         if testResultsToBeAdded:
             self.addTestResults(testResultsToBeAdded)
     
+    def getFailedTests(self):
+        """
+        Public method to extract the test ids of all failed tests.
+        
+        @return test ids of all failed tests
+        @rtype list of str
+        """
+        failedIds = [
+            res.id for res in self.__testResults if (
+                res.category == ResultCategory.FAIL and
+                not res.subtestResult
+            )
+        ]
+        return failedIds
+    
     def __summary(self):
         """
         Private method to generate a test results summary text.
--- a/eric7/Unittest/UnittestWidget.py	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/UnittestWidget.py	Sun May 15 18:08:31 2022 +0200
@@ -100,29 +100,23 @@
             "Start the selected testsuite"))
         self.__startButton.setWhatsThis(self.tr(
             """<b>Start Test</b>"""
-            """<p>This button starts the selected testsuite.</p>"""))
+            """<p>This button starts the test run.</p>"""))
         
-        # TODO: implement "Rerun Failed"
-##        self.__startFailedButton = self.buttonBox.addButton(
-##            self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
-##        self.__startFailedButton.setToolTip(
-##            self.tr("Reruns failed tests of the selected testsuite"))
-##        self.__startFailedButton.setWhatsThis(self.tr(
-##            """<b>Rerun Failed</b>"""
-##            """<p>This button reruns all failed tests of the selected"""
-##            """ testsuite.</p>"""))
-##        
+        self.__startFailedButton = self.buttonBox.addButton(
+            self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
+        self.__startFailedButton.setToolTip(
+            self.tr("Reruns failed tests of the selected testsuite"))
+        self.__startFailedButton.setWhatsThis(self.tr(
+            """<b>Rerun Failed</b>"""
+            """<p>This button reruns all failed tests of the most recent"""
+            """ test run.</p>"""))
+        
         self.__stopButton = self.buttonBox.addButton(
             self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
         self.__stopButton.setToolTip(self.tr("Stop the running unittest"))
         self.__stopButton.setWhatsThis(self.tr(
             """<b>Stop Test</b>"""
-            """<p>This button stops a running unittest.</p>"""))
-        
-        self.__stopButton.setEnabled(False)
-        self.__startButton.setDefault(True)
-        self.__startButton.setEnabled(False)
-##        self.__startFailedButton.setEnabled(False)
+            """<p>This button stops a running test.</p>"""))
         
         self.setWindowFlags(
             self.windowFlags() |
@@ -148,21 +142,22 @@
         self.__testNameHistory = []
         self.__recentFramework = ""
         self.__recentEnvironment = ""
-        
-        self.__failedTests = set()
+        self.__failedTests = []
         
         self.__editors = []
         self.__testExecutor = None
         
         # connect some signals
         self.frameworkComboBox.currentIndexChanged.connect(
-            self.__updateButtonBoxButtons)
+            self.__resetResults)
         self.discoverCheckBox.toggled.connect(
-            self.__updateButtonBoxButtons)
+            self.__resetResults)
         self.discoveryPicker.editTextChanged.connect(
-            self.__updateButtonBoxButtons)
+            self.__resetResults)
         self.testsuitePicker.editTextChanged.connect(
-            self.__updateButtonBoxButtons)
+            self.__resetResults)
+        self.testComboBox.editTextChanged.connect(
+            self.__resetResults)
         
         self.__frameworkRegistry = UTFrameworkRegistry()
         for framework in Frameworks:
@@ -235,6 +230,25 @@
             
             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 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):
         """
@@ -370,11 +384,30 @@
         # 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 method to update the state of the buttons of the button box.
+        Private slot to update the state of the buttons of the button box.
         """
-        failedAvailable = bool(self.__failedTests)
+        failedAvailable = bool(self.__resultsModel.getFailedTests())
         
         # Start button
         if self.__mode in (
@@ -398,7 +431,14 @@
             self.__startButton.setDefault(False)
         
         # Start Failed button
-        # TODO: not implemented yet (Start Failed button)
+        self.__startFailedButton.setEnabled(
+            self.__mode == UnittestWidgetModes.STOPPED and
+            failedAvailable
+        )
+        self.__startFailedButton.setDefault(
+            self.__mode == UnittestWidgetModes.STOPPED and
+            failedAvailable
+        )
         
         # Stop button
         self.__stopButton.setEnabled(
@@ -413,9 +453,10 @@
             UnittestWidgetModes.IDLE, UnittestWidgetModes.STOPPED
         ))
     
+    @pyqtSlot()
     def __updateProgress(self):
         """
-        Private method update the progress indicators.
+        Private slot update the progress indicators.
         """
         self.progressCounterRunCount.setText(
             str(self.__runCount))
@@ -424,17 +465,20 @@
         self.progressProgressBar.setMaximum(self.__totalCount)
         self.progressProgressBar.setValue(self.__runCount)
     
+    @pyqtSlot()
     def __setIdleMode(self):
         """
-        Private method to switch the widget to idle mode.
+        Private slot to switch the widget to idle mode.
         """
         self.__mode = UnittestWidgetModes.IDLE
         self.__updateButtonBoxButtons()
+        self.progressGroupBox.hide()
         self.tabWidget.setCurrentIndex(0)
     
+    @pyqtSlot()
     def __setRunningMode(self):
         """
-        Private method to switch the widget to running mode.
+        Private slot to switch the widget to running mode.
         """
         self.__mode = UnittestWidgetModes.RUNNING
         
@@ -449,19 +493,51 @@
         self.__updateButtonBoxButtons()
         self.__updateProgress()
         
-        self.__resultsModel.clear()
+        self.progressGroupBox.show()
     
+    @pyqtSlot()
     def __setStoppedMode(self):
         """
-        Private method to switch the widget to stopped mode.
+        Private slot to switch the widget to stopped mode.
         """
         self.__mode = UnittestWidgetModes.STOPPED
+        if self.__totalCount == 0:
+            self.progressProgressBar.setMaximum(100)
+        
+        self.progressGroupBox.hide()
         
         self.__updateButtonBoxButtons()
         
         self.raise_()
         self.activateWindow()
     
+    @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 (*)")
+        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(QAbstractButton)
     def on_buttonBox_clicked(self, button):
         """
@@ -475,8 +551,8 @@
             self.__saveRecent()
         elif button == self.__stopButton:
             self.__stopTests()
-#        elif button == self.__startFailedButton:
-#            self.startTests(failedOnly=True)
+        elif button == self.__startFailedButton:
+            self.startTests(failedOnly=True)
     
     @pyqtSlot(int)
     def on_venvComboBox_currentIndexChanged(self, index):
@@ -548,6 +624,11 @@
         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()
@@ -579,11 +660,11 @@
             testFilename=testFileName,
             testName=testName,
             failFast=self.failfastCheckBox.isChecked(),
+            failedOnly=failedOnly,
             collectCoverage=self.coverageCheckBox.isChecked(),
             eraseCoverage=self.coverageEraseCheckBox.isChecked(),
         )
         
-        self.__resultsModel.clear()
         self.__testExecutor = self.__frameworkRegistry.createExecutor(
             self.__recentFramework, self)
         self.__testExecutor.collected.connect(self.__testsCollected)
@@ -594,6 +675,8 @@
         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, [])
@@ -736,6 +819,14 @@
         
         self.__setStoppedMode()
     
+    @pyqtSlot()
+    def __testRunAboutToBeStarted(self):
+        """
+        Private slot to handle the 'testRunAboutToBeStarted' signal of the
+        executor.
+        """
+        self.__resultsModel.clear()
+    
     @pyqtSlot(str)
     def __coverageData(self, coverageFile):
         """
--- a/eric7/Unittest/UnittestWidget.ui	Sat May 14 18:56:52 2022 +0200
+++ b/eric7/Unittest/UnittestWidget.ui	Sun May 15 18:08:31 2022 +0200
@@ -17,7 +17,7 @@
    <item>
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
-      <number>1</number>
+      <number>0</number>
      </property>
      <widget class="QWidget" name="parametersTab">
       <attribute name="title">

eric ide

mercurial