eric7/Testing/TestingWidget.py

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

eric ide

mercurial