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