src/eric7/Testing/TestingWidget.py

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

eric ide

mercurial