eric7/Testing/TestingWidget.py

branch
eric7-maintenance
changeset 9131
bc0c1b6d6adb
child 9192
a763d57e23bc
equal deleted inserted replaced
9112:9967ae9f0906 9131:bc0c1b6d6adb
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a widget to orchestrate unit test execution.
8 """
9
10 import contextlib
11 import enum
12 import locale
13 import os
14
15 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QCoreApplication
16 from PyQt6.QtWidgets import (
17 QAbstractButton, QComboBox, QDialogButtonBox, QWidget
18 )
19
20 from EricWidgets import EricMessageBox
21 from EricWidgets.EricApplication import ericApp
22 from EricWidgets.EricMainWindow import EricMainWindow
23 from EricWidgets.EricPathPicker import EricPathPickerModes
24
25 from .Ui_TestingWidget import Ui_TestingWidget
26
27 from .TestResultsTree import TestResultsModel, TestResultsTreeView
28 from .Interfaces import Frameworks
29 from .Interfaces.TestExecutorBase import (
30 TestConfig, TestResult, TestResultCategory
31 )
32 from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry
33
34 import Preferences
35 import UI.PixmapCache
36
37 from Globals import (
38 recentNameTestDiscoverHistory, recentNameTestFileHistory,
39 recentNameTestNameHistory, recentNameTestFramework,
40 recentNameTestEnvironment
41 )
42
43
44 class TestingWidgetModes(enum.Enum):
45 """
46 Class defining the various modes of the testing widget.
47 """
48 IDLE = 0 # idle, no test were run yet
49 RUNNING = 1 # test run being performed
50 STOPPED = 2 # test run finished
51
52
53 class TestingWidget(QWidget, Ui_TestingWidget):
54 """
55 Class implementing a widget to orchestrate unit test execution.
56
57 @signal testFile(str, int, bool) emitted to show the source of a
58 test file
59 @signal testRunStopped() emitted after a test run has finished
60 """
61 testFile = pyqtSignal(str, int, bool)
62 testRunStopped = pyqtSignal()
63
64 def __init__(self, testfile=None, parent=None):
65 """
66 Constructor
67
68 @param testfile file name of the test to load
69 @type str
70 @param parent reference to the parent widget (defaults to None)
71 @type QWidget (optional)
72 """
73 super().__init__(parent)
74 self.setupUi(self)
75
76 self.__resultsModel = TestResultsModel(self)
77 self.__resultsModel.summary.connect(self.__setStatusLabel)
78 self.__resultsTree = TestResultsTreeView(self)
79 self.__resultsTree.setModel(self.__resultsModel)
80 self.__resultsTree.goto.connect(self.__showSource)
81 self.resultsGroupBox.layout().addWidget(self.__resultsTree)
82
83 self.versionsButton.setIcon(
84 UI.PixmapCache.getIcon("info"))
85 self.clearHistoriesButton.setIcon(
86 UI.PixmapCache.getIcon("clearPrivateData"))
87
88 self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
89 self.testsuitePicker.setInsertPolicy(
90 QComboBox.InsertPolicy.InsertAtTop)
91 self.testsuitePicker.setSizeAdjustPolicy(
92 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
93
94 self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
95 self.discoveryPicker.setInsertPolicy(
96 QComboBox.InsertPolicy.InsertAtTop)
97 self.discoveryPicker.setSizeAdjustPolicy(
98 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
99
100 self.testComboBox.lineEdit().setClearButtonEnabled(True)
101
102 # create some more dialog buttons for orchestration
103 self.__showLogButton = self.buttonBox.addButton(
104 self.tr("Show Output..."),
105 QDialogButtonBox.ButtonRole.ActionRole)
106 self.__showLogButton.setToolTip(
107 self.tr("Show the output of the test runner process"))
108 self.__showLogButton.setWhatsThis(self.tr(
109 """<b>Show Output...</b"""
110 """<p>This button opens a dialog containing the output of the"""
111 """ test runner process of the most recent run.</p>"""))
112
113 self.__showCoverageButton = self.buttonBox.addButton(
114 self.tr("Show Coverage..."),
115 QDialogButtonBox.ButtonRole.ActionRole)
116 self.__showCoverageButton.setToolTip(
117 self.tr("Show code coverage in a new dialog"))
118 self.__showCoverageButton.setWhatsThis(self.tr(
119 """<b>Show Coverage...</b>"""
120 """<p>This button opens a dialog containing the collected code"""
121 """ coverage data.</p>"""))
122
123 self.__startButton = self.buttonBox.addButton(
124 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
125
126 self.__startButton.setToolTip(self.tr(
127 "Start the selected testsuite"))
128 self.__startButton.setWhatsThis(self.tr(
129 """<b>Start Test</b>"""
130 """<p>This button starts the test run.</p>"""))
131
132 self.__startFailedButton = self.buttonBox.addButton(
133 self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
134 self.__startFailedButton.setToolTip(
135 self.tr("Reruns failed tests of the selected testsuite"))
136 self.__startFailedButton.setWhatsThis(self.tr(
137 """<b>Rerun Failed</b>"""
138 """<p>This button reruns all failed tests of the most recent"""
139 """ test run.</p>"""))
140
141 self.__stopButton = self.buttonBox.addButton(
142 self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
143 self.__stopButton.setToolTip(self.tr("Stop the running test"))
144 self.__stopButton.setWhatsThis(self.tr(
145 """<b>Stop Test</b>"""
146 """<p>This button stops a running test.</p>"""))
147
148 self.setWindowFlags(
149 self.windowFlags() |
150 Qt.WindowType.WindowContextHelpButtonHint
151 )
152 self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
153 self.setWindowTitle(self.tr("Testing"))
154
155 try:
156 # we are called from within the eric IDE
157 self.__venvManager = ericApp().getObject("VirtualEnvManager")
158 self.__project = ericApp().getObject("Project")
159 self.__project.projectOpened.connect(self.__projectOpened)
160 self.__project.projectClosed.connect(self.__projectClosed)
161 except KeyError:
162 # we were called as a standalone application
163 from VirtualEnv.VirtualenvManager import VirtualenvManager
164 self.__venvManager = VirtualenvManager(self)
165 self.__venvManager.virtualEnvironmentAdded.connect(
166 self.__populateVenvComboBox)
167 self.__venvManager.virtualEnvironmentRemoved.connect(
168 self.__populateVenvComboBox)
169 self.__venvManager.virtualEnvironmentChanged.connect(
170 self.__populateVenvComboBox)
171 ericApp().registerObject("VirtualEnvManager", self.__venvManager)
172
173 self.__project = None
174
175 self.__discoverHistory = []
176 self.__fileHistory = []
177 self.__testNameHistory = []
178 self.__recentFramework = ""
179 self.__recentEnvironment = ""
180 self.__failedTests = []
181
182 self.__coverageFile = ""
183 self.__coverageDialog = None
184
185 self.__editors = []
186 self.__testExecutor = None
187 self.__recentLog = ""
188
189 # connect some signals
190 self.discoveryPicker.editTextChanged.connect(
191 self.__resetResults)
192 self.testsuitePicker.editTextChanged.connect(
193 self.__resetResults)
194 self.testComboBox.editTextChanged.connect(
195 self.__resetResults)
196
197 self.__frameworkRegistry = TestFrameworkRegistry()
198 for framework in Frameworks:
199 self.__frameworkRegistry.register(framework)
200
201 self.__setIdleMode()
202
203 self.__loadRecent()
204 self.__populateVenvComboBox()
205
206 if self.__project and self.__project.isOpen():
207 self.venvComboBox.setCurrentText(self.__project.getProjectVenv())
208 self.frameworkComboBox.setCurrentText(
209 self.__project.getProjectTestingFramework())
210 self.__insertDiscovery(self.__project.getProjectPath())
211 else:
212 self.__insertDiscovery("")
213
214 self.__insertTestFile(testfile)
215 self.__insertTestName("")
216
217 self.clearHistoriesButton.clicked.connect(self.clearRecent)
218
219 self.tabWidget.setCurrentIndex(0)
220
221 def __populateVenvComboBox(self):
222 """
223 Private method to (re-)populate the virtual environments selector.
224 """
225 currentText = self.venvComboBox.currentText()
226 if not currentText:
227 currentText = self.__recentEnvironment
228
229 self.venvComboBox.clear()
230 self.venvComboBox.addItem("")
231 self.venvComboBox.addItems(
232 sorted(self.__venvManager.getVirtualenvNames()))
233 self.venvComboBox.setCurrentText(currentText)
234
235 def __populateTestFrameworkComboBox(self):
236 """
237 Private method to (re-)populate the test framework selector.
238 """
239 currentText = self.frameworkComboBox.currentText()
240 if not currentText:
241 currentText = self.__recentFramework
242
243 self.frameworkComboBox.clear()
244
245 if bool(self.venvComboBox.currentText()):
246 interpreter = self.__venvManager.getVirtualenvInterpreter(
247 self.venvComboBox.currentText())
248 self.frameworkComboBox.addItem("")
249 for index, (name, executor) in enumerate(
250 sorted(self.__frameworkRegistry.getFrameworks().items()),
251 start=1
252 ):
253 isInstalled = executor.isInstalled(interpreter)
254 entry = (
255 name
256 if isInstalled else
257 self.tr("{0} (not available)").format(name)
258 )
259 self.frameworkComboBox.addItem(entry)
260 self.frameworkComboBox.model().item(index).setEnabled(
261 isInstalled)
262
263 self.frameworkComboBox.setCurrentText(self.__recentFramework)
264
265 def getResultsModel(self):
266 """
267 Public method to get a reference to the model containing the test
268 result data.
269
270 @return reference to the test results model
271 @rtype TestResultsModel
272 """
273 return self.__resultsModel
274
275 def hasFailedTests(self):
276 """
277 Public method to check for failed tests.
278
279 @return flag indicating the existence of failed tests
280 @rtype bool
281 """
282 return bool(self.__resultsModel.getFailedTests())
283
284 def getFailedTests(self):
285 """
286 Public method to get the list of failed tests (if any).
287
288 @return list of IDs of failed tests
289 @rtype list of str
290 """
291 return self.__failedTests[:]
292
293 @pyqtSlot(str)
294 def __insertHistory(self, widget, history, item):
295 """
296 Private slot to insert an item into a history object.
297
298 @param widget reference to the widget
299 @type QComboBox or EricComboPathPicker
300 @param history array containing the history
301 @type list of str
302 @param item item to be inserted
303 @type str
304 """
305 # prepend the given directory to the discovery picker
306 if item is None:
307 item = ""
308 if item in history:
309 history.remove(item)
310 history.insert(0, item)
311 widget.clear()
312 widget.addItems(history)
313 widget.setEditText(item)
314
315 @pyqtSlot(str)
316 def __insertDiscovery(self, start):
317 """
318 Private slot to insert the discovery start directory into the
319 discoveryPicker object.
320
321 @param start start directory name to be inserted
322 @type str
323 """
324 self.__insertHistory(self.discoveryPicker, self.__discoverHistory,
325 start)
326
327 @pyqtSlot(str)
328 def setTestFile(self, testFile, forProject=False):
329 """
330 Public slot to set the given test file as the current one.
331
332 @param testFile path of the test file
333 @type str
334 @param forProject flag indicating that this call is for a project
335 (defaults to False)
336 @type bool (optional)
337 """
338 if testFile:
339 self.__insertTestFile(testFile)
340
341 self.discoverCheckBox.setChecked(forProject or not bool(testFile))
342
343 if forProject:
344 self.__projectOpened()
345
346 self.tabWidget.setCurrentIndex(0)
347
348 @pyqtSlot(str)
349 def __insertTestFile(self, prog):
350 """
351 Private slot to insert a test file name into the testsuitePicker
352 object.
353
354 @param prog test file name to be inserted
355 @type str
356 """
357 self.__insertHistory(self.testsuitePicker, self.__fileHistory,
358 prog)
359
360 @pyqtSlot(str)
361 def __insertTestName(self, testName):
362 """
363 Private slot to insert a test name into the testComboBox object.
364
365 @param testName name of the test to be inserted
366 @type str
367 """
368 self.__insertHistory(self.testComboBox, self.__testNameHistory,
369 testName)
370
371 def __loadRecent(self):
372 """
373 Private method to load the most recently used lists.
374 """
375 Preferences.Prefs.rsettings.sync()
376
377 # 1. recently selected test framework and virtual environment
378 self.__recentEnvironment = Preferences.Prefs.rsettings.value(
379 recentNameTestEnvironment, "")
380 self.__recentFramework = Preferences.Prefs.rsettings.value(
381 recentNameTestFramework, "")
382
383 # 2. discovery history
384 self.__discoverHistory = []
385 rs = Preferences.Prefs.rsettings.value(
386 recentNameTestDiscoverHistory)
387 if rs is not None:
388 recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
389 self.__discoverHistory = recent[
390 :Preferences.getDebugger("RecentNumber")]
391
392 # 3. test file history
393 self.__fileHistory = []
394 rs = Preferences.Prefs.rsettings.value(
395 recentNameTestFileHistory)
396 if rs is not None:
397 recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
398 self.__fileHistory = recent[
399 :Preferences.getDebugger("RecentNumber")]
400
401 # 4. test name history
402 self.__testNameHistory = []
403 rs = Preferences.Prefs.rsettings.value(
404 recentNameTestNameHistory)
405 if rs is not None:
406 recent = [n for n in Preferences.toList(rs) if n]
407 self.__testNameHistory = recent[
408 :Preferences.getDebugger("RecentNumber")]
409
410 def __saveRecent(self):
411 """
412 Private method to save the most recently used lists.
413 """
414 Preferences.Prefs.rsettings.setValue(
415 recentNameTestEnvironment, self.__recentEnvironment)
416 Preferences.Prefs.rsettings.setValue(
417 recentNameTestFramework, self.__recentFramework)
418 Preferences.Prefs.rsettings.setValue(
419 recentNameTestDiscoverHistory, self.__discoverHistory)
420 Preferences.Prefs.rsettings.setValue(
421 recentNameTestFileHistory, self.__fileHistory)
422 Preferences.Prefs.rsettings.setValue(
423 recentNameTestNameHistory, self.__testNameHistory)
424
425 Preferences.Prefs.rsettings.sync()
426
427 @pyqtSlot()
428 def clearRecent(self):
429 """
430 Public slot to clear the recently used lists.
431 """
432 # clear histories
433 self.__discoverHistory = []
434 self.__fileHistory = []
435 self.__testNameHistory = []
436
437 # clear widgets with histories
438 self.discoveryPicker.clear()
439 self.testsuitePicker.clear()
440 self.testComboBox.clear()
441
442 # sync histories
443 self.__saveRecent()
444
445 @pyqtSlot()
446 def __resetResults(self):
447 """
448 Private slot to reset the test results tab and data.
449 """
450 self.__totalCount = 0
451 self.__runCount = 0
452
453 self.progressCounterRunCount.setText("0")
454 self.progressCounterRemCount.setText("0")
455 self.progressProgressBar.setMaximum(100)
456 self.progressProgressBar.setValue(0)
457
458 self.statusLabel.clear()
459
460 self.__resultsModel.clear()
461 self.__updateButtonBoxButtons()
462
463 @pyqtSlot()
464 def __updateButtonBoxButtons(self):
465 """
466 Private slot to update the state of the buttons of the button box.
467 """
468 failedAvailable = bool(self.__resultsModel.getFailedTests())
469
470 # Start button
471 if self.__mode in (
472 TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
473 ):
474 self.__startButton.setEnabled(
475 bool(self.venvComboBox.currentText()) and
476 bool(self.frameworkComboBox.currentText()) and
477 (
478 (self.discoverCheckBox.isChecked() and
479 bool(self.discoveryPicker.currentText())) or
480 bool(self.testsuitePicker.currentText())
481 )
482 )
483 self.__startButton.setDefault(
484 self.__mode == TestingWidgetModes.IDLE or
485 not failedAvailable
486 )
487 else:
488 self.__startButton.setEnabled(False)
489 self.__startButton.setDefault(False)
490
491 # Start Failed button
492 self.__startFailedButton.setEnabled(
493 self.__mode == TestingWidgetModes.STOPPED and
494 failedAvailable
495 )
496 self.__startFailedButton.setDefault(
497 self.__mode == TestingWidgetModes.STOPPED and
498 failedAvailable
499 )
500
501 # Stop button
502 self.__stopButton.setEnabled(
503 self.__mode == TestingWidgetModes.RUNNING)
504 self.__stopButton.setDefault(
505 self.__mode == TestingWidgetModes.RUNNING)
506
507 # Code coverage button
508 self.__showCoverageButton.setEnabled(
509 self.__mode == TestingWidgetModes.STOPPED and
510 bool(self.__coverageFile) and
511 (
512 (self.discoverCheckBox.isChecked() and
513 bool(self.discoveryPicker.currentText())) or
514 bool(self.testsuitePicker.currentText())
515 )
516 )
517
518 # Log output button
519 self.__showLogButton.setEnabled(bool(self.__recentLog))
520
521 # Close button
522 self.buttonBox.button(
523 QDialogButtonBox.StandardButton.Close
524 ).setEnabled(self.__mode in (
525 TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED
526 ))
527
528 @pyqtSlot()
529 def __updateProgress(self):
530 """
531 Private slot update the progress indicators.
532 """
533 self.progressCounterRunCount.setText(
534 str(self.__runCount))
535 self.progressCounterRemCount.setText(
536 str(self.__totalCount - self.__runCount))
537 self.progressProgressBar.setMaximum(self.__totalCount)
538 self.progressProgressBar.setValue(self.__runCount)
539
540 @pyqtSlot()
541 def __setIdleMode(self):
542 """
543 Private slot to switch the widget to idle mode.
544 """
545 self.__mode = TestingWidgetModes.IDLE
546 self.__updateButtonBoxButtons()
547 self.progressGroupBox.hide()
548 self.tabWidget.setCurrentIndex(0)
549
550 @pyqtSlot()
551 def __setRunningMode(self):
552 """
553 Private slot to switch the widget to running mode.
554 """
555 self.__mode = TestingWidgetModes.RUNNING
556
557 self.__totalCount = 0
558 self.__runCount = 0
559
560 self.__coverageFile = ""
561
562 self.sbLabel.setText(self.tr("Running"))
563 self.tabWidget.setCurrentIndex(1)
564 self.__updateButtonBoxButtons()
565 self.__updateProgress()
566
567 self.progressGroupBox.show()
568
569 @pyqtSlot()
570 def __setStoppedMode(self):
571 """
572 Private slot to switch the widget to stopped mode.
573 """
574 self.__mode = TestingWidgetModes.STOPPED
575 if self.__totalCount == 0:
576 self.progressProgressBar.setMaximum(100)
577
578 self.progressGroupBox.hide()
579
580 self.__updateButtonBoxButtons()
581
582 self.testRunStopped.emit()
583
584 self.raise_()
585 self.activateWindow()
586
587 @pyqtSlot(bool)
588 def on_discoverCheckBox_toggled(self, checked):
589 """
590 Private slot handling state changes of the 'discover' checkbox.
591
592 @param checked state of the checkbox
593 @type bool
594 """
595 if not bool(self.discoveryPicker.currentText()):
596 if self.__project and self.__project.isOpen():
597 self.__insertDiscovery(self.__project.getProjectPath())
598 else:
599 self.__insertDiscovery(
600 Preferences.getMultiProject("Workspace"))
601
602 self.__resetResults()
603
604 @pyqtSlot()
605 def on_testsuitePicker_aboutToShowPathPickerDialog(self):
606 """
607 Private slot called before the test file selection dialog is shown.
608 """
609 if self.__project:
610 # we were called from within eric
611 py3Extensions = ' '.join([
612 "*{0}".format(ext)
613 for ext in
614 ericApp().getObject("DebugServer").getExtensions('Python3')
615 ])
616 fileFilter = self.tr(
617 "Python3 Files ({0});;All Files (*)"
618 ).format(py3Extensions)
619 else:
620 # standalone application
621 fileFilter = self.tr("Python Files (*.py);;All Files (*)")
622 self.testsuitePicker.setFilters(fileFilter)
623
624 defaultDirectory = (
625 self.__project.getProjectPath()
626 if self.__project and self.__project.isOpen() else
627 Preferences.getMultiProject("Workspace")
628 )
629 if not defaultDirectory:
630 defaultDirectory = os.path.expanduser("~")
631 self.testsuitePicker.setDefaultDirectory(defaultDirectory)
632
633 @pyqtSlot(QAbstractButton)
634 def on_buttonBox_clicked(self, button):
635 """
636 Private slot called by a button of the button box clicked.
637
638 @param button button that was clicked
639 @type QAbstractButton
640 """
641 if button == self.__startButton:
642 self.startTests()
643 self.__saveRecent()
644 elif button == self.__stopButton:
645 self.__stopTests()
646 elif button == self.__startFailedButton:
647 self.startTests(failedOnly=True)
648 elif button == self.__showCoverageButton:
649 self.__showCoverageDialog()
650 elif button == self.__showLogButton:
651 self.__showLogOutput()
652
653 @pyqtSlot(int)
654 def on_venvComboBox_currentIndexChanged(self, index):
655 """
656 Private slot handling the selection of a virtual environment.
657
658 @param index index of the selected environment
659 @type int
660 """
661 self.__populateTestFrameworkComboBox()
662 self.__updateButtonBoxButtons()
663
664 self.versionsButton.setEnabled(bool(self.venvComboBox.currentText()))
665
666 self.__updateCoverage()
667
668 @pyqtSlot(int)
669 def on_frameworkComboBox_currentIndexChanged(self, index):
670 """
671 Private slot handling the selection of a test framework.
672
673 @param index index of the selected framework
674 @type int
675 """
676 self.__resetResults()
677 self.__updateCoverage()
678
679 @pyqtSlot()
680 def __updateCoverage(self):
681 """
682 Private slot to update the state of the coverage checkbox depending on
683 the selected framework's capabilities.
684 """
685 hasCoverage = False
686
687 venvName = self.venvComboBox.currentText()
688 if venvName:
689 framework = self.frameworkComboBox.currentText()
690 if framework:
691 interpreter = self.__venvManager.getVirtualenvInterpreter(
692 venvName)
693 executor = self.__frameworkRegistry.createExecutor(
694 framework, self)
695 hasCoverage = executor.hasCoverage(interpreter)
696
697 self.coverageCheckBox.setEnabled(hasCoverage)
698 if not hasCoverage:
699 self.coverageCheckBox.setChecked(False)
700
701 @pyqtSlot()
702 def on_versionsButton_clicked(self):
703 """
704 Private slot to show the versions of available plugins.
705 """
706 venvName = self.venvComboBox.currentText()
707 if venvName:
708 headerText = self.tr("<h3>Versions of Frameworks and their"
709 " Plugins</h3>")
710 versionsText = ""
711 interpreter = self.__venvManager.getVirtualenvInterpreter(venvName)
712 for framework in sorted(
713 self.__frameworkRegistry.getFrameworks().keys()
714 ):
715 executor = self.__frameworkRegistry.createExecutor(
716 framework, self)
717 versions = executor.getVersions(interpreter)
718 if versions:
719 txt = "<p><strong>{0} {1}</strong>".format(
720 versions["name"], versions["version"])
721
722 if versions["plugins"]:
723 txt += "<table>"
724 for pluginVersion in versions["plugins"]:
725 txt += self.tr(
726 "<tr><td>{0}</td><td>{1}</td></tr>"
727 ).format(
728 pluginVersion["name"], pluginVersion["version"]
729 )
730 txt += "</table>"
731 txt += "</p>"
732
733 versionsText += txt
734
735 if not versionsText:
736 versionsText = self.tr("No version information available.")
737
738 EricMessageBox.information(
739 self,
740 self.tr("Versions"),
741 headerText + versionsText
742 )
743
744 @pyqtSlot()
745 def startTests(self, failedOnly=False):
746 """
747 Public slot to start the test run.
748
749 @param failedOnly flag indicating to run only failed tests
750 @type bool
751 """
752 if self.__mode == TestingWidgetModes.RUNNING:
753 return
754
755 self.__recentLog = ""
756
757 self.__recentEnvironment = self.venvComboBox.currentText()
758 self.__recentFramework = self.frameworkComboBox.currentText()
759
760 self.__failedTests = (
761 self.__resultsModel.getFailedTests()
762 if failedOnly else
763 []
764 )
765 discover = self.discoverCheckBox.isChecked()
766 if discover:
767 discoveryStart = self.discoveryPicker.currentText()
768 testFileName = ""
769 testName = ""
770
771 if discoveryStart:
772 self.__insertDiscovery(discoveryStart)
773 else:
774 discoveryStart = ""
775 testFileName = self.testsuitePicker.currentText()
776 if testFileName:
777 self.__insertTestFile(testFileName)
778 testName = self.testComboBox.currentText()
779 if testName:
780 self.__insertTestName(testName)
781
782 self.sbLabel.setText(self.tr("Preparing Testsuite"))
783 QCoreApplication.processEvents()
784
785 if self.__project:
786 mainScript = self.__project.getMainScript(True)
787 coverageFile = (
788 os.path.splitext(mainScript)[0] + ".coverage"
789 if mainScript else
790 ""
791 )
792 else:
793 coverageFile = ""
794 interpreter = self.__venvManager.getVirtualenvInterpreter(
795 self.__recentEnvironment)
796 config = TestConfig(
797 interpreter=interpreter,
798 discover=discover,
799 discoveryStart=discoveryStart,
800 testFilename=testFileName,
801 testName=testName,
802 failFast=self.failfastCheckBox.isChecked(),
803 failedOnly=failedOnly,
804 collectCoverage=self.coverageCheckBox.isChecked(),
805 eraseCoverage=self.coverageEraseCheckBox.isChecked(),
806 coverageFile=coverageFile,
807 )
808
809 self.__testExecutor = self.__frameworkRegistry.createExecutor(
810 self.__recentFramework, self)
811 self.__testExecutor.collected.connect(self.__testsCollected)
812 self.__testExecutor.collectError.connect(self.__testsCollectError)
813 self.__testExecutor.startTest.connect(self.__testStarted)
814 self.__testExecutor.testResult.connect(self.__processTestResult)
815 self.__testExecutor.testFinished.connect(self.__testProcessFinished)
816 self.__testExecutor.testRunFinished.connect(self.__testRunFinished)
817 self.__testExecutor.stop.connect(self.__testsStopped)
818 self.__testExecutor.coverageDataSaved.connect(self.__coverageData)
819 self.__testExecutor.testRunAboutToBeStarted.connect(
820 self.__testRunAboutToBeStarted)
821
822 self.__setRunningMode()
823 self.__testExecutor.start(config, [])
824
825 @pyqtSlot()
826 def __stopTests(self):
827 """
828 Private slot to stop the current test run.
829 """
830 self.__testExecutor.stopIfRunning()
831
832 @pyqtSlot(list)
833 def __testsCollected(self, testNames):
834 """
835 Private slot handling the 'collected' signal of the executor.
836
837 @param testNames list of tuples containing the test id, the test name
838 and a description of collected tests
839 @type list of tuple of (str, str, str)
840 """
841 testResults = [
842 TestResult(
843 category=TestResultCategory.PENDING,
844 status=self.tr("pending"),
845 name=name,
846 id=id,
847 message=desc,
848 ) for id, name, desc in testNames
849 ]
850 self.__resultsModel.addTestResults(testResults)
851
852 self.__totalCount += len(testResults)
853 self.__updateProgress()
854
855 @pyqtSlot(list)
856 def __testsCollectError(self, errors):
857 """
858 Private slot handling the 'collectError' signal of the executor.
859
860 @param errors list of tuples containing the test name and a description
861 of the error
862 @type list of tuple of (str, str)
863 """
864 testResults = []
865
866 for testFile, error in errors:
867 if testFile:
868 testResults.append(TestResult(
869 category=TestResultCategory.FAIL,
870 status=self.tr("Failure"),
871 name=testFile,
872 id=testFile,
873 message=self.tr("Collection Error"),
874 extra=error.splitlines()
875 ))
876 else:
877 EricMessageBox.critical(
878 self,
879 self.tr("Collection Error"),
880 self.tr(
881 "<p>There was an error while collecting tests."
882 "</p><p>{0}</p>"
883 ).format("<br/>".join(error.splitlines()))
884 )
885
886 if testResults:
887 self.__resultsModel.addTestResults(testResults)
888
889 @pyqtSlot(tuple)
890 def __testStarted(self, test):
891 """
892 Private slot handling the 'startTest' signal of the executor.
893
894 @param test tuple containing the id, name and short description of the
895 tests about to be run
896 @type tuple of (str, str, str)
897 """
898 self.__resultsModel.updateTestResults([
899 TestResult(
900 category=TestResultCategory.RUNNING,
901 status=self.tr("running"),
902 id=test[0],
903 name=test[1],
904 message="" if test[2] is None else test[2],
905 )
906 ])
907
908 @pyqtSlot(TestResult)
909 def __processTestResult(self, result):
910 """
911 Private slot to handle the receipt of a test result object.
912
913 @param result test result object
914 @type TestResult
915 """
916 if not result.subtestResult:
917 self.__runCount += 1
918 self.__updateProgress()
919
920 self.__resultsModel.updateTestResults([result])
921
922 @pyqtSlot(list, str)
923 def __testProcessFinished(self, results, output):
924 """
925 Private slot to handle the 'testFinished' signal of the executor.
926
927 @param results list of test result objects (if not sent via the
928 'testResult' signal
929 @type list of TestResult
930 @param output string containing the test process output (if any)
931 @type str
932 """
933 self.__recentLog = output
934
935 self.__setStoppedMode()
936 self.__testExecutor = None
937
938 self.__adjustPendingState()
939
940 @pyqtSlot(int, float)
941 def __testRunFinished(self, noTests, duration):
942 """
943 Private slot to handle the 'testRunFinished' signal of the executor.
944
945 @param noTests number of tests run by the executor
946 @type int
947 @param duration time needed in seconds to run the tests
948 @type float
949 """
950 self.sbLabel.setText(
951 self.tr("Ran %n test(s) in {0}s", "", noTests).format(
952 locale.format_string("%.3f", duration, grouping=True)
953 )
954 )
955
956 self.__setStoppedMode()
957
958 @pyqtSlot()
959 def __testsStopped(self):
960 """
961 Private slot to handle the 'stop' signal of the executor.
962 """
963 self.sbLabel.setText(self.tr("Ran %n test(s)", "", self.__runCount))
964
965 self.__setStoppedMode()
966
967 @pyqtSlot()
968 def __testRunAboutToBeStarted(self):
969 """
970 Private slot to handle the 'testRunAboutToBeStarted' signal of the
971 executor.
972 """
973 self.__resultsModel.clear()
974
975 def __adjustPendingState(self):
976 """
977 Private method to change the status indicator of all still pending
978 tests to "not run".
979 """
980 newResults = []
981 for result in self.__resultsModel.getTestResults():
982 if result.category == TestResultCategory.PENDING:
983 result.category = TestResultCategory.SKIP
984 result.status = self.tr("not run")
985 newResults.append(result)
986
987 if newResults:
988 self.__resultsModel.updateTestResults(newResults)
989
990 @pyqtSlot(str)
991 def __coverageData(self, coverageFile):
992 """
993 Private slot to handle the 'coverageData' signal of the executor.
994
995 @param coverageFile file containing the coverage data
996 @type str
997 """
998 self.__coverageFile = coverageFile
999
1000 @pyqtSlot()
1001 def __showCoverageDialog(self):
1002 """
1003 Private slot to show a code coverage dialog for the most recent test
1004 run.
1005 """
1006 if self.__coverageDialog is None:
1007 from DataViews.PyCoverageDialog import PyCoverageDialog
1008 self.__coverageDialog = PyCoverageDialog(self)
1009 self.__coverageDialog.openFile.connect(self.__openEditor)
1010
1011 testDir = (
1012 self.discoveryPicker.currentText()
1013 if self.discoverCheckBox.isChecked() else
1014 os.path.dirname(self.testsuitePicker.currentText())
1015 )
1016 if testDir:
1017 self.__coverageDialog.show()
1018 self.__coverageDialog.start(self.__coverageFile, testDir)
1019
1020 @pyqtSlot()
1021 def __showLogOutput(self):
1022 """
1023 Private slot to show the output of the most recent test run.
1024 """
1025 from EricWidgets.EricPlainTextDialog import EricPlainTextDialog
1026 dlg = EricPlainTextDialog(
1027 title=self.tr("Test Run Output"),
1028 text=self.__recentLog
1029 )
1030 dlg.exec()
1031
1032 @pyqtSlot(str)
1033 def __setStatusLabel(self, statusText):
1034 """
1035 Private slot to set the status label to the text sent by the model.
1036
1037 @param statusText text to be shown
1038 @type str
1039 """
1040 self.statusLabel.setText(f"<b>{statusText}</b>")
1041
1042 @pyqtSlot()
1043 def __projectOpened(self):
1044 """
1045 Private slot to handle a project being opened.
1046 """
1047 self.venvComboBox.setCurrentText(self.__project.getProjectVenv())
1048 self.frameworkComboBox.setCurrentText(
1049 self.__project.getProjectTestingFramework())
1050 self.__insertDiscovery(self.__project.getProjectPath())
1051
1052 @pyqtSlot()
1053 def __projectClosed(self):
1054 """
1055 Private slot to handle a project being closed.
1056 """
1057 self.venvComboBox.setCurrentText("")
1058 self.frameworkComboBox.setCurrentText("")
1059 self.__insertDiscovery("")
1060
1061 @pyqtSlot(str, int)
1062 def __showSource(self, filename, lineno):
1063 """
1064 Private slot to show the source of a traceback in an editor.
1065
1066 @param filename file name of the file to be shown
1067 @type str
1068 @param lineno line number to go to in the file
1069 @type int
1070 """
1071 if self.__project:
1072 # running as part of eric IDE
1073 self.testFile.emit(filename, lineno, True)
1074 else:
1075 self.__openEditor(filename, lineno)
1076
1077 def __openEditor(self, filename, linenumber=1):
1078 """
1079 Private method to open an editor window for the given file.
1080
1081 Note: This method opens an editor window when the testing dialog
1082 is called as a standalone application.
1083
1084 @param filename path of the file to be opened
1085 @type str
1086 @param linenumber line number to place the cursor at (defaults to 1)
1087 @type int (optional)
1088 """
1089 from QScintilla.MiniEditor import MiniEditor
1090 editor = MiniEditor(filename, "Python3", self)
1091 editor.gotoLine(linenumber)
1092 editor.show()
1093
1094 self.__editors.append(editor)
1095
1096 def closeEvent(self, event):
1097 """
1098 Protected method to handle the close event.
1099
1100 @param event close event
1101 @type QCloseEvent
1102 """
1103 event.accept()
1104
1105 for editor in self.__editors:
1106 with contextlib.suppress(Exception):
1107 editor.close()
1108
1109
1110 class TestingWindow(EricMainWindow):
1111 """
1112 Main window class for the standalone dialog.
1113 """
1114 def __init__(self, testfile=None, parent=None):
1115 """
1116 Constructor
1117
1118 @param testfile file name of the test script to open
1119 @type str
1120 @param parent reference to the parent widget
1121 @type QWidget
1122 """
1123 super().__init__(parent)
1124 self.__cw = TestingWidget(testfile=testfile, parent=self)
1125 self.__cw.installEventFilter(self)
1126 size = self.__cw.size()
1127 self.setCentralWidget(self.__cw)
1128 self.resize(size)
1129
1130 self.setStyle(Preferences.getUI("Style"),
1131 Preferences.getUI("StyleSheet"))
1132
1133 self.__cw.buttonBox.accepted.connect(self.close)
1134 self.__cw.buttonBox.rejected.connect(self.close)
1135
1136 def eventFilter(self, obj, event):
1137 """
1138 Public method to filter events.
1139
1140 @param obj reference to the object the event is meant for (QObject)
1141 @param event reference to the event object (QEvent)
1142 @return flag indicating, whether the event was handled (boolean)
1143 """
1144 if event.type() == QEvent.Type.Close:
1145 QCoreApplication.exit(0)
1146 return True
1147
1148 return False
1149
1150
1151 def clearSavedHistories(self):
1152 """
1153 Function to clear the saved history lists.
1154 """
1155 Preferences.Prefs.rsettings.setValue(
1156 recentNameTestDiscoverHistory, [])
1157 Preferences.Prefs.rsettings.setValue(
1158 recentNameTestFileHistory, [])
1159 Preferences.Prefs.rsettings.setValue(
1160 recentNameTestNameHistory, [])
1161
1162 Preferences.Prefs.rsettings.sync()

eric ide

mercurial