1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the UI to the pyunit package. |
|
8 """ |
|
9 |
|
10 import unittest |
|
11 import sys |
|
12 import time |
|
13 import re |
|
14 import os |
|
15 import contextlib |
|
16 |
|
17 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QFileInfo |
|
18 from PyQt6.QtGui import QColor |
|
19 from PyQt6.QtWidgets import ( |
|
20 QWidget, QDialog, QApplication, QDialogButtonBox, QListWidgetItem, |
|
21 QComboBox, QTreeWidgetItem |
|
22 ) |
|
23 |
|
24 from EricWidgets.EricApplication import ericApp |
|
25 from EricWidgets import EricMessageBox |
|
26 from EricWidgets.EricMainWindow import EricMainWindow |
|
27 from EricWidgets.EricPathPicker import EricPathPickerModes |
|
28 |
|
29 from .Ui_UnittestDialog import Ui_UnittestDialog |
|
30 |
|
31 import UI.PixmapCache |
|
32 |
|
33 import Preferences |
|
34 |
|
35 from Globals import ( |
|
36 recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory, |
|
37 recentNameUnittestTestnameHistory |
|
38 ) |
|
39 |
|
40 |
|
41 class UnittestDialog(QWidget, Ui_UnittestDialog): |
|
42 """ |
|
43 Class implementing the UI to the pyunit package. |
|
44 |
|
45 @signal unittestFile(str, int, bool) emitted to show the source of a |
|
46 unittest file |
|
47 @signal unittestStopped() emitted after a unit test was run |
|
48 """ |
|
49 unittestFile = pyqtSignal(str, int, bool) |
|
50 unittestStopped = pyqtSignal() |
|
51 |
|
52 TestCaseNameRole = Qt.ItemDataRole.UserRole |
|
53 TestCaseFileRole = Qt.ItemDataRole.UserRole + 1 |
|
54 |
|
55 ErrorsInfoRole = Qt.ItemDataRole.UserRole |
|
56 |
|
57 SkippedColorDarkTheme = QColor("#00aaff") |
|
58 FailedExpectedColorDarkTheme = QColor("#ccaaff") |
|
59 SucceededUnexpectedColorDarkTheme = QColor("#ff99dd") |
|
60 SkippedColorLightTheme = QColor("#0000ff") |
|
61 FailedExpectedColorLightTheme = QColor("#7700bb") |
|
62 SucceededUnexpectedColorLightTheme = QColor("#ff0000") |
|
63 |
|
64 def __init__(self, prog=None, dbs=None, ui=None, parent=None, name=None): |
|
65 """ |
|
66 Constructor |
|
67 |
|
68 @param prog filename of the program to open |
|
69 @type str |
|
70 @param dbs reference to the debug server object. It is an indication |
|
71 whether we were called from within the eric IDE. |
|
72 @type DebugServer |
|
73 @param ui reference to the UI object |
|
74 @type UserInterface |
|
75 @param parent parent widget of this dialog |
|
76 @type QWidget |
|
77 @param name name of this dialog |
|
78 @type str |
|
79 """ |
|
80 super().__init__(parent) |
|
81 if name: |
|
82 self.setObjectName(name) |
|
83 self.setupUi(self) |
|
84 |
|
85 self.clearHistoriesButton.setIcon( |
|
86 UI.PixmapCache.getIcon("clearPrivateData")) |
|
87 |
|
88 self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) |
|
89 self.testsuitePicker.setInsertPolicy( |
|
90 QComboBox.InsertPolicy.InsertAtTop) |
|
91 self.testsuitePicker.setSizeAdjustPolicy( |
|
92 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) |
|
93 |
|
94 self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE) |
|
95 self.discoveryPicker.setInsertPolicy( |
|
96 QComboBox.InsertPolicy.InsertAtTop) |
|
97 self.discoveryPicker.setSizeAdjustPolicy( |
|
98 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) |
|
99 |
|
100 self.testComboBox.lineEdit().setClearButtonEnabled(True) |
|
101 |
|
102 self.discoverButton = self.buttonBox.addButton( |
|
103 self.tr("Discover"), QDialogButtonBox.ButtonRole.ActionRole) |
|
104 self.discoverButton.setToolTip(self.tr( |
|
105 "Discover tests")) |
|
106 self.discoverButton.setWhatsThis(self.tr( |
|
107 """<b>Discover</b>""" |
|
108 """<p>This button starts a discovery of available tests.</p>""")) |
|
109 self.startButton = self.buttonBox.addButton( |
|
110 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole) |
|
111 |
|
112 self.startButton.setToolTip(self.tr( |
|
113 "Start the selected testsuite")) |
|
114 self.startButton.setWhatsThis(self.tr( |
|
115 """<b>Start Test</b>""" |
|
116 """<p>This button starts the selected testsuite.</p>""")) |
|
117 |
|
118 self.startFailedButton = self.buttonBox.addButton( |
|
119 self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole) |
|
120 self.startFailedButton.setToolTip( |
|
121 self.tr("Reruns failed tests of the selected testsuite")) |
|
122 self.startFailedButton.setWhatsThis(self.tr( |
|
123 """<b>Rerun Failed</b>""" |
|
124 """<p>This button reruns all failed tests of the selected""" |
|
125 """ testsuite.</p>""")) |
|
126 |
|
127 self.stopButton = self.buttonBox.addButton( |
|
128 self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole) |
|
129 self.stopButton.setToolTip(self.tr("Stop the running unittest")) |
|
130 self.stopButton.setWhatsThis(self.tr( |
|
131 """<b>Stop Test</b>""" |
|
132 """<p>This button stops a running unittest.</p>""")) |
|
133 |
|
134 self.discoverButton.setEnabled(False) |
|
135 self.stopButton.setEnabled(False) |
|
136 self.startButton.setDefault(True) |
|
137 self.startFailedButton.setEnabled(False) |
|
138 |
|
139 self.__dbs = dbs |
|
140 self.__forProject = False |
|
141 |
|
142 self.setWindowFlags( |
|
143 self.windowFlags() | |
|
144 Qt.WindowType.WindowContextHelpButtonHint |
|
145 ) |
|
146 self.setWindowIcon(UI.PixmapCache.getIcon("eric")) |
|
147 self.setWindowTitle(self.tr("Unittest")) |
|
148 if dbs: |
|
149 self.ui = ui |
|
150 |
|
151 self.debuggerCheckBox.setChecked(True) |
|
152 |
|
153 # virtual environment manager is only used in the integrated |
|
154 # variant |
|
155 self.__venvManager = ericApp().getObject("VirtualEnvManager") |
|
156 self.__populateVenvComboBox() |
|
157 self.__venvManager.virtualEnvironmentAdded.connect( |
|
158 self.__populateVenvComboBox) |
|
159 self.__venvManager.virtualEnvironmentRemoved.connect( |
|
160 self.__populateVenvComboBox) |
|
161 self.__venvManager.virtualEnvironmentChanged.connect( |
|
162 self.__populateVenvComboBox) |
|
163 else: |
|
164 self.__venvManager = None |
|
165 self.debuggerCheckBox.setVisible(False) |
|
166 self.venvComboBox.setVisible(bool(self.__venvManager)) |
|
167 self.venvLabel.setVisible(bool(self.__venvManager)) |
|
168 |
|
169 self.__setProgressColor("green") |
|
170 self.progressLed.setDarkFactor(150) |
|
171 self.progressLed.off() |
|
172 |
|
173 self.discoverHistory = [] |
|
174 self.fileHistory = [] |
|
175 self.testNameHistory = [] |
|
176 self.running = False |
|
177 self.savedModulelist = None |
|
178 self.savedSysPath = sys.path |
|
179 self.savedCwd = os.getcwd() |
|
180 |
|
181 self.rxPatterns = [ |
|
182 self.tr("^Failure: "), |
|
183 self.tr("^Error: "), |
|
184 # These are for untranslated/partially translated situations |
|
185 "^Failure: ", |
|
186 "^Error: ", |
|
187 ] |
|
188 |
|
189 self.__failedTests = set() |
|
190 |
|
191 # now connect the debug server signals if called from the eric IDE |
|
192 if self.__dbs: |
|
193 self.__dbs.utDiscovered.connect(self.__UTDiscovered) |
|
194 self.__dbs.utPrepared.connect(self.__UTPrepared) |
|
195 self.__dbs.utFinished.connect(self.__setStoppedMode) |
|
196 self.__dbs.utStartTest.connect(self.testStarted) |
|
197 self.__dbs.utStopTest.connect(self.testFinished) |
|
198 self.__dbs.utTestFailed.connect(self.testFailed) |
|
199 self.__dbs.utTestErrored.connect(self.testErrored) |
|
200 self.__dbs.utTestSkipped.connect(self.testSkipped) |
|
201 self.__dbs.utTestFailedExpected.connect(self.testFailedExpected) |
|
202 self.__dbs.utTestSucceededUnexpected.connect( |
|
203 self.testSucceededUnexpected) |
|
204 |
|
205 self.__editors = [] |
|
206 |
|
207 self.__loadRecent() |
|
208 |
|
209 self.insertProg(prog) |
|
210 self.insertTestName("") |
|
211 |
|
212 self.clearHistoriesButton.clicked.connect(self.clearRecent) |
|
213 |
|
214 def keyPressEvent(self, evt): |
|
215 """ |
|
216 Protected slot to handle key press events. |
|
217 |
|
218 @param evt key press event to handle (QKeyEvent) |
|
219 """ |
|
220 if evt.key() == Qt.Key.Key_Escape and self.__dbs: |
|
221 self.close() |
|
222 |
|
223 def __populateVenvComboBox(self): |
|
224 """ |
|
225 Private method to (re-)populate the virtual environments selector. |
|
226 """ |
|
227 currentText = self.venvComboBox.currentText() |
|
228 self.venvComboBox.clear() |
|
229 self.venvComboBox.addItem("") |
|
230 self.venvComboBox.addItems( |
|
231 sorted(self.__venvManager.getVirtualenvNames())) |
|
232 index = self.venvComboBox.findText(currentText) |
|
233 if index < 0: |
|
234 index = 0 |
|
235 self.venvComboBox.setCurrentIndex(index) |
|
236 |
|
237 def __setProgressColor(self, color): |
|
238 """ |
|
239 Private method to set the color of the progress color label. |
|
240 |
|
241 @param color colour to be shown (string) |
|
242 """ |
|
243 self.progressLed.setColor(QColor(color)) |
|
244 |
|
245 def setProjectMode(self, forProject): |
|
246 """ |
|
247 Public method to set the project mode of the dialog. |
|
248 |
|
249 @param forProject flag indicating to run for the open project |
|
250 @type bool |
|
251 """ |
|
252 self.__forProject = forProject |
|
253 if forProject: |
|
254 project = ericApp().getObject("Project") |
|
255 if project.isOpen(): |
|
256 self.insertDiscovery(project.getProjectPath()) |
|
257 else: |
|
258 self.insertDiscovery("") |
|
259 else: |
|
260 self.insertDiscovery("") |
|
261 |
|
262 self.discoveryList.clear() |
|
263 self.tabWidget.setCurrentIndex(0) |
|
264 |
|
265 def insertDiscovery(self, start): |
|
266 """ |
|
267 Public slot to insert the discovery start directory into the |
|
268 discoveryPicker object. |
|
269 |
|
270 @param start start directory name to be inserted |
|
271 @type str |
|
272 """ |
|
273 current = self.discoveryPicker.currentText() |
|
274 |
|
275 # prepend the given directory to the discovery picker |
|
276 if start is None: |
|
277 start = "" |
|
278 if start in self.discoverHistory: |
|
279 self.discoverHistory.remove(start) |
|
280 self.discoverHistory.insert(0, start) |
|
281 self.discoveryPicker.clear() |
|
282 self.discoveryPicker.addItems(self.discoverHistory) |
|
283 |
|
284 if current: |
|
285 self.discoveryPicker.setText(current) |
|
286 |
|
287 def insertProg(self, prog): |
|
288 """ |
|
289 Public slot to insert the filename prog into the testsuitePicker |
|
290 object. |
|
291 |
|
292 @param prog filename to be inserted (string) |
|
293 """ |
|
294 current = self.testsuitePicker.currentText() |
|
295 |
|
296 # prepend the selected file to the testsuite picker |
|
297 if prog is None: |
|
298 prog = "" |
|
299 if prog in self.fileHistory: |
|
300 self.fileHistory.remove(prog) |
|
301 self.fileHistory.insert(0, prog) |
|
302 self.testsuitePicker.clear() |
|
303 self.testsuitePicker.addItems(self.fileHistory) |
|
304 |
|
305 if current: |
|
306 self.testsuitePicker.setText(current) |
|
307 |
|
308 def insertTestName(self, testName): |
|
309 """ |
|
310 Public slot to insert a test name into the testComboBox object. |
|
311 |
|
312 @param testName name of the test to be inserted (string) |
|
313 """ |
|
314 current = self.testComboBox.currentText() |
|
315 |
|
316 # prepend the selected file to the testsuite combobox |
|
317 if testName is None: |
|
318 testName = "" |
|
319 if testName in self.testNameHistory: |
|
320 self.testNameHistory.remove(testName) |
|
321 self.testNameHistory.insert(0, testName) |
|
322 self.testComboBox.clear() |
|
323 self.testComboBox.addItems(self.testNameHistory) |
|
324 |
|
325 if current: |
|
326 self.testComboBox.setCurrentText(current) |
|
327 |
|
328 @pyqtSlot() |
|
329 def on_testsuitePicker_aboutToShowPathPickerDialog(self): |
|
330 """ |
|
331 Private slot called before the test suite selection dialog is shown. |
|
332 """ |
|
333 if self.__dbs: |
|
334 py3Extensions = ' '.join( |
|
335 ["*{0}".format(ext) |
|
336 for ext in self.__dbs.getExtensions('Python3')] |
|
337 ) |
|
338 fileFilter = self.tr( |
|
339 "Python3 Files ({0});;All Files (*)" |
|
340 ).format(py3Extensions) |
|
341 else: |
|
342 fileFilter = self.tr("Python Files (*.py);;All Files (*)") |
|
343 self.testsuitePicker.setFilters(fileFilter) |
|
344 |
|
345 defaultDirectory = Preferences.getMultiProject("Workspace") |
|
346 if not defaultDirectory: |
|
347 defaultDirectory = os.path.expanduser("~") |
|
348 if self.__dbs: |
|
349 project = ericApp().getObject("Project") |
|
350 if self.__forProject and project.isOpen(): |
|
351 defaultDirectory = project.getProjectPath() |
|
352 self.testsuitePicker.setDefaultDirectory(defaultDirectory) |
|
353 |
|
354 @pyqtSlot(str) |
|
355 def on_testsuitePicker_pathSelected(self, suite): |
|
356 """ |
|
357 Private slot called after a test suite has been selected. |
|
358 |
|
359 @param suite file name of the test suite |
|
360 @type str |
|
361 """ |
|
362 self.insertProg(suite) |
|
363 |
|
364 @pyqtSlot(str) |
|
365 def on_testsuitePicker_editTextChanged(self, path): |
|
366 """ |
|
367 Private slot handling changes of the test suite path. |
|
368 |
|
369 @param path path of the test suite file |
|
370 @type str |
|
371 """ |
|
372 self.startFailedButton.setEnabled(False) |
|
373 |
|
374 @pyqtSlot(bool) |
|
375 def on_discoverCheckBox_toggled(self, checked): |
|
376 """ |
|
377 Private slot handling state changes of the 'discover' checkbox. |
|
378 |
|
379 @param checked state of the checkbox |
|
380 @type bool |
|
381 """ |
|
382 self.discoverButton.setEnabled(checked) |
|
383 self.discoveryList.clear() |
|
384 |
|
385 if not bool(self.discoveryPicker.currentText()): |
|
386 if self.__forProject: |
|
387 project = ericApp().getObject("Project") |
|
388 if project.isOpen(): |
|
389 self.insertDiscovery(project.getProjectPath()) |
|
390 return |
|
391 |
|
392 self.insertDiscovery(Preferences.getMultiProject("Workspace")) |
|
393 |
|
394 def on_buttonBox_clicked(self, button): |
|
395 """ |
|
396 Private slot called by a button of the button box clicked. |
|
397 |
|
398 @param button button that was clicked (QAbstractButton) |
|
399 """ |
|
400 if button == self.discoverButton: |
|
401 self.__discover() |
|
402 self.__saveRecent() |
|
403 elif button == self.startButton: |
|
404 self.startTests() |
|
405 self.__saveRecent() |
|
406 elif button == self.stopButton: |
|
407 self.__stopTests() |
|
408 elif button == self.startFailedButton: |
|
409 self.startTests(failedOnly=True) |
|
410 |
|
411 def __loadRecent(self): |
|
412 """ |
|
413 Private method to load the most recently used lists. |
|
414 """ |
|
415 Preferences.Prefs.rsettings.sync() |
|
416 |
|
417 # 1. discovery history |
|
418 self.discoverHistory = [] |
|
419 rs = Preferences.Prefs.rsettings.value( |
|
420 recentNameUnittestDiscoverHistory) |
|
421 if rs is not None: |
|
422 recent = [f |
|
423 for f in Preferences.toList(rs) |
|
424 if QFileInfo(f).exists()] |
|
425 self.discoverHistory = recent[ |
|
426 :Preferences.getDebugger("RecentNumber")] |
|
427 |
|
428 # 2. test file history |
|
429 self.fileHistory = [] |
|
430 rs = Preferences.Prefs.rsettings.value( |
|
431 recentNameUnittestFileHistory) |
|
432 if rs is not None: |
|
433 recent = [f |
|
434 for f in Preferences.toList(rs) |
|
435 if QFileInfo(f).exists()] |
|
436 self.fileHistory = recent[ |
|
437 :Preferences.getDebugger("RecentNumber")] |
|
438 |
|
439 # 3. test name history |
|
440 self.testNameHistory = [] |
|
441 rs = Preferences.Prefs.rsettings.value( |
|
442 recentNameUnittestTestnameHistory) |
|
443 if rs is not None: |
|
444 recent = [n for n in Preferences.toList(rs) if n] |
|
445 self.testNameHistory = recent[ |
|
446 :Preferences.getDebugger("RecentNumber")] |
|
447 |
|
448 def __saveRecent(self): |
|
449 """ |
|
450 Private method to save the most recently used lists. |
|
451 """ |
|
452 Preferences.Prefs.rsettings.setValue( |
|
453 recentNameUnittestDiscoverHistory, self.discoverHistory) |
|
454 Preferences.Prefs.rsettings.setValue( |
|
455 recentNameUnittestFileHistory, self.fileHistory) |
|
456 Preferences.Prefs.rsettings.setValue( |
|
457 recentNameUnittestTestnameHistory, self.testNameHistory) |
|
458 |
|
459 Preferences.Prefs.rsettings.sync() |
|
460 |
|
461 @pyqtSlot() |
|
462 def clearRecent(self): |
|
463 """ |
|
464 Public slot to clear the recently used lists. |
|
465 """ |
|
466 # clear histories |
|
467 self.discoverHistory = [] |
|
468 self.fileHistory = [] |
|
469 self.testNameHistory = [] |
|
470 |
|
471 # clear widgets with histories |
|
472 self.discoveryPicker.clear() |
|
473 self.testsuitePicker.clear() |
|
474 self.testComboBox.clear() |
|
475 |
|
476 # sync histories |
|
477 self.__saveRecent() |
|
478 |
|
479 @pyqtSlot() |
|
480 def __discover(self): |
|
481 """ |
|
482 Private slot to discover unit test but don't run them. |
|
483 """ |
|
484 if self.running: |
|
485 return |
|
486 |
|
487 self.discoveryList.clear() |
|
488 |
|
489 discoveryStart = self.discoveryPicker.currentText() |
|
490 self.insertDiscovery(discoveryStart) |
|
491 self.sbLabel.setText(self.tr("Discovering Tests")) |
|
492 QApplication.processEvents() |
|
493 |
|
494 self.testName = self.tr("Unittest with auto-discovery") |
|
495 if self.__dbs: |
|
496 venvName = self.venvComboBox.currentText() |
|
497 |
|
498 # we are cooperating with the eric IDE |
|
499 project = ericApp().getObject("Project") |
|
500 if self.__forProject: |
|
501 mainScript = project.getMainScript(True) |
|
502 clientType = project.getProjectLanguage() |
|
503 if mainScript: |
|
504 workdir = os.path.dirname(os.path.abspath(mainScript)) |
|
505 else: |
|
506 workdir = project.getProjectPath() |
|
507 sysPath = [workdir] |
|
508 if not discoveryStart: |
|
509 discoveryStart = workdir |
|
510 else: |
|
511 if not discoveryStart: |
|
512 EricMessageBox.critical( |
|
513 self, |
|
514 self.tr("Unittest"), |
|
515 self.tr("You must enter a start directory for" |
|
516 " auto-discovery.")) |
|
517 return |
|
518 |
|
519 workdir = "" |
|
520 clientType = "Python3" |
|
521 sysPath = [] |
|
522 self.__dbs.remoteUTDiscover(clientType, self.__forProject, |
|
523 venvName, sysPath, workdir, |
|
524 discoveryStart) |
|
525 else: |
|
526 # we are running as an application |
|
527 if not discoveryStart: |
|
528 EricMessageBox.critical( |
|
529 self, |
|
530 self.tr("Unittest"), |
|
531 self.tr("You must enter a start directory for" |
|
532 " auto-discovery.")) |
|
533 return |
|
534 |
|
535 if discoveryStart: |
|
536 sys.path = ( |
|
537 [os.path.abspath(discoveryStart)] + |
|
538 self.savedSysPath |
|
539 ) |
|
540 |
|
541 # clean up list of imported modules to force a reimport upon |
|
542 # running the test |
|
543 if self.savedModulelist: |
|
544 for modname in list(sys.modules.keys()): |
|
545 if modname not in self.savedModulelist: |
|
546 # delete it |
|
547 del(sys.modules[modname]) |
|
548 self.savedModulelist = sys.modules.copy() |
|
549 |
|
550 # now try to discover the testsuite |
|
551 os.chdir(discoveryStart) |
|
552 try: |
|
553 testLoader = unittest.TestLoader() |
|
554 test = testLoader.discover(discoveryStart) |
|
555 if hasattr(testLoader, "errors") and bool(testLoader.errors): |
|
556 EricMessageBox.critical( |
|
557 self, |
|
558 self.tr("Unittest"), |
|
559 self.tr( |
|
560 "<p>Unable to discover tests.</p>" |
|
561 "<p>{0}</p>" |
|
562 ).format("<br/>".join(testLoader.errors) |
|
563 .replace("\n", "<br/>")) |
|
564 ) |
|
565 self.sbLabel.clear() |
|
566 else: |
|
567 testsList = self.__assembleTestCasesList( |
|
568 test, discoveryStart) |
|
569 self.__populateDiscoveryResults(testsList) |
|
570 self.sbLabel.setText( |
|
571 self.tr("Discovered %n Test(s)", "", |
|
572 len(testsList))) |
|
573 self.tabWidget.setCurrentIndex(0) |
|
574 except Exception: |
|
575 exc_type, exc_value, exc_tb = sys.exc_info() |
|
576 EricMessageBox.critical( |
|
577 self, |
|
578 self.tr("Unittest"), |
|
579 self.tr( |
|
580 "<p>Unable to discover tests.</p>" |
|
581 "<p>{0}<br/>{1}</p>") |
|
582 .format(str(exc_type), |
|
583 str(exc_value).replace("\n", "<br/>")) |
|
584 ) |
|
585 self.sbLabel.clear() |
|
586 |
|
587 sys.path = self.savedSysPath |
|
588 |
|
589 def __assembleTestCasesList(self, suite, start): |
|
590 """ |
|
591 Private method to assemble a list of test cases included in a test |
|
592 suite. |
|
593 |
|
594 @param suite test suite to be inspected |
|
595 @type unittest.TestSuite |
|
596 @param start name of directory discovery was started at |
|
597 @type str |
|
598 @return list of tuples containing the test case ID, a short description |
|
599 and the path of the test file name |
|
600 @rtype list of tuples of (str, str, str) |
|
601 """ |
|
602 testCases = [] |
|
603 for test in suite: |
|
604 if isinstance(test, unittest.TestSuite): |
|
605 testCases.extend(self.__assembleTestCasesList(test, start)) |
|
606 else: |
|
607 testId = test.id() |
|
608 if ( |
|
609 "ModuleImportFailure" not in testId and |
|
610 "LoadTestsFailure" not in testId and |
|
611 "_FailedTest" not in testId |
|
612 ): |
|
613 filename = os.path.join( |
|
614 start, |
|
615 test.__module__.replace(".", os.sep) + ".py") |
|
616 testCases.append( |
|
617 (test.id(), test.shortDescription(), filename) |
|
618 ) |
|
619 return testCases |
|
620 |
|
621 def __findDiscoveryItem(self, modulePath): |
|
622 """ |
|
623 Private method to find an item given the module path. |
|
624 |
|
625 @param modulePath path of the module in dotted notation |
|
626 @type str |
|
627 @return reference to the item or None |
|
628 @rtype QTreeWidgetItem or None |
|
629 """ |
|
630 itm = self.discoveryList.topLevelItem(0) |
|
631 while itm is not None: |
|
632 if itm.data(0, UnittestDialog.TestCaseNameRole) == modulePath: |
|
633 return itm |
|
634 itm = self.discoveryList.itemBelow(itm) |
|
635 |
|
636 return None |
|
637 |
|
638 def __populateDiscoveryResults(self, tests): |
|
639 """ |
|
640 Private method to populate the test discovery results list. |
|
641 |
|
642 @param tests list of tuples containing the discovery results |
|
643 @type list of tuples of (str, str, str) |
|
644 """ |
|
645 for test, _testDescription, filename in tests: |
|
646 testPath = test.split(".") |
|
647 pitm = None |
|
648 for index in range(1, len(testPath) + 1): |
|
649 modulePath = ".".join(testPath[:index]) |
|
650 itm = self.__findDiscoveryItem(modulePath) |
|
651 if itm is not None: |
|
652 pitm = itm |
|
653 else: |
|
654 if pitm is None: |
|
655 itm = QTreeWidgetItem(self.discoveryList, |
|
656 [testPath[index - 1]]) |
|
657 else: |
|
658 itm = QTreeWidgetItem(pitm, |
|
659 [testPath[index - 1]]) |
|
660 pitm.setExpanded(True) |
|
661 itm.setFlags(Qt.ItemFlag.ItemIsUserCheckable | |
|
662 Qt.ItemFlag.ItemIsEnabled) |
|
663 itm.setCheckState(0, Qt.CheckState.Unchecked) |
|
664 itm.setData(0, UnittestDialog.TestCaseNameRole, modulePath) |
|
665 if ( |
|
666 os.path.splitext(os.path.basename(filename))[0] == |
|
667 itm.text(0) |
|
668 ): |
|
669 itm.setData(0, UnittestDialog.TestCaseFileRole, |
|
670 filename) |
|
671 elif pitm: |
|
672 fn = pitm.data(0, UnittestDialog.TestCaseFileRole) |
|
673 if fn: |
|
674 itm.setData(0, UnittestDialog.TestCaseFileRole, fn) |
|
675 pitm = itm |
|
676 |
|
677 def __selectedTestCases(self, parent=None): |
|
678 """ |
|
679 Private method to assemble the list of selected test cases and suites. |
|
680 |
|
681 @param parent reference to the parent item |
|
682 @type QTreeWidgetItem |
|
683 @return list of selected test cases |
|
684 @rtype list of str |
|
685 """ |
|
686 selectedTests = [] |
|
687 itemsList = [ |
|
688 self.discoveryList.topLevelItem(index) |
|
689 for index in range(self.discoveryList.topLevelItemCount()) |
|
690 ] if parent is None else [ |
|
691 parent.child(index) |
|
692 for index in range(parent.childCount()) |
|
693 ] |
|
694 |
|
695 for itm in itemsList: |
|
696 if ( |
|
697 itm.checkState(0) == Qt.CheckState.Checked and |
|
698 itm.childCount() == 0 |
|
699 ): |
|
700 selectedTests.append( |
|
701 itm.data(0, UnittestDialog.TestCaseNameRole)) |
|
702 if itm.childCount(): |
|
703 # recursively check children |
|
704 selectedTests.extend(self.__selectedTestCases(itm)) |
|
705 |
|
706 return selectedTests |
|
707 |
|
708 def __UTDiscovered(self, testCases, exc_type, exc_value): |
|
709 """ |
|
710 Private slot to handle the utDiscovered signal. |
|
711 |
|
712 If the unittest suite was loaded successfully, we ask the |
|
713 client to run the test suite. |
|
714 |
|
715 @param testCases list of detected test cases |
|
716 @type str |
|
717 @param exc_type exception type occured during discovery |
|
718 @type str |
|
719 @param exc_value value of exception occured during discovery |
|
720 @type str |
|
721 """ |
|
722 if testCases: |
|
723 self.__populateDiscoveryResults(testCases) |
|
724 self.sbLabel.setText( |
|
725 self.tr("Discovered %n Test(s)", "", |
|
726 len(testCases))) |
|
727 self.tabWidget.setCurrentIndex(0) |
|
728 else: |
|
729 EricMessageBox.critical( |
|
730 self, |
|
731 self.tr("Unittest"), |
|
732 self.tr("<p>Unable to discover tests.</p>" |
|
733 "<p>{0}<br/>{1}</p>") |
|
734 .format(exc_type, exc_value.replace("\n", "<br/>")) |
|
735 ) |
|
736 |
|
737 @pyqtSlot(QTreeWidgetItem, int) |
|
738 def on_discoveryList_itemChanged(self, item, column): |
|
739 """ |
|
740 Private slot handling the user checking or unchecking an item. |
|
741 |
|
742 @param item reference to the item |
|
743 @type QTreeWidgetItem |
|
744 @param column changed column |
|
745 @type int |
|
746 """ |
|
747 if column == 0: |
|
748 for index in range(item.childCount()): |
|
749 item.child(index).setCheckState(0, item.checkState(0)) |
|
750 |
|
751 @pyqtSlot(QTreeWidgetItem, int) |
|
752 def on_discoveryList_itemDoubleClicked(self, item, column): |
|
753 """ |
|
754 Private slot handling the user double clicking an item. |
|
755 |
|
756 @param item reference to the item |
|
757 @type QTreeWidgetItem |
|
758 @param column column of the double click |
|
759 @type int |
|
760 """ |
|
761 if item: |
|
762 filename = item.data(0, UnittestDialog.TestCaseFileRole) |
|
763 if filename: |
|
764 if self.__dbs: |
|
765 # running as part of eric IDE |
|
766 self.unittestFile.emit(filename, 1, False) |
|
767 else: |
|
768 self.__openEditor(filename, 1) |
|
769 |
|
770 @pyqtSlot() |
|
771 def startTests(self, failedOnly=False): |
|
772 """ |
|
773 Public slot to start the test. |
|
774 |
|
775 @param failedOnly flag indicating to run only failed tests (boolean) |
|
776 """ |
|
777 if self.running: |
|
778 return |
|
779 |
|
780 discover = self.discoverCheckBox.isChecked() |
|
781 if discover: |
|
782 discoveryStart = self.discoveryPicker.currentText() |
|
783 testFileName = "" |
|
784 testName = "" |
|
785 |
|
786 if discoveryStart: |
|
787 self.insertDiscovery(discoveryStart) |
|
788 else: |
|
789 discoveryStart = "" |
|
790 testFileName = self.testsuitePicker.currentText() |
|
791 testName = self.testComboBox.currentText() |
|
792 if testName: |
|
793 self.insertTestName(testName) |
|
794 if testFileName and not testName: |
|
795 testName = "suite" |
|
796 |
|
797 if not discover and not testFileName and not testName: |
|
798 EricMessageBox.critical( |
|
799 self, |
|
800 self.tr("Unittest"), |
|
801 self.tr("You must select auto-discovery or enter a test suite" |
|
802 " file or a dotted test name.")) |
|
803 return |
|
804 |
|
805 # prepend the selected file to the testsuite combobox |
|
806 self.insertProg(testFileName) |
|
807 self.sbLabel.setText(self.tr("Preparing Testsuite")) |
|
808 QApplication.processEvents() |
|
809 |
|
810 if discover: |
|
811 self.testName = self.tr("Unittest with auto-discovery") |
|
812 else: |
|
813 # build the module name from the filename without extension |
|
814 if testFileName: |
|
815 self.testName = os.path.splitext( |
|
816 os.path.basename(testFileName))[0] |
|
817 elif testName: |
|
818 self.testName = testName |
|
819 else: |
|
820 self.testName = self.tr("<Unnamed Test>") |
|
821 |
|
822 if failedOnly: |
|
823 testCases = [] |
|
824 else: |
|
825 testCases = self.__selectedTestCases() |
|
826 |
|
827 if not testCases and self.discoveryList.topLevelItemCount(): |
|
828 ok = EricMessageBox.yesNo( |
|
829 self, |
|
830 self.tr("Unittest"), |
|
831 self.tr("""No test case has been selected. Shall all""" |
|
832 """ test cases be run?""")) |
|
833 if not ok: |
|
834 return |
|
835 |
|
836 if self.__dbs: |
|
837 venvName = self.venvComboBox.currentText() |
|
838 |
|
839 # we are cooperating with the eric IDE |
|
840 project = ericApp().getObject("Project") |
|
841 if self.__forProject: |
|
842 mainScript = project.getMainScript(True) |
|
843 clientType = project.getProjectLanguage() |
|
844 if mainScript: |
|
845 workdir = os.path.dirname(os.path.abspath(mainScript)) |
|
846 coverageFile = os.path.splitext(mainScript)[0] |
|
847 else: |
|
848 workdir = project.getProjectPath() |
|
849 coverageFile = os.path.join(discoveryStart, "unittest") |
|
850 sysPath = [workdir] |
|
851 if discover and not discoveryStart: |
|
852 discoveryStart = workdir |
|
853 else: |
|
854 if discover: |
|
855 if not discoveryStart: |
|
856 EricMessageBox.critical( |
|
857 self, |
|
858 self.tr("Unittest"), |
|
859 self.tr("You must enter a start directory for" |
|
860 " auto-discovery.")) |
|
861 return |
|
862 |
|
863 coverageFile = os.path.join(discoveryStart, "unittest") |
|
864 workdir = "" |
|
865 clientType = "Python3" |
|
866 elif testFileName: |
|
867 mainScript = os.path.abspath(testFileName) |
|
868 workdir = os.path.dirname(mainScript) |
|
869 clientType = "Python3" |
|
870 coverageFile = os.path.splitext(mainScript)[0] |
|
871 else: |
|
872 coverageFile = os.path.abspath("unittest") |
|
873 workdir = "" |
|
874 clientType = "Python3" |
|
875 sysPath = [] |
|
876 if failedOnly and self.__failedTests: |
|
877 failed = list(self.__failedTests) |
|
878 if discover: |
|
879 workdir = discoveryStart |
|
880 discover = False |
|
881 else: |
|
882 failed = [] |
|
883 self.__failedTests = set() |
|
884 self.__dbs.remoteUTPrepare( |
|
885 testFileName, self.testName, testName, failed, |
|
886 self.coverageCheckBox.isChecked(), coverageFile, |
|
887 self.coverageEraseCheckBox.isChecked(), clientType=clientType, |
|
888 forProject=self.__forProject, workdir=workdir, |
|
889 venvName=venvName, syspath=sysPath, |
|
890 discover=discover, discoveryStart=discoveryStart, |
|
891 testCases=testCases, debug=self.debuggerCheckBox.isChecked()) |
|
892 else: |
|
893 # we are running as an application |
|
894 if discover and not discoveryStart: |
|
895 EricMessageBox.critical( |
|
896 self, |
|
897 self.tr("Unittest"), |
|
898 self.tr("You must enter a start directory for" |
|
899 " auto-discovery.")) |
|
900 return |
|
901 |
|
902 if testFileName: |
|
903 sys.path = ( |
|
904 [os.path.dirname(os.path.abspath(testFileName))] + |
|
905 self.savedSysPath |
|
906 ) |
|
907 elif discoveryStart: |
|
908 sys.path = ( |
|
909 [os.path.abspath(discoveryStart)] + |
|
910 self.savedSysPath |
|
911 ) |
|
912 |
|
913 # clean up list of imported modules to force a reimport upon |
|
914 # running the test |
|
915 if self.savedModulelist: |
|
916 for modname in list(sys.modules.keys()): |
|
917 if modname not in self.savedModulelist: |
|
918 # delete it |
|
919 del(sys.modules[modname]) |
|
920 self.savedModulelist = sys.modules.copy() |
|
921 |
|
922 os.chdir(self.savedCwd) |
|
923 |
|
924 # now try to generate the testsuite |
|
925 try: |
|
926 testLoader = unittest.TestLoader() |
|
927 if failedOnly and self.__failedTests: |
|
928 failed = list(self.__failedTests) |
|
929 if discover: |
|
930 os.chdir(discoveryStart) |
|
931 discover = False |
|
932 else: |
|
933 failed = [] |
|
934 if discover: |
|
935 if testCases: |
|
936 test = testLoader.loadTestsFromNames(testCases) |
|
937 else: |
|
938 test = testLoader.discover(discoveryStart) |
|
939 else: |
|
940 if testFileName: |
|
941 module = __import__(self.testName) |
|
942 else: |
|
943 module = None |
|
944 if failedOnly and self.__failedTests: |
|
945 if module: |
|
946 failed = [t.split(".", 1)[1] |
|
947 for t in self.__failedTests] |
|
948 else: |
|
949 failed = list(self.__failedTests) |
|
950 test = testLoader.loadTestsFromNames( |
|
951 failed, module) |
|
952 else: |
|
953 test = testLoader.loadTestsFromName( |
|
954 testName, module) |
|
955 except Exception: |
|
956 exc_type, exc_value, exc_tb = sys.exc_info() |
|
957 EricMessageBox.critical( |
|
958 self, |
|
959 self.tr("Unittest"), |
|
960 self.tr( |
|
961 "<p>Unable to run test <b>{0}</b>.</p>" |
|
962 "<p>{1}<br/>{2}</p>") |
|
963 .format(self.testName, str(exc_type), |
|
964 str(exc_value).replace("\n", "<br/>")) |
|
965 ) |
|
966 return |
|
967 |
|
968 # now set up the coverage stuff |
|
969 if self.coverageCheckBox.isChecked(): |
|
970 if discover: |
|
971 covname = os.path.join(discoveryStart, "unittest") |
|
972 elif testFileName: |
|
973 covname = os.path.splitext( |
|
974 os.path.abspath(testFileName))[0] |
|
975 else: |
|
976 covname = "unittest" |
|
977 |
|
978 from DebugClients.Python.coverage import coverage |
|
979 cover = coverage(data_file="{0}.coverage".format(covname)) |
|
980 if self.coverageEraseCheckBox.isChecked(): |
|
981 cover.erase() |
|
982 else: |
|
983 cover = None |
|
984 |
|
985 self.testResult = QtTestResult( |
|
986 self, self.failfastCheckBox.isChecked()) |
|
987 self.totalTests = test.countTestCases() |
|
988 if self.totalTests == 0: |
|
989 EricMessageBox.warning( |
|
990 self, |
|
991 self.tr("Unittest"), |
|
992 self.tr("""No unittest were found. Aborting...""")) |
|
993 else: |
|
994 self.__failedTests = set() |
|
995 self.__setRunningMode() |
|
996 if cover: |
|
997 cover.start() |
|
998 test.run(self.testResult) |
|
999 if cover: |
|
1000 cover.stop() |
|
1001 cover.save() |
|
1002 self.__setStoppedMode() |
|
1003 sys.path = self.savedSysPath |
|
1004 |
|
1005 def __UTPrepared(self, nrTests, exc_type, exc_value): |
|
1006 """ |
|
1007 Private slot to handle the utPrepared signal. |
|
1008 |
|
1009 If the unittest suite was loaded successfully, we ask the |
|
1010 client to run the test suite. |
|
1011 |
|
1012 @param nrTests number of tests contained in the test suite (integer) |
|
1013 @param exc_type type of exception occured during preparation (string) |
|
1014 @param exc_value value of exception occured during preparation (string) |
|
1015 """ |
|
1016 if nrTests == 0: |
|
1017 EricMessageBox.critical( |
|
1018 self, |
|
1019 self.tr("Unittest"), |
|
1020 self.tr( |
|
1021 "<p>Unable to run test <b>{0}</b>.</p>" |
|
1022 "<p>{1}<br/>{2}</p>") |
|
1023 .format(self.testName, exc_type, |
|
1024 exc_value.replace("\n", "<br/>")) |
|
1025 ) |
|
1026 return |
|
1027 |
|
1028 self.totalTests = nrTests |
|
1029 self.__setRunningMode() |
|
1030 self.__dbs.remoteUTRun(debug=self.debuggerCheckBox.isChecked(), |
|
1031 failfast=self.failfastCheckBox.isChecked()) |
|
1032 |
|
1033 @pyqtSlot() |
|
1034 def __stopTests(self): |
|
1035 """ |
|
1036 Private slot to stop the test. |
|
1037 """ |
|
1038 if self.__dbs: |
|
1039 self.__dbs.remoteUTStop() |
|
1040 elif self.testResult: |
|
1041 self.testResult.stop() |
|
1042 |
|
1043 def on_errorsListWidget_currentTextChanged(self, text): |
|
1044 """ |
|
1045 Private slot to handle the highlighted signal. |
|
1046 |
|
1047 @param text current text (string) |
|
1048 """ |
|
1049 if text: |
|
1050 for pattern in self.rxPatterns: |
|
1051 text = re.sub(pattern, "", text) |
|
1052 |
|
1053 foundItems = self.testsListWidget.findItems( |
|
1054 text, Qt.MatchFlag.MatchExactly) |
|
1055 if len(foundItems) > 0: |
|
1056 itm = foundItems[0] |
|
1057 self.testsListWidget.setCurrentItem(itm) |
|
1058 self.testsListWidget.scrollToItem(itm) |
|
1059 |
|
1060 def __setRunningMode(self): |
|
1061 """ |
|
1062 Private method to set the GUI in running mode. |
|
1063 """ |
|
1064 self.running = True |
|
1065 self.tabWidget.setCurrentIndex(1) |
|
1066 |
|
1067 # reset counters and error infos |
|
1068 self.runCount = 0 |
|
1069 self.failCount = 0 |
|
1070 self.errorCount = 0 |
|
1071 self.skippedCount = 0 |
|
1072 self.expectedFailureCount = 0 |
|
1073 self.unexpectedSuccessCount = 0 |
|
1074 self.remainingCount = self.totalTests |
|
1075 |
|
1076 # reset the GUI |
|
1077 self.progressCounterRunCount.setText(str(self.runCount)) |
|
1078 self.progressCounterRemCount.setText(str(self.remainingCount)) |
|
1079 self.progressCounterFailureCount.setText(str(self.failCount)) |
|
1080 self.progressCounterErrorCount.setText(str(self.errorCount)) |
|
1081 self.progressCounterSkippedCount.setText(str(self.skippedCount)) |
|
1082 self.progressCounterExpectedFailureCount.setText( |
|
1083 str(self.expectedFailureCount)) |
|
1084 self.progressCounterUnexpectedSuccessCount.setText( |
|
1085 str(self.unexpectedSuccessCount)) |
|
1086 |
|
1087 self.errorsListWidget.clear() |
|
1088 self.testsListWidget.clear() |
|
1089 |
|
1090 self.progressProgressBar.setRange(0, self.totalTests) |
|
1091 self.__setProgressColor("green") |
|
1092 self.progressProgressBar.reset() |
|
1093 |
|
1094 self.stopButton.setEnabled(True) |
|
1095 self.startButton.setEnabled(False) |
|
1096 self.startFailedButton.setEnabled(False) |
|
1097 self.stopButton.setDefault(True) |
|
1098 |
|
1099 self.sbLabel.setText(self.tr("Running")) |
|
1100 self.progressLed.on() |
|
1101 QApplication.processEvents() |
|
1102 |
|
1103 self.startTime = time.time() |
|
1104 |
|
1105 def __setStoppedMode(self): |
|
1106 """ |
|
1107 Private method to set the GUI in stopped mode. |
|
1108 """ |
|
1109 self.stopTime = time.time() |
|
1110 self.timeTaken = float(self.stopTime - self.startTime) |
|
1111 self.running = False |
|
1112 |
|
1113 failedAvailable = bool(self.__failedTests) |
|
1114 self.startButton.setEnabled(True) |
|
1115 self.startFailedButton.setEnabled(failedAvailable) |
|
1116 self.stopButton.setEnabled(False) |
|
1117 if failedAvailable: |
|
1118 self.startFailedButton.setDefault(True) |
|
1119 self.startButton.setDefault(False) |
|
1120 else: |
|
1121 self.startFailedButton.setDefault(False) |
|
1122 self.startButton.setDefault(True) |
|
1123 self.sbLabel.setText( |
|
1124 self.tr("Ran %n test(s) in {0:.3f}s", "", self.runCount) |
|
1125 .format(self.timeTaken)) |
|
1126 self.progressLed.off() |
|
1127 |
|
1128 self.unittestStopped.emit() |
|
1129 |
|
1130 self.raise_() |
|
1131 self.activateWindow() |
|
1132 |
|
1133 def testFailed(self, test, exc, testId): |
|
1134 """ |
|
1135 Public method called if a test fails. |
|
1136 |
|
1137 @param test name of the test (string) |
|
1138 @param exc string representation of the exception (string) |
|
1139 @param testId id of the test (string) |
|
1140 """ |
|
1141 self.failCount += 1 |
|
1142 self.progressCounterFailureCount.setText(str(self.failCount)) |
|
1143 itm = QListWidgetItem(self.tr("Failure: {0}").format(test)) |
|
1144 itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc)) |
|
1145 self.errorsListWidget.insertItem(0, itm) |
|
1146 self.__failedTests.add(testId) |
|
1147 |
|
1148 def testErrored(self, test, exc, testId): |
|
1149 """ |
|
1150 Public method called if a test errors. |
|
1151 |
|
1152 @param test name of the test (string) |
|
1153 @param exc string representation of the exception (string) |
|
1154 @param testId id of the test (string) |
|
1155 """ |
|
1156 self.errorCount += 1 |
|
1157 self.progressCounterErrorCount.setText(str(self.errorCount)) |
|
1158 itm = QListWidgetItem(self.tr("Error: {0}").format(test)) |
|
1159 itm.setData(UnittestDialog.ErrorsInfoRole, (test, exc)) |
|
1160 self.errorsListWidget.insertItem(0, itm) |
|
1161 self.__failedTests.add(testId) |
|
1162 |
|
1163 def testSkipped(self, test, reason, testId): |
|
1164 """ |
|
1165 Public method called if a test was skipped. |
|
1166 |
|
1167 @param test name of the test (string) |
|
1168 @param reason reason for skipping the test (string) |
|
1169 @param testId id of the test (string) |
|
1170 """ |
|
1171 self.skippedCount += 1 |
|
1172 self.progressCounterSkippedCount.setText(str(self.skippedCount)) |
|
1173 itm = QListWidgetItem(self.tr(" Skipped: {0}").format(reason)) |
|
1174 if ericApp().usesDarkPalette(): |
|
1175 itm.setForeground(self.SkippedColorDarkTheme) |
|
1176 else: |
|
1177 itm.setForeground(self.SkippedColorLightTheme) |
|
1178 self.testsListWidget.insertItem(1, itm) |
|
1179 |
|
1180 def testFailedExpected(self, test, exc, testId): |
|
1181 """ |
|
1182 Public method called if a test fails as expected. |
|
1183 |
|
1184 @param test name of the test (string) |
|
1185 @param exc string representation of the exception (string) |
|
1186 @param testId id of the test (string) |
|
1187 """ |
|
1188 self.expectedFailureCount += 1 |
|
1189 self.progressCounterExpectedFailureCount.setText( |
|
1190 str(self.expectedFailureCount)) |
|
1191 itm = QListWidgetItem(self.tr(" Expected Failure")) |
|
1192 if ericApp().usesDarkPalette(): |
|
1193 itm.setForeground(self.FailedExpectedColorDarkTheme) |
|
1194 else: |
|
1195 itm.setForeground(self.FailedExpectedColorLightTheme) |
|
1196 self.testsListWidget.insertItem(1, itm) |
|
1197 |
|
1198 def testSucceededUnexpected(self, test, testId): |
|
1199 """ |
|
1200 Public method called if a test succeeds unexpectedly. |
|
1201 |
|
1202 @param test name of the test (string) |
|
1203 @param testId id of the test (string) |
|
1204 """ |
|
1205 self.unexpectedSuccessCount += 1 |
|
1206 self.progressCounterUnexpectedSuccessCount.setText( |
|
1207 str(self.unexpectedSuccessCount)) |
|
1208 itm = QListWidgetItem(self.tr(" Unexpected Success")) |
|
1209 if ericApp().usesDarkPalette(): |
|
1210 itm.setForeground(self.SucceededUnexpectedColorDarkTheme) |
|
1211 else: |
|
1212 itm.setForeground(self.SucceededUnexpectedColorLightTheme) |
|
1213 self.testsListWidget.insertItem(1, itm) |
|
1214 |
|
1215 def testStarted(self, test, doc): |
|
1216 """ |
|
1217 Public method called if a test is about to be run. |
|
1218 |
|
1219 @param test name of the started test (string) |
|
1220 @param doc documentation of the started test (string) |
|
1221 """ |
|
1222 if doc: |
|
1223 self.testsListWidget.insertItem(0, " {0}".format(doc)) |
|
1224 self.testsListWidget.insertItem(0, test) |
|
1225 if self.__dbs is None: |
|
1226 QApplication.processEvents() |
|
1227 |
|
1228 def testFinished(self): |
|
1229 """ |
|
1230 Public method called if a test has finished. |
|
1231 |
|
1232 <b>Note</b>: It is also called if it has already failed or errored. |
|
1233 """ |
|
1234 # update the counters |
|
1235 self.remainingCount -= 1 |
|
1236 self.runCount += 1 |
|
1237 self.progressCounterRunCount.setText(str(self.runCount)) |
|
1238 self.progressCounterRemCount.setText(str(self.remainingCount)) |
|
1239 |
|
1240 # update the progressbar |
|
1241 if self.errorCount: |
|
1242 self.__setProgressColor("red") |
|
1243 elif self.failCount: |
|
1244 self.__setProgressColor("orange") |
|
1245 self.progressProgressBar.setValue(self.runCount) |
|
1246 |
|
1247 def on_errorsListWidget_itemDoubleClicked(self, lbitem): |
|
1248 """ |
|
1249 Private slot called by doubleclicking an errorlist entry. |
|
1250 |
|
1251 It will popup a dialog showing the stacktrace. |
|
1252 If called from eric, an additional button is displayed |
|
1253 to show the python source in an eric source viewer (in |
|
1254 erics main window. |
|
1255 |
|
1256 @param lbitem the listbox item that was double clicked |
|
1257 """ |
|
1258 self.errListIndex = self.errorsListWidget.row(lbitem) |
|
1259 text = lbitem.text() |
|
1260 self.on_errorsListWidget_currentTextChanged(text) |
|
1261 |
|
1262 # get the error info |
|
1263 test, tracebackText = lbitem.data(UnittestDialog.ErrorsInfoRole) |
|
1264 |
|
1265 # now build the dialog |
|
1266 from .Ui_UnittestStacktraceDialog import Ui_UnittestStacktraceDialog |
|
1267 self.dlg = QDialog(self) |
|
1268 ui = Ui_UnittestStacktraceDialog() |
|
1269 ui.setupUi(self.dlg) |
|
1270 self.dlg.traceback = ui.traceback |
|
1271 |
|
1272 ui.showButton = ui.buttonBox.addButton( |
|
1273 self.tr("Show Source"), QDialogButtonBox.ButtonRole.ActionRole) |
|
1274 ui.showButton.clicked.connect(self.__showSource) |
|
1275 |
|
1276 ui.buttonBox.button( |
|
1277 QDialogButtonBox.StandardButton.Close).setDefault(True) |
|
1278 |
|
1279 self.dlg.setWindowTitle(text) |
|
1280 ui.testLabel.setText(test) |
|
1281 ui.traceback.setPlainText(tracebackText) |
|
1282 |
|
1283 # and now fire it up |
|
1284 self.dlg.show() |
|
1285 self.dlg.exec() |
|
1286 |
|
1287 def __showSource(self): |
|
1288 """ |
|
1289 Private slot to show the source of a traceback in an eric editor. |
|
1290 """ |
|
1291 # get the error info |
|
1292 tracebackLines = self.dlg.traceback.toPlainText().splitlines() |
|
1293 # find the last entry matching the pattern |
|
1294 for index in range(len(tracebackLines) - 1, -1, -1): |
|
1295 fmatch = re.search(r'File "(.*?)", line (\d*?),.*', |
|
1296 tracebackLines[index]) |
|
1297 if fmatch: |
|
1298 break |
|
1299 if fmatch: |
|
1300 fn, ln = fmatch.group(1, 2) |
|
1301 if self.__dbs: |
|
1302 # running as part of eric IDE |
|
1303 self.unittestFile.emit(fn, int(ln), True) |
|
1304 else: |
|
1305 self.__openEditor(fn, int(ln)) |
|
1306 |
|
1307 def hasFailedTests(self): |
|
1308 """ |
|
1309 Public method to check, if there are failed tests from the last run. |
|
1310 |
|
1311 @return flag indicating the presence of failed tests (boolean) |
|
1312 """ |
|
1313 return bool(self.__failedTests) |
|
1314 |
|
1315 def __openEditor(self, filename, linenumber): |
|
1316 """ |
|
1317 Private method to open an editor window for the given file. |
|
1318 |
|
1319 Note: This method opens an editor window when the unittest dialog |
|
1320 is called as a standalone application. |
|
1321 |
|
1322 @param filename path of the file to be opened |
|
1323 @type str |
|
1324 @param linenumber line number to place the cursor at |
|
1325 @type int |
|
1326 """ |
|
1327 from QScintilla.MiniEditor import MiniEditor |
|
1328 editor = MiniEditor(filename, "Python3", self) |
|
1329 editor.gotoLine(linenumber) |
|
1330 editor.show() |
|
1331 |
|
1332 self.__editors.append(editor) |
|
1333 |
|
1334 def closeEvent(self, event): |
|
1335 """ |
|
1336 Protected method to handle the close event. |
|
1337 |
|
1338 @param event close event |
|
1339 @type QCloseEvent |
|
1340 """ |
|
1341 event.accept() |
|
1342 |
|
1343 for editor in self.__editors: |
|
1344 with contextlib.suppress(Exception): |
|
1345 editor.close() |
|
1346 |
|
1347 |
|
1348 class QtTestResult(unittest.TestResult): |
|
1349 """ |
|
1350 A TestResult derivative to work with a graphical GUI. |
|
1351 |
|
1352 For more details see pyunit.py of the standard Python distribution. |
|
1353 """ |
|
1354 def __init__(self, parent, failfast): |
|
1355 """ |
|
1356 Constructor |
|
1357 |
|
1358 @param parent reference to the parent widget |
|
1359 @type UnittestDialog |
|
1360 @param failfast flag indicating to stop at the first error |
|
1361 @type bool |
|
1362 """ |
|
1363 super().__init__() |
|
1364 self.parent = parent |
|
1365 self.failfast = failfast |
|
1366 |
|
1367 def addFailure(self, test, err): |
|
1368 """ |
|
1369 Public method called if a test failed. |
|
1370 |
|
1371 @param test reference to the test object |
|
1372 @param err error traceback |
|
1373 """ |
|
1374 super().addFailure(test, err) |
|
1375 tracebackLines = self._exc_info_to_string(err, test) |
|
1376 self.parent.testFailed(str(test), tracebackLines, test.id()) |
|
1377 |
|
1378 def addError(self, test, err): |
|
1379 """ |
|
1380 Public method called if a test errored. |
|
1381 |
|
1382 @param test reference to the test object |
|
1383 @param err error traceback |
|
1384 """ |
|
1385 super().addError(test, err) |
|
1386 tracebackLines = self._exc_info_to_string(err, test) |
|
1387 self.parent.testErrored(str(test), tracebackLines, test.id()) |
|
1388 |
|
1389 def addSubTest(self, test, subtest, err): |
|
1390 """ |
|
1391 Public method called for each subtest to record its result. |
|
1392 |
|
1393 @param test reference to the test object |
|
1394 @param subtest reference to the subtest object |
|
1395 @param err error traceback |
|
1396 """ |
|
1397 if err is not None: |
|
1398 super().addSubTest(test, subtest, err) |
|
1399 tracebackLines = self._exc_info_to_string(err, test) |
|
1400 if issubclass(err[0], test.failureException): |
|
1401 self.parent.testFailed( |
|
1402 str(subtest), tracebackLines, test.id()) |
|
1403 else: |
|
1404 self.parent.testErrored( |
|
1405 str(subtest), tracebackLines, test.id()) |
|
1406 |
|
1407 def addSkip(self, test, reason): |
|
1408 """ |
|
1409 Public method called if a test was skipped. |
|
1410 |
|
1411 @param test reference to the test object |
|
1412 @param reason reason for skipping the test (string) |
|
1413 """ |
|
1414 super().addSkip(test, reason) |
|
1415 self.parent.testSkipped(str(test), reason, test.id()) |
|
1416 |
|
1417 def addExpectedFailure(self, test, err): |
|
1418 """ |
|
1419 Public method called if a test failed expected. |
|
1420 |
|
1421 @param test reference to the test object |
|
1422 @param err error traceback |
|
1423 """ |
|
1424 super().addExpectedFailure(test, err) |
|
1425 tracebackLines = self._exc_info_to_string(err, test) |
|
1426 self.parent.testFailedExpected(str(test), tracebackLines, test.id()) |
|
1427 |
|
1428 def addUnexpectedSuccess(self, test): |
|
1429 """ |
|
1430 Public method called if a test succeeded expectedly. |
|
1431 |
|
1432 @param test reference to the test object |
|
1433 """ |
|
1434 super().addUnexpectedSuccess(test) |
|
1435 self.parent.testSucceededUnexpected(str(test), test.id()) |
|
1436 |
|
1437 def startTest(self, test): |
|
1438 """ |
|
1439 Public method called at the start of a test. |
|
1440 |
|
1441 @param test Reference to the test object |
|
1442 """ |
|
1443 super().startTest(test) |
|
1444 self.parent.testStarted(str(test), test.shortDescription()) |
|
1445 |
|
1446 def stopTest(self, test): |
|
1447 """ |
|
1448 Public method called at the end of a test. |
|
1449 |
|
1450 @param test Reference to the test object |
|
1451 """ |
|
1452 super().stopTest(test) |
|
1453 self.parent.testFinished() |
|
1454 |
|
1455 |
|
1456 class UnittestWindow(EricMainWindow): |
|
1457 """ |
|
1458 Main window class for the standalone dialog. |
|
1459 """ |
|
1460 def __init__(self, prog=None, parent=None): |
|
1461 """ |
|
1462 Constructor |
|
1463 |
|
1464 @param prog filename of the program to open |
|
1465 @param parent reference to the parent widget (QWidget) |
|
1466 """ |
|
1467 super().__init__(parent) |
|
1468 self.cw = UnittestDialog(prog, parent=self) |
|
1469 self.cw.installEventFilter(self) |
|
1470 size = self.cw.size() |
|
1471 self.setCentralWidget(self.cw) |
|
1472 self.resize(size) |
|
1473 |
|
1474 self.setStyle(Preferences.getUI("Style"), |
|
1475 Preferences.getUI("StyleSheet")) |
|
1476 |
|
1477 self.cw.buttonBox.accepted.connect(self.close) |
|
1478 self.cw.buttonBox.rejected.connect(self.close) |
|
1479 |
|
1480 def eventFilter(self, obj, event): |
|
1481 """ |
|
1482 Public method to filter events. |
|
1483 |
|
1484 @param obj reference to the object the event is meant for (QObject) |
|
1485 @param event reference to the event object (QEvent) |
|
1486 @return flag indicating, whether the event was handled (boolean) |
|
1487 """ |
|
1488 if event.type() == QEvent.Type.Close: |
|
1489 QApplication.exit() |
|
1490 return True |
|
1491 |
|
1492 return False |
|
1493 |
|
1494 |
|
1495 def clearSavedHistories(self): |
|
1496 """ |
|
1497 Function to clear the saved history lists. |
|
1498 """ |
|
1499 Preferences.Prefs.rsettings.setValue( |
|
1500 recentNameUnittestDiscoverHistory, []) |
|
1501 Preferences.Prefs.rsettings.setValue( |
|
1502 recentNameUnittestFileHistory, []) |
|
1503 Preferences.Prefs.rsettings.setValue( |
|
1504 recentNameUnittestTestnameHistory, []) |
|
1505 |
|
1506 Preferences.Prefs.rsettings.sync() |
|