Sat, 14 May 2022 18:56:52 +0200
Corrected the code dealing with subtests.
--- 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):