Mon, 16 May 2022 17:22:43 +0200
Integrated the new testing widget into the eric IDE (compared to as a standalone app) and implemented the 'Show Source' functionality.
--- 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>&Progr. Language:</string> + <string>&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&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&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 &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 &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>&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>&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 &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>&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>&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>&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):