Corrected the code dealing with subtests. unittest

Sat, 14 May 2022 18:56:52 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 14 May 2022 18:56:52 +0200
branch
unittest
changeset 9063
f1d7dd7ae471
parent 9062
7f27bf3b50c3
child 9064
339bb8c8007d

Corrected the code dealing with subtests.

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
--- a/eric7/Unittest/Interfaces/UTExecutorBase.py	Fri May 13 17:23:21 2022 +0200
+++ b/eric7/Unittest/Interfaces/UTExecutorBase.py	Sat May 14 18:56:52 2022 +0200
@@ -33,16 +33,17 @@
     """
     Class containing the test result data.
     """
-    category: ResultCategory    # result category
-    status: str                 # test status
-    name: str                   # test name
-    id: str                     # test id
-    description: str = ""       # short description of test
-    message: str = ""           # short result message
-    extra: list = None          # additional information text
-    duration: float = None      # test duration
-    filename: str = None        # file name of a failed test
-    lineno: int = None          # line number of a failed test
+    category: ResultCategory        # result category
+    status: str                     # test status
+    name: str                       # test name
+    id: str                         # test id
+    description: str = ""           # short description of test
+    message: str = ""               # short result message
+    extra: list = None              # additional information text
+    duration: float = None          # test duration
+    filename: str = None            # file name of a failed test
+    lineno: int = None              # line number of a failed test
+    subtestResult: bool = False     # flag indicating the result of a subtest
 
 
 @dataclass
@@ -50,14 +51,14 @@
     """
     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
-    collectCoverage: bool       # coverage collection flag
-    eraseCoverage: bool         # erase coverage data first
+    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
+    collectCoverage: bool           # coverage collection flag
+    eraseCoverage: bool             # erase coverage data first
 
 
 class UTExecutorBase(QObject):
--- a/eric7/Unittest/Interfaces/UnittestExecutor.py	Fri May 13 17:23:21 2022 +0200
+++ b/eric7/Unittest/Interfaces/UnittestExecutor.py	Sat May 14 18:56:52 2022 +0200
@@ -195,9 +195,12 @@
                 description=data["description"],
                 message=message,
                 extra=tracebackLines,
-                duration=data["duration_ms"],
+                duration=(
+                    data["duration_ms"] if "duration_ms" in data else None
+                ),
                 filename=fn,
                 lineno=ln,
+                subtestResult=data["subtest"] if "subtest" in data else False
             ))
         
         # test run finished
--- a/eric7/Unittest/Interfaces/UnittestRunner.py	Fri May 13 17:23:21 2022 +0200
+++ b/eric7/Unittest/Interfaces/UnittestRunner.py	Sat May 14 18:56:52 2022 +0200
@@ -77,36 +77,6 @@
             "traceback": tracebackLines,
         })
     
-    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"
-            )
-            
-            self.__currentTestStatus.update({
-                "status": status,
-                "name": str(subtest),
-                "traceback": tracebackLines,
-            })
-            
-            if self.failfast:
-                self.stop()
-    
     def addSkip(self, test, reason):
         """
         Public method called if a test was skipped.
@@ -152,6 +122,52 @@
         
         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.
@@ -168,6 +184,7 @@
             "name": str(test),
             "id": test.id(),
             "description": test.shortDescription(),
+            "subtest": False,
         }
         
         self.__writer.write({
--- a/eric7/Unittest/UTTestResultsTree.py	Fri May 13 17:23:21 2022 +0200
+++ b/eric7/Unittest/UTTestResultsTree.py	Sat May 14 18:56:52 2022 +0200
@@ -11,6 +11,8 @@
 import contextlib
 import copy
 import locale
+
+from collections import Counter
 from operator import attrgetter
 
 from PyQt6.QtCore import (
@@ -31,7 +33,12 @@
 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"),
@@ -129,7 +136,7 @@
             elif column == TestResultsModel.DurationColumn:
                 duration = self.__testResults[row].duration
                 return (
-                    ''
+                    ""
                     if duration is None else
                     locale.format_string("%.2f", duration, grouping=True)
                 )
@@ -289,6 +296,8 @@
         self.beginResetModel()
         self.__testResults = copy.deepcopy(testResults)
         self.endResetModel()
+        
+        self.summary.emit(self.__summary())
     
     def addTestResults(self, testResults):
         """
@@ -303,6 +312,8 @@
         self.beginInsertRows(QModelIndex(), firstRow, lastRow)
         self.__testResults.extend(testResults)
         self.endInsertRows()
+        
+        self.summary.emit(self.__summary())
     
     def updateTestResults(self, testResults):
         """
@@ -314,6 +325,8 @@
         minIndex = None
         maxIndex = None
         
+        testResultsToBeAdded = []
+        
         for testResult in testResults:
             for (index, currentResult) in enumerate(self.__testResults):
                 if currentResult.id == testResult.id:
@@ -324,12 +337,52 @@
                     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 __summary(self):
+        """
+        Private method to generate a test results summary text.
+        
+        @return test results summary text
+        @rtype str
+        """
+        if len(self.__testResults) == 0:
+            return self.tr("No results to show")
+        
+        counts = Counter(res.category for res in self.__testResults)
+        if all(
+            counts[category] == 0
+            for category in (ResultCategory.FAIL, ResultCategory.OK,
+                             ResultCategory.SKIP)
+        ):
+            return self.tr("Collected %n test(s)", "", len(self.__testResults))
+        
+        return self.tr(
+            "%n test(s)/subtest(s) total, {0} failed, {1} passed,"
+            " {2} skipped, {3} pending",
+            "", len(self.__testResults)
+        ).format(
+            counts[ResultCategory.FAIL],
+            counts[ResultCategory.OK],
+            counts[ResultCategory.SKIP],
+            counts[ResultCategory.PENDING]
+        )
 
 
 class TestResultsTreeView(QTreeView):
--- a/eric7/Unittest/UnittestWidget.py	Fri May 13 17:23:21 2022 +0200
+++ b/eric7/Unittest/UnittestWidget.py	Sat May 14 18:56:52 2022 +0200
@@ -68,6 +68,7 @@
         self.setupUi(self)
         
         self.__resultsModel = TestResultsModel(self)
+        self.__resultsModel.summary.connect(self.__setStatusLabel)
         self.__resultsTree = TestResultsTreeView(self)
         self.__resultsTree.setModel(self.__resultsModel)
         self.resultsGroupBox.layout().addWidget(self.__resultsTree)
@@ -688,7 +689,8 @@
         @param result test result object
         @type UTTestResult
         """
-        self.__runCount += 1
+        if not result.subtestResult:
+            self.__runCount += 1
         self.__updateProgress()
         
         self.__resultsModel.updateTestResults([result])
@@ -745,6 +747,16 @@
         self.__coverageFile = coverageFile
         
         # TODO: implement the handling of the 'Show Coverage' button
+    
+    @pyqtSlot(str)
+    def __setStatusLabel(self, statusText):
+        """
+        Private slot to set the status label to the text sent by the model.
+        
+        @param statusText text to be shown
+        @type str
+        """
+        self.statusLabel.setText(f"<b>{statusText}</b>")
 
 
 class UnittestWindow(EricMainWindow):

eric ide

mercurial