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 """ |
9 |
9 |
|
10 import contextlib |
10 import enum |
11 import enum |
11 import locale |
12 import locale |
12 import os |
13 import os |
13 |
14 |
14 from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QCoreApplication |
15 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QCoreApplication |
15 from PyQt6.QtWidgets import ( |
16 from PyQt6.QtWidgets import ( |
16 QAbstractButton, QComboBox, QDialogButtonBox, QWidget |
17 QAbstractButton, QComboBox, QDialogButtonBox, QWidget |
17 ) |
18 ) |
18 |
19 |
19 from EricWidgets import EricMessageBox |
20 from EricWidgets import EricMessageBox |
52 # TODO: add a "Show Coverage" function using PyCoverageDialog |
53 # TODO: add a "Show Coverage" function using PyCoverageDialog |
53 |
54 |
54 class UnittestWidget(QWidget, Ui_UnittestWidget): |
55 class UnittestWidget(QWidget, Ui_UnittestWidget): |
55 """ |
56 """ |
56 Class implementing a widget to orchestrate unit test execution. |
57 Class implementing a widget to orchestrate unit test execution. |
|
58 |
|
59 @signal unittestFile(str, int, bool) emitted to show the source of a |
|
60 unittest file |
|
61 @signal unittestStopped() emitted after a unit test was run |
57 """ |
62 """ |
|
63 unittestFile = pyqtSignal(str, int, bool) |
|
64 unittestStopped = pyqtSignal() |
|
65 |
58 def __init__(self, testfile=None, parent=None): |
66 def __init__(self, testfile=None, parent=None): |
59 """ |
67 """ |
60 Constructor |
68 Constructor |
61 |
69 |
62 @param testfile file name of the test to load |
70 @param testfile file name of the test to load |
69 |
77 |
70 self.__resultsModel = TestResultsModel(self) |
78 self.__resultsModel = TestResultsModel(self) |
71 self.__resultsModel.summary.connect(self.__setStatusLabel) |
79 self.__resultsModel.summary.connect(self.__setStatusLabel) |
72 self.__resultsTree = TestResultsTreeView(self) |
80 self.__resultsTree = TestResultsTreeView(self) |
73 self.__resultsTree.setModel(self.__resultsModel) |
81 self.__resultsTree.setModel(self.__resultsModel) |
|
82 self.__resultsTree.goto.connect(self.__showSource) |
74 self.resultsGroupBox.layout().addWidget(self.__resultsTree) |
83 self.resultsGroupBox.layout().addWidget(self.__resultsTree) |
75 |
84 |
76 self.versionsButton.setIcon( |
85 self.versionsButton.setIcon( |
77 UI.PixmapCache.getIcon("info")) |
86 UI.PixmapCache.getIcon("info")) |
78 self.clearHistoriesButton.setIcon( |
87 self.clearHistoriesButton.setIcon( |
123 Qt.WindowType.WindowContextHelpButtonHint |
132 Qt.WindowType.WindowContextHelpButtonHint |
124 ) |
133 ) |
125 self.setWindowIcon(UI.PixmapCache.getIcon("eric")) |
134 self.setWindowIcon(UI.PixmapCache.getIcon("eric")) |
126 self.setWindowTitle(self.tr("Unittest")) |
135 self.setWindowTitle(self.tr("Unittest")) |
127 |
136 |
128 from VirtualEnv.VirtualenvManager import VirtualenvManager |
137 try: |
129 self.__venvManager = VirtualenvManager(self) |
138 # we are called from within the eric IDE |
130 self.__venvManager.virtualEnvironmentAdded.connect( |
139 self.__venvManager = ericApp().getObject("VirtualEnvManager") |
131 self.__populateVenvComboBox) |
140 self.__project = ericApp().getObject("Project") |
132 self.__venvManager.virtualEnvironmentRemoved.connect( |
141 self.__project.projectOpened.connect(self.__projectOpened) |
133 self.__populateVenvComboBox) |
142 self.__project.projectClosed.connect(self.__projectClosed) |
134 self.__venvManager.virtualEnvironmentChanged.connect( |
143 except KeyError: |
135 self.__populateVenvComboBox) |
144 # we were called as a standalone application |
136 |
145 from VirtualEnv.VirtualenvManager import VirtualenvManager |
137 # TODO: implement project mode |
146 self.__venvManager = VirtualenvManager(self) |
138 self.__forProject = False |
147 self.__venvManager.virtualEnvironmentAdded.connect( |
|
148 self.__populateVenvComboBox) |
|
149 self.__venvManager.virtualEnvironmentRemoved.connect( |
|
150 self.__populateVenvComboBox) |
|
151 self.__venvManager.virtualEnvironmentChanged.connect( |
|
152 self.__populateVenvComboBox) |
|
153 |
|
154 self.__project = None |
139 |
155 |
140 self.__discoverHistory = [] |
156 self.__discoverHistory = [] |
141 self.__fileHistory = [] |
157 self.__fileHistory = [] |
142 self.__testNameHistory = [] |
158 self.__testNameHistory = [] |
143 self.__recentFramework = "" |
159 self.__recentFramework = "" |
166 self.__setIdleMode() |
180 self.__setIdleMode() |
167 |
181 |
168 self.__loadRecent() |
182 self.__loadRecent() |
169 self.__populateVenvComboBox() |
183 self.__populateVenvComboBox() |
170 |
184 |
171 if self.__forProject: |
185 if self.__project and self.__project.isOpen(): |
172 project = ericApp().getObject("Project") |
186 self.venvComboBox.setCurrentText(self.__project.getProjectVenv()) |
173 if project.isOpen(): |
187 self.frameworkComboBox.setCurrentText( |
174 self.__insertDiscovery(project.getProjectPath()) |
188 self.__project.getProjectTestingFramework()) |
175 else: |
189 self.__insertDiscovery(self.__project.getProjectPath()) |
176 self.__insertDiscovery("") |
|
177 else: |
190 else: |
178 self.__insertDiscovery("") |
191 self.__insertDiscovery("") |
|
192 |
179 self.__insertTestFile(testfile) |
193 self.__insertTestFile(testfile) |
180 self.__insertTestName("") |
194 self.__insertTestName("") |
181 |
195 |
182 self.clearHistoriesButton.clicked.connect(self.clearRecent) |
196 self.clearHistoriesButton.clicked.connect(self.clearRecent) |
183 |
197 |
193 |
207 |
194 self.venvComboBox.clear() |
208 self.venvComboBox.clear() |
195 self.venvComboBox.addItem("") |
209 self.venvComboBox.addItem("") |
196 self.venvComboBox.addItems( |
210 self.venvComboBox.addItems( |
197 sorted(self.__venvManager.getVirtualenvNames())) |
211 sorted(self.__venvManager.getVirtualenvNames())) |
198 index = self.venvComboBox.findText(currentText) |
212 self.venvComboBox.setCurrentText(currentText) |
199 if index < 0: |
|
200 index = 0 |
|
201 self.venvComboBox.setCurrentIndex(index) |
|
202 |
213 |
203 def __populateTestFrameworkComboBox(self): |
214 def __populateTestFrameworkComboBox(self): |
204 """ |
215 """ |
205 Private method to (re-)populate the test framework selector. |
216 Private method to (re-)populate the test framework selector. |
206 """ |
217 """ |
238 @return reference to the test results model |
249 @return reference to the test results model |
239 @rtype TestResultsModel |
250 @rtype TestResultsModel |
240 """ |
251 """ |
241 return self.__resultsModel |
252 return self.__resultsModel |
242 |
253 |
|
254 def hasFailedTests(self): |
|
255 """ |
|
256 Public method to check for failed tests. |
|
257 |
|
258 @return flag indicating the existence of failed tests |
|
259 @rtype bool |
|
260 """ |
|
261 return bool(self.__resultsModel.getFailedTests()) |
|
262 |
243 def getFailedTests(self): |
263 def getFailedTests(self): |
244 """ |
264 """ |
245 Public method to get the list of failed tests (if any). |
265 Public method to get the list of failed tests (if any). |
246 |
266 |
247 @return list of IDs of failed tests |
267 @return list of IDs of failed tests |
259 @param history array containing the history |
279 @param history array containing the history |
260 @type list of str |
280 @type list of str |
261 @param item item to be inserted |
281 @param item item to be inserted |
262 @type str |
282 @type str |
263 """ |
283 """ |
264 current = widget.currentText() |
|
265 |
|
266 # prepend the given directory to the discovery picker |
284 # prepend the given directory to the discovery picker |
267 if item is None: |
285 if item is None: |
268 item = "" |
286 item = "" |
269 if item in history: |
287 if item in history: |
270 history.remove(item) |
288 history.remove(item) |
271 history.insert(0, item) |
289 history.insert(0, item) |
272 widget.clear() |
290 widget.clear() |
273 widget.addItems(history) |
291 widget.addItems(history) |
274 |
292 widget.setEditText(item) |
275 if current: |
|
276 widget.setEditText(current) |
|
277 |
293 |
278 @pyqtSlot(str) |
294 @pyqtSlot(str) |
279 def __insertDiscovery(self, start): |
295 def __insertDiscovery(self, start): |
280 """ |
296 """ |
281 Private slot to insert the discovery start directory into the |
297 Private slot to insert the discovery start directory into the |
284 @param start start directory name to be inserted |
300 @param start start directory name to be inserted |
285 @type str |
301 @type str |
286 """ |
302 """ |
287 self.__insertHistory(self.discoveryPicker, self.__discoverHistory, |
303 self.__insertHistory(self.discoveryPicker, self.__discoverHistory, |
288 start) |
304 start) |
|
305 |
|
306 @pyqtSlot(str) |
|
307 def setTestFile(self, testFile): |
|
308 """ |
|
309 Public slot to set the given test file as the current one. |
|
310 |
|
311 @param testFile path of the test file |
|
312 @type str |
|
313 """ |
|
314 if testFile: |
|
315 self.__insertTestFile(testFile) |
|
316 |
|
317 self.discoverCheckBox.setChecked(not bool(testFile)) |
|
318 |
|
319 self.tabWidget.setCurrentIndex(0) |
289 |
320 |
290 @pyqtSlot(str) |
321 @pyqtSlot(str) |
291 def __insertTestFile(self, prog): |
322 def __insertTestFile(self, prog): |
292 """ |
323 """ |
293 Private slot to insert a test file name into the testsuitePicker |
324 Private slot to insert a test file name into the testsuitePicker |
506 |
537 |
507 self.progressGroupBox.hide() |
538 self.progressGroupBox.hide() |
508 |
539 |
509 self.__updateButtonBoxButtons() |
540 self.__updateButtonBoxButtons() |
510 |
541 |
|
542 self.unittestStopped.emit() |
|
543 |
511 self.raise_() |
544 self.raise_() |
512 self.activateWindow() |
545 self.activateWindow() |
513 |
546 |
|
547 @pyqtSlot(bool) |
|
548 def on_discoverCheckBox_toggled(self, checked): |
|
549 """ |
|
550 Private slot handling state changes of the 'discover' checkbox. |
|
551 |
|
552 @param checked state of the checkbox |
|
553 @type bool |
|
554 """ |
|
555 if not bool(self.discoveryPicker.currentText()): |
|
556 if self.__project and self.__project.isOpen(): |
|
557 self.__insertDiscovery(self.__project.getProjectPath()) |
|
558 else: |
|
559 self.__insertDiscovery( |
|
560 Preferences.getMultiProject("Workspace")) |
|
561 |
|
562 self.__resetResults() |
|
563 |
514 @pyqtSlot() |
564 @pyqtSlot() |
515 def on_testsuitePicker_aboutToShowPathPickerDialog(self): |
565 def on_testsuitePicker_aboutToShowPathPickerDialog(self): |
516 """ |
566 """ |
517 Private slot called before the test file selection dialog is shown. |
567 Private slot called before the test file selection dialog is shown. |
518 """ |
568 """ |
519 # TODO: implement eric-ide mode |
569 if self.__project: |
520 # if self.__dbs: |
570 # we were called from within eric |
521 # py3Extensions = ' '.join( |
571 py3Extensions = ' '.join([ |
522 # ["*{0}".format(ext) |
572 "*{0}".format(ext) |
523 # for ext in self.__dbs.getExtensions('Python3')] |
573 for ext in |
524 # ) |
574 ericApp().getObject("DebugServer").getExtensions('Python3') |
525 # fileFilter = self.tr( |
575 ]) |
526 # "Python3 Files ({0});;All Files (*)" |
576 fileFilter = self.tr( |
527 # ).format(py3Extensions) |
577 "Python3 Files ({0});;All Files (*)" |
528 # else: |
578 ).format(py3Extensions) |
529 fileFilter = self.tr("Python Files (*.py);;All Files (*)") |
579 else: |
|
580 # standalone application |
|
581 fileFilter = self.tr("Python Files (*.py);;All Files (*)") |
530 self.testsuitePicker.setFilters(fileFilter) |
582 self.testsuitePicker.setFilters(fileFilter) |
531 |
583 |
532 defaultDirectory = Preferences.getMultiProject("Workspace") |
584 defaultDirectory = ( |
|
585 self.__project.getProjectPath() |
|
586 if self.__project and self.__project.isOpen() else |
|
587 Preferences.getMultiProject("Workspace") |
|
588 ) |
533 if not defaultDirectory: |
589 if not defaultDirectory: |
534 defaultDirectory = os.path.expanduser("~") |
590 defaultDirectory = os.path.expanduser("~") |
535 # if self.__dbs: |
|
536 # project = ericApp().getObject("Project") |
|
537 # if self.__forProject and project.isOpen(): |
|
538 # defaultDirectory = project.getProjectPath() |
|
539 self.testsuitePicker.setDefaultDirectory(defaultDirectory) |
591 self.testsuitePicker.setDefaultDirectory(defaultDirectory) |
540 |
592 |
541 @pyqtSlot(QAbstractButton) |
593 @pyqtSlot(QAbstractButton) |
542 def on_buttonBox_clicked(self, button): |
594 def on_buttonBox_clicked(self, button): |
543 """ |
595 """ |
846 |
898 |
847 @param statusText text to be shown |
899 @param statusText text to be shown |
848 @type str |
900 @type str |
849 """ |
901 """ |
850 self.statusLabel.setText(f"<b>{statusText}</b>") |
902 self.statusLabel.setText(f"<b>{statusText}</b>") |
|
903 |
|
904 @pyqtSlot() |
|
905 def __projectOpened(self): |
|
906 """ |
|
907 Private slot to handle a project being opened. |
|
908 """ |
|
909 self.venvComboBox.setCurrentText(self.__project.getProjectVenv()) |
|
910 self.frameworkComboBox.setCurrentText( |
|
911 self.__project.getProjectTestingFramework()) |
|
912 self.__insertDiscovery(self.__project.getProjectPath()) |
|
913 |
|
914 @pyqtSlot() |
|
915 def __projectClosed(self): |
|
916 """ |
|
917 Private slot to handle a project being closed. |
|
918 """ |
|
919 self.venvComboBox.setCurrentText("") |
|
920 self.frameworkComboBox.setCurrentText("") |
|
921 self.__insertDiscovery("") |
|
922 |
|
923 @pyqtSlot(str, int) |
|
924 def __showSource(self, filename, lineno): |
|
925 """ |
|
926 Private slot to show the source of a traceback in an editor. |
|
927 |
|
928 @param filename file name of the file to be shown |
|
929 @type str |
|
930 @param lineno line number to go to in the file |
|
931 @type int |
|
932 """ |
|
933 if self.__project: |
|
934 # running as part of eric IDE |
|
935 self.unittestFile.emit(filename, lineno, True) |
|
936 else: |
|
937 self.__openEditor(filename, lineno) |
|
938 |
|
939 def __openEditor(self, filename, linenumber): |
|
940 """ |
|
941 Private method to open an editor window for the given file. |
|
942 |
|
943 Note: This method opens an editor window when the unittest dialog |
|
944 is called as a standalone application. |
|
945 |
|
946 @param filename path of the file to be opened |
|
947 @type str |
|
948 @param linenumber line number to place the cursor at |
|
949 @type int |
|
950 """ |
|
951 from QScintilla.MiniEditor import MiniEditor |
|
952 editor = MiniEditor(filename, "Python3", self) |
|
953 editor.gotoLine(linenumber) |
|
954 editor.show() |
|
955 |
|
956 self.__editors.append(editor) |
|
957 |
|
958 def closeEvent(self, event): |
|
959 """ |
|
960 Protected method to handle the close event. |
|
961 |
|
962 @param event close event |
|
963 @type QCloseEvent |
|
964 """ |
|
965 event.accept() |
|
966 |
|
967 for editor in self.__editors: |
|
968 with contextlib.suppress(Exception): |
|
969 editor.close() |
851 |
970 |
852 |
971 |
853 class UnittestWindow(EricMainWindow): |
972 class UnittestWindow(EricMainWindow): |
854 """ |
973 """ |
855 Main window class for the standalone dialog. |
974 Main window class for the standalone dialog. |