eric7/PyUnit/UnittestDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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

eric ide

mercurial