Sun, 15 May 2022 18:08:31 +0200
Implemented the "Rerun Failed" functionality for the new unit test interface.
--- 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">