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 |