eric6/PyUnit/UnittestDialog.py

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

eric ide

mercurial