src/eric7/Testing/TestingWidget.py

branch
eric7-maintenance
changeset 10460
3b34efa2857c
parent 10349
df7edc29cbfb
parent 10453
16235de22ee7
child 10694
f46c1e224e8a
equal deleted inserted replaced
10366:411df92e881f 10460:3b34efa2857c
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 2
3 # Copyright (c) 2022 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> 3 # Copyright (c) 2022 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
4 # 4 #
5 5
6 """ 6 """
7 Module implementing a widget to orchestrate unit test execution. 7 Module implementing a widget to orchestrate unit test execution.
8 """ 8 """
10 import contextlib 10 import contextlib
11 import enum 11 import enum
12 import locale 12 import locale
13 import os 13 import os
14 14
15 from PyQt6.QtCore import QCoreApplication, QEvent, Qt, pyqtSignal, pyqtSlot 15 from PyQt6.QtCore import QCoreApplication, QEvent, QPoint, Qt, pyqtSignal, pyqtSlot
16 from PyQt6.QtWidgets import QAbstractButton, QComboBox, QDialogButtonBox, QWidget 16 from PyQt6.QtWidgets import (
17 QAbstractButton,
18 QComboBox,
19 QDialogButtonBox,
20 QMenu,
21 QTreeWidgetItem,
22 QWidget,
23 )
17 24
18 from eric7 import Preferences 25 from eric7 import Preferences
19 from eric7.DataViews.PyCoverageDialog import PyCoverageDialog 26 from eric7.DataViews.PyCoverageDialog import PyCoverageDialog
20 from eric7.EricGui import EricPixmapCache 27 from eric7.EricGui import EricPixmapCache
21 from eric7.EricWidgets import EricMessageBox 28 from eric7.EricWidgets import EricMessageBox
31 ) 38 )
32 39
33 from .Interfaces import Frameworks 40 from .Interfaces import Frameworks
34 from .Interfaces.TestExecutorBase import TestConfig, TestResult, TestResultCategory 41 from .Interfaces.TestExecutorBase import TestConfig, TestResult, TestResultCategory
35 from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry 42 from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry
36 from .TestResultsTree import TestResultsModel, TestResultsTreeView 43 from .TestResultsTree import (
44 TestResultsFilterModel,
45 TestResultsModel,
46 TestResultsTreeView,
47 )
37 from .Ui_TestingWidget import Ui_TestingWidget 48 from .Ui_TestingWidget import Ui_TestingWidget
38 49
39 50
40 class TestingWidgetModes(enum.Enum): 51 class TestingWidgetModes(enum.Enum):
41 """ 52 """
43 """ 54 """
44 55
45 IDLE = 0 # idle, no test were run yet 56 IDLE = 0 # idle, no test were run yet
46 RUNNING = 1 # test run being performed 57 RUNNING = 1 # test run being performed
47 STOPPED = 2 # test run finished 58 STOPPED = 2 # test run finished
59 DISCOVERY = 3 # discovery of tests being performed
48 60
49 61
50 class TestingWidget(QWidget, Ui_TestingWidget): 62 class TestingWidget(QWidget, Ui_TestingWidget):
51 """ 63 """
52 Class implementing a widget to orchestrate unit test execution. 64 Class implementing a widget to orchestrate unit test execution.
57 """ 69 """
58 70
59 testFile = pyqtSignal(str, int, bool) 71 testFile = pyqtSignal(str, int, bool)
60 testRunStopped = pyqtSignal() 72 testRunStopped = pyqtSignal()
61 73
74 TestCaseNameRole = Qt.ItemDataRole.UserRole
75 TestCaseFileRole = Qt.ItemDataRole.UserRole + 1
76 TestCaseLinenoRole = Qt.ItemDataRole.UserRole + 2
77 TestCaseIdRole = Qt.ItemDataRole.UserRole + 3
78
62 def __init__(self, testfile=None, parent=None): 79 def __init__(self, testfile=None, parent=None):
63 """ 80 """
64 Constructor 81 Constructor
65 82
66 @param testfile file name of the test to load 83 @param testfile file name of the test to load
71 super().__init__(parent) 88 super().__init__(parent)
72 self.setupUi(self) 89 self.setupUi(self)
73 90
74 self.__resultsModel = TestResultsModel(self) 91 self.__resultsModel = TestResultsModel(self)
75 self.__resultsModel.summary.connect(self.__setStatusLabel) 92 self.__resultsModel.summary.connect(self.__setStatusLabel)
93 self.__resultFilterModel = TestResultsFilterModel(self)
94 self.__resultFilterModel.setSourceModel(self.__resultsModel)
76 self.__resultsTree = TestResultsTreeView(self) 95 self.__resultsTree = TestResultsTreeView(self)
77 self.__resultsTree.setModel(self.__resultsModel) 96 self.__resultsTree.setModel(self.__resultFilterModel)
78 self.__resultsTree.goto.connect(self.__showSource) 97 self.__resultsTree.goto.connect(self.__showSource)
79 self.resultsGroupBox.layout().addWidget(self.__resultsTree) 98 self.resultsGroupBox.layout().addWidget(self.__resultsTree)
80 99
81 self.versionsButton.setIcon(EricPixmapCache.getIcon("info")) 100 self.versionsButton.setIcon(EricPixmapCache.getIcon("info"))
82 self.clearHistoriesButton.setIcon(EricPixmapCache.getIcon("clearPrivateData")) 101 self.clearHistoriesButton.setIcon(EricPixmapCache.getIcon("clearPrivateData"))
96 115
97 self.testComboBox.completer().setCaseSensitivity( 116 self.testComboBox.completer().setCaseSensitivity(
98 Qt.CaseSensitivity.CaseSensitive 117 Qt.CaseSensitivity.CaseSensitive
99 ) 118 )
100 self.testComboBox.lineEdit().setClearButtonEnabled(True) 119 self.testComboBox.lineEdit().setClearButtonEnabled(True)
120
121 self.__allFilter = self.tr("<all>")
101 122
102 # create some more dialog buttons for orchestration 123 # create some more dialog buttons for orchestration
103 self.__showLogButton = self.buttonBox.addButton( 124 self.__showLogButton = self.buttonBox.addButton(
104 self.tr("Show Output..."), QDialogButtonBox.ButtonRole.ActionRole 125 self.tr("Show Output..."), QDialogButtonBox.ButtonRole.ActionRole
105 ) 126 )
126 """<p>This button opens a dialog containing the collected code""" 147 """<p>This button opens a dialog containing the collected code"""
127 """ coverage data.</p>""" 148 """ coverage data.</p>"""
128 ) 149 )
129 ) 150 )
130 151
152 self.__discoverButton = self.buttonBox.addButton(
153 self.tr("Discover"), QDialogButtonBox.ButtonRole.ActionRole
154 )
155 self.__discoverButton.setToolTip(self.tr("Discover Tests"))
156 self.__discoverButton.setWhatsThis(
157 self.tr(
158 """<b>Discover Tests</b>"""
159 """<p>This button starts a discovery of available tests.</p>"""
160 )
161 )
162
131 self.__startButton = self.buttonBox.addButton( 163 self.__startButton = self.buttonBox.addButton(
132 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole 164 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole
133 ) 165 )
134 166
135 self.__startButton.setToolTip(self.tr("Start the selected testsuite")) 167 self.__startButton.setToolTip(self.tr("Start the selected test suite"))
136 self.__startButton.setWhatsThis( 168 self.__startButton.setWhatsThis(
137 self.tr("""<b>Start Test</b><p>This button starts the test run.</p>""") 169 self.tr("""<b>Start Test</b><p>This button starts the test run.</p>""")
138 ) 170 )
139 171
140 self.__startFailedButton = self.buttonBox.addButton( 172 self.__startFailedButton = self.buttonBox.addButton(
169 # we are called from within the eric IDE 201 # we are called from within the eric IDE
170 self.__venvManager = ericApp().getObject("VirtualEnvManager") 202 self.__venvManager = ericApp().getObject("VirtualEnvManager")
171 self.__project = ericApp().getObject("Project") 203 self.__project = ericApp().getObject("Project")
172 self.__project.projectOpened.connect(self.__projectOpened) 204 self.__project.projectOpened.connect(self.__projectOpened)
173 self.__project.projectClosed.connect(self.__projectClosed) 205 self.__project.projectClosed.connect(self.__projectClosed)
174 self.__projectEnvironmentMarker = self.tr("<project>")
175 except KeyError: 206 except KeyError:
176 # we were called as a standalone application 207 # we were called as a standalone application
177 from eric7.VirtualEnv.VirtualenvManager import ( # __IGNORE_WARNING_I101__ 208 from eric7.VirtualEnv.VirtualenvManager import ( # __IGNORE_WARNING_I101__
178 VirtualenvManager, 209 VirtualenvManager,
179 ) 210 )
189 self.__populateVenvComboBox 220 self.__populateVenvComboBox
190 ) 221 )
191 ericApp().registerObject("VirtualEnvManager", self.__venvManager) 222 ericApp().registerObject("VirtualEnvManager", self.__venvManager)
192 223
193 self.__project = None 224 self.__project = None
194 self.__projectEnvironmentMarker = "" 225
226 self.debuggerCheckBox.setChecked(False)
227 self.debuggerCheckBox.setVisible(False)
195 228
196 self.__discoverHistory = [] 229 self.__discoverHistory = []
197 self.__fileHistory = [] 230 self.__fileHistory = []
198 self.__testNameHistory = [] 231 self.__testNameHistory = []
199 self.__recentFramework = "" 232 self.__recentFramework = ""
204 self.__coverageDialog = None 237 self.__coverageDialog = None
205 238
206 self.__editors = [] 239 self.__editors = []
207 self.__testExecutor = None 240 self.__testExecutor = None
208 self.__recentLog = "" 241 self.__recentLog = ""
242 self.__projectString = ""
209 243
210 self.__markersWindow = None 244 self.__markersWindow = None
245
246 self.__discoveryListContextMenu = QMenu(self.discoveryList)
247 self.__discoveryListContextMenu.addAction(
248 self.tr("Collapse All"), self.discoveryList.collapseAll
249 )
250 self.__discoveryListContextMenu.addAction(
251 self.tr("Expand All"), self.discoveryList.expandAll
252 )
211 253
212 # connect some signals 254 # connect some signals
213 self.discoveryPicker.editTextChanged.connect(self.__resetResults) 255 self.discoveryPicker.editTextChanged.connect(self.__resetResults)
214 self.testsuitePicker.editTextChanged.connect(self.__resetResults) 256 self.testsuitePicker.editTextChanged.connect(self.__resetResults)
215 self.testComboBox.editTextChanged.connect(self.__resetResults) 257 self.testComboBox.editTextChanged.connect(self.__resetResults)
246 @param venvName name of the virtual environment 288 @param venvName name of the virtual environment
247 @type str 289 @type str
248 @return path of the interpreter executable 290 @return path of the interpreter executable
249 @rtype str 291 @rtype str
250 """ 292 """
251 if self.__project and venvName == self.__projectEnvironmentMarker: 293 if (
294 self.__project
295 and venvName == ericApp().getObject("DebugUI").getProjectEnvironmentString()
296 ):
252 return self.__project.getProjectInterpreter() 297 return self.__project.getProjectInterpreter()
253 else: 298 else:
254 return self.__venvManager.getVirtualenvInterpreter(venvName) 299 return self.__venvManager.getVirtualenvInterpreter(venvName)
255 300
256 def __populateVenvComboBox(self): 301 def __populateVenvComboBox(self):
262 currentText = self.__recentEnvironment 307 currentText = self.__recentEnvironment
263 308
264 self.venvComboBox.clear() 309 self.venvComboBox.clear()
265 self.venvComboBox.addItem("") 310 self.venvComboBox.addItem("")
266 if self.__project and self.__project.isOpen(): 311 if self.__project and self.__project.isOpen():
267 self.venvComboBox.addItem(self.__projectEnvironmentMarker) 312 venvName = ericApp().getObject("DebugUI").getProjectEnvironmentString()
313 if venvName:
314 self.venvComboBox.addItem(venvName)
315 self.__projectString = venvName
268 self.venvComboBox.addItems(sorted(self.__venvManager.getVirtualenvNames())) 316 self.venvComboBox.addItems(sorted(self.__venvManager.getVirtualenvNames()))
269 self.venvComboBox.setCurrentText(currentText) 317 self.venvComboBox.setCurrentText(currentText)
270 318
271 def __populateTestFrameworkComboBox(self): 319 def __populateTestFrameworkComboBox(self):
272 """ 320 """
331 @param history array containing the history 379 @param history array containing the history
332 @type list of str 380 @type list of str
333 @param item item to be inserted 381 @param item item to be inserted
334 @type str 382 @type str
335 """ 383 """
336 # prepend the given directory to the discovery picker 384 if history and item != history[0]:
337 if item is None: 385 # prepend the given directory to the given widget
338 item = "" 386 if item is None:
339 if item in history: 387 item = ""
340 history.remove(item) 388 if item in history:
341 history.insert(0, item) 389 history.remove(item)
342 widget.clear() 390 history.insert(0, item)
343 widget.addItems(history) 391 widget.clear()
344 widget.setEditText(item) 392 widget.addItems(history)
393 widget.setEditText(item)
345 394
346 @pyqtSlot(str) 395 @pyqtSlot(str)
347 def __insertDiscovery(self, start): 396 def __insertDiscovery(self, start):
348 """ 397 """
349 Private slot to insert the discovery start directory into the 398 Private slot to insert the discovery start directory into the
493 def __updateButtonBoxButtons(self): 542 def __updateButtonBoxButtons(self):
494 """ 543 """
495 Private slot to update the state of the buttons of the button box. 544 Private slot to update the state of the buttons of the button box.
496 """ 545 """
497 failedAvailable = bool(self.__resultsModel.getFailedTests()) 546 failedAvailable = bool(self.__resultsModel.getFailedTests())
547
548 # Discover button
549 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED):
550 self.__discoverButton.setEnabled(
551 bool(self.venvComboBox.currentText())
552 and bool(self.frameworkComboBox.currentText())
553 and self.discoverCheckBox.isChecked()
554 and bool(self.discoveryPicker.currentText())
555 )
556 else:
557 self.__discoverButton.setEnabled(False)
558 self.__discoverButton.setDefault(False)
498 559
499 # Start button 560 # Start button
500 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED): 561 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED):
501 self.__startButton.setEnabled( 562 self.__startButton.setEnabled(
502 bool(self.venvComboBox.currentText()) 563 bool(self.venvComboBox.currentText())
567 self.__mode = TestingWidgetModes.IDLE 628 self.__mode = TestingWidgetModes.IDLE
568 self.__updateButtonBoxButtons() 629 self.__updateButtonBoxButtons()
569 self.progressGroupBox.hide() 630 self.progressGroupBox.hide()
570 self.tabWidget.setCurrentIndex(0) 631 self.tabWidget.setCurrentIndex(0)
571 632
633 self.raise_()
634 self.activateWindow()
635
636 @pyqtSlot()
637 def __setDiscoverMode(self):
638 """
639 Private slot to switch the widget to test discovery mode.
640 """
641 self.__mode = TestingWidgetModes.DISCOVERY
642
643 self.__totalCount = 0
644
645 self.tabWidget.setCurrentIndex(0)
646 self.__updateButtonBoxButtons()
647
572 @pyqtSlot() 648 @pyqtSlot()
573 def __setRunningMode(self): 649 def __setRunningMode(self):
574 """ 650 """
575 Private slot to switch the widget to running mode. 651 Private slot to switch the widget to running mode.
576 """ 652 """
621 self.__insertDiscovery(self.__project.getProjectPath()) 697 self.__insertDiscovery(self.__project.getProjectPath())
622 else: 698 else:
623 self.__insertDiscovery(Preferences.getMultiProject("Workspace")) 699 self.__insertDiscovery(Preferences.getMultiProject("Workspace"))
624 700
625 self.__resetResults() 701 self.__resetResults()
702
703 self.discoveryList.clear()
704
705 @pyqtSlot(str)
706 def on_discoveryPicker_editTextChanged(self, txt):
707 """
708 Private slot to handle a change of the discovery start directory.
709
710 @param txt new discovery start directory
711 @type str
712 """
713 self.discoveryList.clear()
626 714
627 @pyqtSlot() 715 @pyqtSlot()
628 def on_testsuitePicker_aboutToShowPathPickerDialog(self): 716 def on_testsuitePicker_aboutToShowPathPickerDialog(self):
629 """ 717 """
630 Private slot called before the test file selection dialog is shown. 718 Private slot called before the test file selection dialog is shown.
662 Private slot called by a button of the button box clicked. 750 Private slot called by a button of the button box clicked.
663 751
664 @param button button that was clicked 752 @param button button that was clicked
665 @type QAbstractButton 753 @type QAbstractButton
666 """ 754 """
755 if button == self.__discoverButton:
756 self.__discoverTests()
667 if button == self.__startButton: 757 if button == self.__startButton:
668 self.startTests() 758 self.startTests(debug=self.debuggerCheckBox.isChecked())
669 self.__saveRecent() 759 self.__saveRecent()
670 elif button == self.__stopButton: 760 elif button == self.__stopButton:
671 self.__stopTests() 761 self.__stopTests()
672 elif button == self.__startFailedButton: 762 elif button == self.__startFailedButton:
673 self.startTests(failedOnly=True) 763 self.startTests(failedOnly=True, debug=self.debuggerCheckBox.isChecked())
674 elif button == self.__showCoverageButton: 764 elif button == self.__showCoverageButton:
675 self.__showCoverageDialog() 765 self.__showCoverageDialog()
676 elif button == self.__showLogButton: 766 elif button == self.__showLogButton:
677 self.__showLogOutput() 767 self.__showLogOutput()
678 768
683 773
684 @param index index of the selected environment 774 @param index index of the selected environment
685 @type int 775 @type int
686 """ 776 """
687 self.__populateTestFrameworkComboBox() 777 self.__populateTestFrameworkComboBox()
778 self.discoveryList.clear()
779
688 self.__updateButtonBoxButtons() 780 self.__updateButtonBoxButtons()
689 781
690 self.versionsButton.setEnabled(bool(self.venvComboBox.currentText())) 782 self.versionsButton.setEnabled(bool(self.venvComboBox.currentText()))
691 783
692 self.__updateCoverage() 784 self.__updateCoverage()
701 """ 793 """
702 self.__resetResults() 794 self.__resetResults()
703 self.__updateCoverage() 795 self.__updateCoverage()
704 self.__updateMarkerSupport() 796 self.__updateMarkerSupport()
705 self.__updatePatternSupport() 797 self.__updatePatternSupport()
798 self.discoveryList.clear()
706 799
707 @pyqtSlot() 800 @pyqtSlot()
708 def __updateCoverage(self): 801 def __updateCoverage(self):
709 """ 802 """
710 Private slot to update the state of the coverage checkbox depending on 803 Private slot to update the state of the coverage checkbox depending on
803 venvName = self.venvComboBox.currentText() 896 venvName = self.venvComboBox.currentText()
804 if venvName: 897 if venvName:
805 headerText = self.tr("<h3>Versions of Frameworks and their Plugins</h3>") 898 headerText = self.tr("<h3>Versions of Frameworks and their Plugins</h3>")
806 versionsText = "" 899 versionsText = ""
807 interpreter = self.__determineInterpreter(venvName) 900 interpreter = self.__determineInterpreter(venvName)
808 for framework in sorted(self.__frameworkRegistry.getFrameworks().keys()): 901 for framework in sorted(self.__frameworkRegistry.getFrameworks()):
809 executor = self.__frameworkRegistry.createExecutor(framework, self) 902 executor = self.__frameworkRegistry.createExecutor(framework, self)
810 versions = executor.getVersions(interpreter) 903 versions = executor.getVersions(interpreter)
811 if versions: 904 if versions:
812 txt = "<p><strong>{0} {1}</strong>".format( 905 txt = "<p><strong>{0} {1}</strong>".format(
813 versions["name"], versions["version"] 906 versions["name"], versions["version"]
830 EricMessageBox.information( 923 EricMessageBox.information(
831 self, self.tr("Versions"), headerText + versionsText 924 self, self.tr("Versions"), headerText + versionsText
832 ) 925 )
833 926
834 @pyqtSlot() 927 @pyqtSlot()
835 def startTests(self, failedOnly=False): 928 def __discoverTests(self):
929 """
930 Private slot to discover tests but don't execute them.
931 """
932 if self.__mode in (TestingWidgetModes.RUNNING, TestingWidgetModes.DISCOVERY):
933 return
934
935 self.__recentLog = ""
936
937 environment = self.venvComboBox.currentText()
938 framework = self.frameworkComboBox.currentText()
939
940 discoveryStart = self.discoveryPicker.currentText()
941 if discoveryStart:
942 self.__insertDiscovery(discoveryStart)
943
944 self.sbLabel.setText(self.tr("Discovering Tests"))
945 QCoreApplication.processEvents()
946
947 interpreter = self.__determineInterpreter(environment)
948 config = TestConfig(
949 interpreter=interpreter,
950 discover=True,
951 discoveryStart=discoveryStart,
952 discoverOnly=True,
953 testNamePattern=self.testNamePatternEdit.text(),
954 testMarkerExpression=self.markerExpressionEdit.text(),
955 failFast=self.failfastCheckBox.isChecked(),
956 )
957
958 self.__testExecutor = self.__frameworkRegistry.createExecutor(framework, self)
959 self.__testExecutor.collected.connect(self.__testsDiscovered)
960 self.__testExecutor.collectError.connect(self.__testDiscoveryError)
961 self.__testExecutor.testFinished.connect(self.__testDiscoveryProcessFinished)
962 self.__testExecutor.discoveryAboutToBeStarted.connect(
963 self.__testDiscoveryAboutToBeStarted
964 )
965
966 self.__setDiscoverMode()
967 self.__testExecutor.discover(config, [])
968
969 @pyqtSlot()
970 def startTests(self, failedOnly=False, debug=False):
836 """ 971 """
837 Public slot to start the test run. 972 Public slot to start the test run.
838 973
839 @param failedOnly flag indicating to run only failed tests 974 @param failedOnly flag indicating to run only failed tests (defaults to False)
840 @type bool 975 @type bool (optional)
841 """ 976 @param debug flag indicating to start the test run with debugger support
842 if self.__mode == TestingWidgetModes.RUNNING: 977 (defaults to False)
978 @type bool (optional)
979 """
980 if self.__mode in (TestingWidgetModes.RUNNING, TestingWidgetModes.DISCOVERY):
843 return 981 return
844 982
845 self.__recentLog = "" 983 self.__recentLog = ""
846 984
847 self.__recentEnvironment = self.venvComboBox.currentText() 985 self.__recentEnvironment = self.venvComboBox.currentText()
874 os.path.splitext(mainScript)[0] + ".coverage" if mainScript else "" 1012 os.path.splitext(mainScript)[0] + ".coverage" if mainScript else ""
875 ) 1013 )
876 else: 1014 else:
877 coverageFile = "" 1015 coverageFile = ""
878 interpreter = self.__determineInterpreter(self.__recentEnvironment) 1016 interpreter = self.__determineInterpreter(self.__recentEnvironment)
1017
1018 testCases = self.__selectedTestCases()
1019 if not testCases and self.discoveryList.topLevelItemCount() > 0:
1020 ok = EricMessageBox.yesNo(
1021 self,
1022 self.tr("Running Tests"),
1023 self.tr("No test case has been selected. Shall all test cases be run?"),
1024 )
1025 if not ok:
1026 return
1027
879 config = TestConfig( 1028 config = TestConfig(
880 interpreter=interpreter, 1029 interpreter=interpreter,
881 discover=discover, 1030 discover=discover,
882 discoveryStart=discoveryStart, 1031 discoveryStart=discoveryStart,
1032 testCases=testCases,
883 testFilename=testFileName, 1033 testFilename=testFileName,
884 testName=testName, 1034 testName=testName,
885 testNamePattern=self.testNamePatternEdit.text(), 1035 testNamePattern=self.testNamePatternEdit.text(),
886 testMarkerExpression=self.markerExpressionEdit.text(), 1036 testMarkerExpression=self.markerExpressionEdit.text(),
887 failFast=self.failfastCheckBox.isChecked(), 1037 failFast=self.failfastCheckBox.isChecked(),
888 failedOnly=failedOnly, 1038 failedOnly=failedOnly,
889 collectCoverage=self.coverageCheckBox.isChecked(), 1039 collectCoverage=self.coverageCheckBox.isChecked(),
890 eraseCoverage=self.coverageEraseCheckBox.isChecked(), 1040 eraseCoverage=self.coverageEraseCheckBox.isChecked(),
891 coverageFile=coverageFile, 1041 coverageFile=coverageFile,
1042 venvName=self.__recentEnvironment,
892 ) 1043 )
893 1044
894 self.__testExecutor = self.__frameworkRegistry.createExecutor( 1045 self.__testExecutor = self.__frameworkRegistry.createExecutor(
895 self.__recentFramework, self 1046 self.__recentFramework, self
896 ) 1047 )
905 self.__testExecutor.testRunAboutToBeStarted.connect( 1056 self.__testExecutor.testRunAboutToBeStarted.connect(
906 self.__testRunAboutToBeStarted 1057 self.__testRunAboutToBeStarted
907 ) 1058 )
908 1059
909 self.__setRunningMode() 1060 self.__setRunningMode()
910 self.__testExecutor.start(config, []) 1061 if debug:
1062 self.__testExecutor.startDebug(config, [], ericApp().getObject("DebugUI"))
1063 else:
1064 self.__testExecutor.start(config, [])
911 1065
912 @pyqtSlot() 1066 @pyqtSlot()
913 def __stopTests(self): 1067 def __stopTests(self):
914 """ 1068 """
915 Private slot to stop the current test run. 1069 Private slot to stop the current test run.
920 def __testsCollected(self, testNames): 1074 def __testsCollected(self, testNames):
921 """ 1075 """
922 Private slot handling the 'collected' signal of the executor. 1076 Private slot handling the 'collected' signal of the executor.
923 1077
924 @param testNames list of tuples containing the test id, the test name 1078 @param testNames list of tuples containing the test id, the test name
925 and a description of collected tests 1079 a description, the file name, the line number and the test path as a list
926 @type list of tuple of (str, str, str) 1080 of collected tests
1081 @type list of tuple of (str, str, str, str, int, list)
927 """ 1082 """
928 testResults = [ 1083 testResults = [
929 TestResult( 1084 TestResult(
930 category=TestResultCategory.PENDING, 1085 category=TestResultCategory.PENDING,
931 status=self.tr("pending"), 1086 status=self.tr("pending"),
932 name=name, 1087 name=name,
933 id=id, 1088 id=id,
934 message=desc, 1089 message=desc,
935 ) 1090 filename=filename,
936 for id, name, desc in testNames 1091 lineno=lineno,
1092 )
1093 for id, name, desc, filename, lineno, _ in testNames
937 ] 1094 ]
938 self.__resultsModel.addTestResults(testResults) 1095 self.__resultsModel.addTestResults(testResults)
939 self.__resultsTree.resizeColumns() 1096 self.__resultsTree.resizeColumns()
940 1097
941 self.__totalCount += len(testResults) 1098 self.__totalCount += len(testResults)
1027 1184
1028 self.__setStoppedMode() 1185 self.__setStoppedMode()
1029 self.__testExecutor = None 1186 self.__testExecutor = None
1030 1187
1031 self.__adjustPendingState() 1188 self.__adjustPendingState()
1189 self.__updateStatusFilterComboBox()
1032 1190
1033 @pyqtSlot(int, float) 1191 @pyqtSlot(int, float)
1034 def __testRunFinished(self, noTests, duration): 1192 def __testRunFinished(self, noTests, duration):
1035 """ 1193 """
1036 Private slot to handle the 'testRunFinished' signal of the executor. 1194 Private slot to handle the 'testRunFinished' signal of the executor.
1062 """ 1220 """
1063 Private slot to handle the 'testRunAboutToBeStarted' signal of the 1221 Private slot to handle the 'testRunAboutToBeStarted' signal of the
1064 executor. 1222 executor.
1065 """ 1223 """
1066 self.__resultsModel.clear() 1224 self.__resultsModel.clear()
1225 self.statusFilterComboBox.clear()
1067 1226
1068 def __adjustPendingState(self): 1227 def __adjustPendingState(self):
1069 """ 1228 """
1070 Private method to change the status indicator of all still pending 1229 Private method to change the status indicator of all still pending
1071 tests to "not run". 1230 tests to "not run".
1134 @pyqtSlot() 1293 @pyqtSlot()
1135 def __projectOpened(self): 1294 def __projectOpened(self):
1136 """ 1295 """
1137 Private slot to handle a project being opened. 1296 Private slot to handle a project being opened.
1138 """ 1297 """
1139 self.venvComboBox.insertItem(1, self.__projectEnvironmentMarker) 1298 self.__projectString = (
1140 self.venvComboBox.setCurrentIndex(1) 1299 ericApp().getObject("DebugUI").getProjectEnvironmentString()
1300 )
1301
1302 if self.__projectString:
1303 # 1a. remove old project venv entries
1304 while (row := self.venvComboBox.findText(self.__projectString)) != -1:
1305 self.venvComboBox.removeItem(row)
1306
1307 # 1b. add a new project venv entry
1308 self.venvComboBox.insertItem(1, self.__projectString)
1309 self.venvComboBox.setCurrentIndex(1)
1310
1311 # 2. set some other project related stuff
1141 self.frameworkComboBox.setCurrentText( 1312 self.frameworkComboBox.setCurrentText(
1142 self.__project.getProjectTestingFramework() 1313 self.__project.getProjectTestingFramework()
1143 ) 1314 )
1144 self.__insertDiscovery(self.__project.getProjectPath()) 1315 self.__insertDiscovery(self.__project.getProjectPath())
1145 1316
1146 @pyqtSlot() 1317 @pyqtSlot()
1147 def __projectClosed(self): 1318 def __projectClosed(self):
1148 """ 1319 """
1149 Private slot to handle a project being closed. 1320 Private slot to handle a project being closed.
1150 """ 1321 """
1151 self.venvComboBox.removeItem(1) # <project> is always at index 1 1322 if self.__projectString:
1152 self.venvComboBox.setCurrentText("") 1323 while (row := self.venvComboBox.findText(self.__projectString)) != -1:
1324 self.venvComboBox.removeItem(row)
1325
1326 self.venvComboBox.setCurrentText("")
1327
1153 self.frameworkComboBox.setCurrentText("") 1328 self.frameworkComboBox.setCurrentText("")
1154 self.__insertDiscovery("") 1329 self.__insertDiscovery("")
1330
1331 # clear latest log assuming it was for a project test run
1332 self.__recentLog = ""
1155 1333
1156 @pyqtSlot(str, int) 1334 @pyqtSlot(str, int)
1157 def __showSource(self, filename, lineno): 1335 def __showSource(self, filename, lineno):
1158 """ 1336 """
1159 Private slot to show the source of a traceback in an editor. 1337 Private slot to show the source of a traceback in an editor.
1166 if self.__project: 1344 if self.__project:
1167 # running as part of eric IDE 1345 # running as part of eric IDE
1168 self.testFile.emit(filename, lineno, True) 1346 self.testFile.emit(filename, lineno, True)
1169 else: 1347 else:
1170 self.__openEditor(filename, lineno) 1348 self.__openEditor(filename, lineno)
1349 self.__resultsTree.resizeColumns()
1171 1350
1172 def __openEditor(self, filename, linenumber=1): 1351 def __openEditor(self, filename, linenumber=1):
1173 """ 1352 """
1174 Private method to open an editor window for the given file. 1353 Private method to open an editor window for the given file.
1175 1354
1199 event.accept() 1378 event.accept()
1200 1379
1201 for editor in self.__editors: 1380 for editor in self.__editors:
1202 with contextlib.suppress(RuntimeError): 1381 with contextlib.suppress(RuntimeError):
1203 editor.close() 1382 editor.close()
1383
1384 @pyqtSlot(str)
1385 def on_statusFilterComboBox_currentTextChanged(self, status):
1386 """
1387 Private slot handling the selection of a status for items to be shown.
1388
1389 @param status selected status
1390 @type str
1391 """
1392 if status == self.__allFilter:
1393 status = ""
1394
1395 self.__resultFilterModel.setStatusFilterString(status)
1396
1397 if not self.__project:
1398 # running in standalone mode
1399 self.__resultsTree.resizeColumns()
1400
1401 def __updateStatusFilterComboBox(self):
1402 """
1403 Private method to update the status filter dialog box.
1404 """
1405 statusFilters = self.__resultsModel.getStatusFilterList()
1406 self.statusFilterComboBox.clear()
1407 self.statusFilterComboBox.addItem(self.__allFilter)
1408 self.statusFilterComboBox.addItems(sorted(statusFilters))
1409
1410 ############################################################################
1411 ## Methods below are handling the discovery only mode.
1412 ############################################################################
1413
1414 def __findDiscoveryItem(self, modulePath):
1415 """
1416 Private method to find an item given the module path.
1417
1418 @param modulePath path of the module in dotted notation
1419 @type str
1420 @return reference to the item or None
1421 @rtype QTreeWidgetItem or None
1422 """
1423 itm = self.discoveryList.topLevelItem(0)
1424 while itm is not None:
1425 if itm.data(0, TestingWidget.TestCaseNameRole) == modulePath:
1426 return itm
1427
1428 itm = self.discoveryList.itemBelow(itm)
1429
1430 return None
1431
1432 @pyqtSlot(list)
1433 def __testsDiscovered(self, testNames):
1434 """
1435 Private slot handling the 'collected' signal of the executor in discovery
1436 mode.
1437
1438 @param testNames list of tuples containing the test id, the test name
1439 a description, the file name, the line number and the test path as a list
1440 of collected tests
1441 @type list of tuple of (str, str, str, str, int, list)
1442 """
1443 for tid, _name, _desc, filename, lineno, testPath in testNames:
1444 parent = None
1445 for index in range(1, len(testPath) + 1):
1446 modulePath = ".".join(testPath[:index])
1447 itm = self.__findDiscoveryItem(modulePath)
1448 if itm is not None:
1449 parent = itm
1450 else:
1451 if parent is None:
1452 itm = QTreeWidgetItem(self.discoveryList, [testPath[index - 1]])
1453 else:
1454 itm = QTreeWidgetItem(parent, [testPath[index - 1]])
1455 parent.setExpanded(True)
1456 itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable)
1457 itm.setCheckState(0, Qt.CheckState.Unchecked)
1458 itm.setData(0, TestingWidget.TestCaseNameRole, modulePath)
1459 itm.setData(0, TestingWidget.TestCaseLinenoRole, 0)
1460 if os.path.splitext(os.path.basename(filename))[0] == itm.text(0):
1461 itm.setData(0, TestingWidget.TestCaseFileRole, filename)
1462 elif parent:
1463 fn = parent.data(0, TestingWidget.TestCaseFileRole)
1464 if fn:
1465 itm.setData(0, TestingWidget.TestCaseFileRole, fn)
1466 parent = itm
1467
1468 if parent:
1469 parent.setData(0, TestingWidget.TestCaseLinenoRole, lineno)
1470 parent.setData(0, TestingWidget.TestCaseIdRole, tid)
1471
1472 self.__totalCount += len(testNames)
1473
1474 self.sbLabel.setText(self.tr("Discovered %n Test(s)", "", self.__totalCount))
1475
1476 def __testDiscoveryError(self, errors):
1477 """
1478 Private slot handling the 'collectError' signal of the executor.
1479
1480 @param errors list of tuples containing the test name and a description
1481 of the error
1482 @type list of tuple of (str, str)
1483 """
1484 for _testFile, error in errors:
1485 EricMessageBox.critical(
1486 self,
1487 self.tr("Discovery Error"),
1488 self.tr(
1489 "<p>There was an error while discovering tests in <b>{0}</b>.</p>"
1490 "<p>{1}</p>"
1491 ).format(
1492 self.discoveryPicker.currentText(),
1493 "<br/>".join(error.splitlines()),
1494 ),
1495 )
1496 self.sbLabel.clear()
1497
1498 def __testDiscoveryProcessFinished(self, results, output): # noqa: U100
1499 """
1500 Private slot to handle the 'testFinished' signal of the executor in
1501 discovery mode.
1502
1503 @param results list of test result objects (if not sent via the
1504 'testResult' signal)
1505 @type list of TestResult
1506 @param output string containing the test process output (if any)
1507 @type str
1508 """
1509 self.__recentLog = output
1510 self.discoveryList.sortItems(0, Qt.SortOrder.AscendingOrder)
1511
1512 self.__setIdleMode()
1513
1514 def __testDiscoveryAboutToBeStarted(self):
1515 """
1516 Private slot to handle the 'testDiscoveryAboutToBeStarted' signal of the
1517 executor.
1518 """
1519 self.discoveryList.clear()
1520
1521 @pyqtSlot(QTreeWidgetItem, int)
1522 def on_discoveryList_itemChanged(self, item, column):
1523 """
1524 Private slot handling the user checking or unchecking an item.
1525
1526 @param item reference to the item
1527 @type QTreeWidgetItem
1528 @param column changed column
1529 @type int
1530 """
1531 if column == 0:
1532 for index in range(item.childCount()):
1533 item.child(index).setCheckState(0, item.checkState(0))
1534
1535 @pyqtSlot(QTreeWidgetItem, int)
1536 def on_discoveryList_itemActivated(self, item, column):
1537 """
1538 Private slot handling the user activating an item.
1539
1540 @param item reference to the item
1541 @type QTreeWidgetItem
1542 @param column column of the double click
1543 @type int
1544 """
1545 if item:
1546 filename = item.data(0, TestingWidget.TestCaseFileRole)
1547 if filename:
1548 self.__showSource(
1549 filename, item.data(0, TestingWidget.TestCaseLinenoRole) + 1
1550 )
1551
1552 def __selectedTestCases(self, parent=None):
1553 """
1554 Private method to assemble the list of selected test cases and suites.
1555
1556 @param parent reference to the parent item
1557 @type QTreeWidgetItem
1558 @return list of selected test cases
1559 @rtype list of str
1560 """
1561 selectedTests = []
1562 itemsList = (
1563 [
1564 # top level
1565 self.discoveryList.topLevelItem(index)
1566 for index in range(self.discoveryList.topLevelItemCount())
1567 ]
1568 if parent is None
1569 else [parent.child(index) for index in range(parent.childCount())]
1570 )
1571
1572 for itm in itemsList:
1573 if itm.checkState(0) == Qt.CheckState.Checked and itm.childCount() == 0:
1574 selectedTests.append(itm.data(0, TestingWidget.TestCaseIdRole))
1575 if itm.childCount():
1576 # recursively check children
1577 selectedTests.extend(self.__selectedTestCases(itm))
1578
1579 return selectedTests
1580
1581 @pyqtSlot(QPoint)
1582 def on_discoveryList_customContextMenuRequested(self, pos):
1583 """
1584 Private slot to show the context menu of the dicovery list.
1585
1586 @param pos the position of the mouse pointer
1587 @type QPoint
1588 """
1589 self.__discoveryListContextMenu.exec(self.discoveryList.mapToGlobal(pos))
1204 1590
1205 1591
1206 class TestingWindow(EricMainWindow): 1592 class TestingWindow(EricMainWindow):
1207 """ 1593 """
1208 Main window class for the standalone dialog. 1594 Main window class for the standalone dialog.
1233 1619
1234 def eventFilter(self, obj, event): 1620 def eventFilter(self, obj, event):
1235 """ 1621 """
1236 Public method to filter events. 1622 Public method to filter events.
1237 1623
1238 @param obj reference to the object the event is meant for (QObject) 1624 @param obj reference to the object the event is meant for
1239 @param event reference to the event object (QEvent) 1625 @type QObject
1240 @return flag indicating, whether the event was handled (boolean) 1626 @param event reference to the event object
1627 @type QEvent
1628 @return flag indicating, whether the event was handled
1629 @rtype bool
1241 """ 1630 """
1242 if event.type() == QEvent.Type.Close: 1631 if event.type() == QEvent.Type.Close:
1243 QCoreApplication.exit(0) 1632 QCoreApplication.exit(0)
1244 return True 1633 return True
1245 1634

eric ide

mercurial