eric7/Unittest/UnittestWidget.py

branch
unittest
changeset 9059
e7fd342f8bfc
child 9062
7f27bf3b50c3
equal deleted inserted replaced
9057:ddc46e93ccc4 9059:e7fd342f8bfc
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 enum
11 import os
12
13 from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QCoreApplication
14 from PyQt6.QtWidgets import (
15 QAbstractButton, QComboBox, QDialogButtonBox, QWidget
16 )
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_UnittestWidget import Ui_UnittestWidget
24
25 from .UTTestResultsTree import TestResultsModel, TestResultsTreeView
26 from .Interfaces import Frameworks
27 from .Interfaces.UTExecutorBase import UTTestConfig, UTTestResult
28 from .Interfaces.UTFrameworkRegistry import UTFrameworkRegistry
29
30 import Preferences
31 import UI.PixmapCache
32
33 from Globals import (
34 recentNameUnittestDiscoverHistory, recentNameUnittestFileHistory,
35 recentNameUnittestTestnameHistory, recentNameUnittestFramework,
36 recentNameUnittestEnvironment
37 )
38
39
40 class UnittestWidgetModes(enum.Enum):
41 """
42 Class defining the various modes of the unittest widget.
43 """
44 IDLE = 0 # idle, no test were run yet
45 RUNNING = 1 # test run being performed
46 STOPPED = 2 # test run finished
47
48
49 class UnittestWidget(QWidget, Ui_UnittestWidget):
50 """
51 Class implementing a widget to orchestrate unit test execution.
52 """
53 def __init__(self, testfile=None, parent=None):
54 """
55 Constructor
56
57 @param testfile file name of the test to load
58 @type str
59 @param parent reference to the parent widget (defaults to None)
60 @type QWidget (optional)
61 """
62 super().__init__(parent)
63 self.setupUi(self)
64
65 self.__resultsModel = TestResultsModel(self)
66 self.__resultsTree = TestResultsTreeView(self)
67 self.__resultsTree.setModel(self.__resultsModel)
68 self.resultsGroupBox.layout().addWidget(self.__resultsTree)
69
70 self.versionsButton.setIcon(
71 UI.PixmapCache.getIcon("info"))
72 self.clearHistoriesButton.setIcon(
73 UI.PixmapCache.getIcon("clearPrivateData"))
74
75 self.testsuitePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
76 self.testsuitePicker.setInsertPolicy(
77 QComboBox.InsertPolicy.InsertAtTop)
78 self.testsuitePicker.setSizeAdjustPolicy(
79 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
80
81 self.discoveryPicker.setMode(EricPathPickerModes.DIRECTORY_MODE)
82 self.discoveryPicker.setInsertPolicy(
83 QComboBox.InsertPolicy.InsertAtTop)
84 self.discoveryPicker.setSizeAdjustPolicy(
85 QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
86
87 self.testComboBox.lineEdit().setClearButtonEnabled(True)
88
89 # create some more dialog buttons for orchestration
90 self.__startButton = self.buttonBox.addButton(
91 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole)
92
93 self.__startButton.setToolTip(self.tr(
94 "Start the selected testsuite"))
95 self.__startButton.setWhatsThis(self.tr(
96 """<b>Start Test</b>"""
97 """<p>This button starts the selected testsuite.</p>"""))
98
99 # TODO: implement "Rerun Failed"
100 ## self.__startFailedButton = self.buttonBox.addButton(
101 ## self.tr("Rerun Failed"), QDialogButtonBox.ButtonRole.ActionRole)
102 ## self.__startFailedButton.setToolTip(
103 ## self.tr("Reruns failed tests of the selected testsuite"))
104 ## self.__startFailedButton.setWhatsThis(self.tr(
105 ## """<b>Rerun Failed</b>"""
106 ## """<p>This button reruns all failed tests of the selected"""
107 ## """ testsuite.</p>"""))
108 ##
109 self.__stopButton = self.buttonBox.addButton(
110 self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole)
111 self.__stopButton.setToolTip(self.tr("Stop the running unittest"))
112 self.__stopButton.setWhatsThis(self.tr(
113 """<b>Stop Test</b>"""
114 """<p>This button stops a running unittest.</p>"""))
115
116 self.__stopButton.setEnabled(False)
117 self.__startButton.setDefault(True)
118 self.__startButton.setEnabled(False)
119 ## self.__startFailedButton.setEnabled(False)
120
121 self.setWindowFlags(
122 self.windowFlags() |
123 Qt.WindowType.WindowContextHelpButtonHint
124 )
125 self.setWindowIcon(UI.PixmapCache.getIcon("eric"))
126 self.setWindowTitle(self.tr("Unittest"))
127
128 from VirtualEnv.VirtualenvManager import VirtualenvManager
129 self.__venvManager = VirtualenvManager(self)
130 self.__venvManager.virtualEnvironmentAdded.connect(
131 self.__populateVenvComboBox)
132 self.__venvManager.virtualEnvironmentRemoved.connect(
133 self.__populateVenvComboBox)
134 self.__venvManager.virtualEnvironmentChanged.connect(
135 self.__populateVenvComboBox)
136
137 # TODO: implement project mode
138 self.__forProject = False
139
140 self.__discoverHistory = []
141 self.__fileHistory = []
142 self.__testNameHistory = []
143 self.__recentFramework = ""
144 self.__recentEnvironment = ""
145
146 self.__failedTests = set()
147
148 self.__editors = []
149 self.__testExecutor = None
150
151 # connect some signals
152 self.frameworkComboBox.currentIndexChanged.connect(
153 self.__updateButtonBoxButtons)
154 self.discoverCheckBox.toggled.connect(
155 self.__updateButtonBoxButtons)
156 self.discoveryPicker.editTextChanged.connect(
157 self.__updateButtonBoxButtons)
158 self.testsuitePicker.editTextChanged.connect(
159 self.__updateButtonBoxButtons)
160
161 self.__frameworkRegistry = UTFrameworkRegistry()
162 for framework in Frameworks:
163 self.__frameworkRegistry.register(framework)
164
165 self.__setIdleMode()
166
167 self.__loadRecent()
168 self.__populateVenvComboBox()
169
170 if self.__forProject:
171 project = ericApp().getObject("Project")
172 if project.isOpen():
173 self.__insertDiscovery(project.getProjectPath())
174 else:
175 self.__insertDiscovery("")
176 else:
177 self.__insertDiscovery("")
178 self.__insertProg(testfile)
179 self.__insertTestName("")
180
181 self.clearHistoriesButton.clicked.connect(self.clearRecent)
182
183 self.tabWidget.setCurrentIndex(0)
184
185 def __populateVenvComboBox(self):
186 """
187 Private method to (re-)populate the virtual environments selector.
188 """
189 currentText = self.venvComboBox.currentText()
190 if not currentText:
191 currentText = self.__recentEnvironment
192
193 self.venvComboBox.clear()
194 self.venvComboBox.addItem("")
195 self.venvComboBox.addItems(
196 sorted(self.__venvManager.getVirtualenvNames()))
197 index = self.venvComboBox.findText(currentText)
198 if index < 0:
199 index = 0
200 self.venvComboBox.setCurrentIndex(index)
201
202 def __populateTestFrameworkComboBox(self):
203 """
204 Private method to (re-)populate the test framework selector.
205 """
206 currentText = self.frameworkComboBox.currentText()
207 if not currentText:
208 currentText = self.__recentFramework
209
210 self.frameworkComboBox.clear()
211
212 if bool(self.venvComboBox.currentText()):
213 interpreter = self.__venvManager.getVirtualenvInterpreter(
214 self.venvComboBox.currentText())
215 self.frameworkComboBox.addItem("")
216 for index, (name, executor) in enumerate(
217 sorted(self.__frameworkRegistry.getFrameworks().items()),
218 start=1
219 ):
220 isInstalled = executor.isInstalled(interpreter)
221 entry = (
222 name
223 if isInstalled else
224 self.tr("{0} (not available)").format(name)
225 )
226 self.frameworkComboBox.addItem(entry)
227 self.frameworkComboBox.model().item(index).setEnabled(
228 isInstalled)
229
230 self.frameworkComboBox.setCurrentText(self.__recentFramework)
231
232 @pyqtSlot(str)
233 def __insertHistory(self, widget, history, item):
234 """
235 Private slot to insert an item into a history object.
236
237 @param widget reference to the widget
238 @type QComboBox or EricComboPathPicker
239 @param history array containing the history
240 @type list of str
241 @param item item to be inserted
242 @type str
243 """
244 current = widget.currentText()
245
246 # prepend the given directory to the discovery picker
247 if item is None:
248 item = ""
249 if item in history:
250 history.remove(item)
251 history.insert(0, item)
252 widget.clear()
253 widget.addItems(history)
254
255 if current:
256 widget.setText(current)
257
258 @pyqtSlot(str)
259 def __insertDiscovery(self, start):
260 """
261 Private slot to insert the discovery start directory into the
262 discoveryPicker object.
263
264 @param start start directory name to be inserted
265 @type str
266 """
267 self.__insertHistory(self.discoveryPicker, self.__discoverHistory,
268 start)
269
270 @pyqtSlot(str)
271 def __insertProg(self, prog):
272 """
273 Private slot to insert a test file name into the testsuitePicker
274 object.
275
276 @param prog test file name to be inserted
277 @type str
278 """
279 self.__insertHistory(self.testsuitePicker, self.__fileHistory,
280 prog)
281
282 @pyqtSlot(str)
283 def __insertTestName(self, testName):
284 """
285 Private slot to insert a test name into the testComboBox object.
286
287 @param testName name of the test to be inserted
288 @type str
289 """
290 self.__insertHistory(self.testComboBox, self.__testNameHistory,
291 testName)
292
293 def __loadRecent(self):
294 """
295 Private method to load the most recently used lists.
296 """
297 Preferences.Prefs.rsettings.sync()
298
299 # 1. recently selected test framework and virtual environment
300 self.__recentEnvironment = Preferences.Prefs.rsettings.value(
301 recentNameUnittestEnvironment, "")
302 self.__recentFramework = Preferences.Prefs.rsettings.value(
303 recentNameUnittestFramework, "")
304
305 # 2. discovery history
306 self.__discoverHistory = []
307 rs = Preferences.Prefs.rsettings.value(
308 recentNameUnittestDiscoverHistory)
309 if rs is not None:
310 recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
311 self.__discoverHistory = recent[
312 :Preferences.getDebugger("RecentNumber")]
313
314 # 3. test file history
315 self.__fileHistory = []
316 rs = Preferences.Prefs.rsettings.value(
317 recentNameUnittestFileHistory)
318 if rs is not None:
319 recent = [f for f in Preferences.toList(rs) if os.path.exists(f)]
320 self.__fileHistory = recent[
321 :Preferences.getDebugger("RecentNumber")]
322
323 # 4. test name history
324 self.__testNameHistory = []
325 rs = Preferences.Prefs.rsettings.value(
326 recentNameUnittestTestnameHistory)
327 if rs is not None:
328 recent = [n for n in Preferences.toList(rs) if n]
329 self.__testNameHistory = recent[
330 :Preferences.getDebugger("RecentNumber")]
331
332 def __saveRecent(self):
333 """
334 Private method to save the most recently used lists.
335 """
336 Preferences.Prefs.rsettings.setValue(
337 recentNameUnittestEnvironment, self.__recentEnvironment)
338 Preferences.Prefs.rsettings.setValue(
339 recentNameUnittestFramework, self.__recentFramework)
340 Preferences.Prefs.rsettings.setValue(
341 recentNameUnittestDiscoverHistory, self.__discoverHistory)
342 Preferences.Prefs.rsettings.setValue(
343 recentNameUnittestFileHistory, self.__fileHistory)
344 Preferences.Prefs.rsettings.setValue(
345 recentNameUnittestTestnameHistory, self.__testNameHistory)
346
347 Preferences.Prefs.rsettings.sync()
348
349 @pyqtSlot()
350 def clearRecent(self):
351 """
352 Public slot to clear the recently used lists.
353 """
354 # clear histories
355 self.__discoverHistory = []
356 self.__fileHistory = []
357 self.__testNameHistory = []
358
359 # clear widgets with histories
360 self.discoveryPicker.clear()
361 self.testsuitePicker.clear()
362 self.testComboBox.clear()
363
364 # sync histories
365 self.__saveRecent()
366
367 def __updateButtonBoxButtons(self):
368 """
369 Private method to update the state of the buttons of the button box.
370 """
371 failedAvailable = bool(self.__failedTests)
372
373 # Start button
374 if self.__mode in (
375 UnittestWidgetModes.IDLE, UnittestWidgetModes.STOPPED
376 ):
377 self.__startButton.setEnabled(
378 bool(self.venvComboBox.currentText()) and
379 bool(self.frameworkComboBox.currentText()) and
380 (
381 (self.discoverCheckBox.isChecked() and
382 bool(self.discoveryPicker.currentText())) or
383 bool(self.testsuitePicker.currentText())
384 )
385 )
386 self.__startButton.setDefault(
387 self.__mode == UnittestWidgetModes.IDLE or
388 not failedAvailable
389 )
390 else:
391 self.__startButton.setEnabled(False)
392 self.__startButton.setDefault(False)
393
394 # Start Failed button
395 # TODO: not implemented yet
396
397 # Stop button
398 self.__stopButton.setEnabled(
399 self.__mode == UnittestWidgetModes.RUNNING)
400 self.__stopButton.setDefault(
401 self.__mode == UnittestWidgetModes.RUNNING)
402
403 def __setIdleMode(self):
404 """
405 Private method to switch the widget to idle mode.
406 """
407 self.__mode = UnittestWidgetModes.IDLE
408 self.__updateButtonBoxButtons()
409
410 def __setRunningMode(self):
411 """
412 Private method to switch the widget to running mode.
413 """
414 # TODO: not implemented yet
415 pass
416
417 def __setStoppedMode(self):
418 """
419 Private method to switch the widget to stopped mode.
420 """
421 # TODO: not implemented yet
422 pass
423
424 @pyqtSlot(QAbstractButton)
425 def on_buttonBox_clicked(self, button):
426 """
427 Private slot called by a button of the button box clicked.
428
429 @param button button that was clicked
430 @type QAbstractButton
431 """
432 ## if button == self.discoverButton:
433 ## self.__discover()
434 ## self.__saveRecent()
435 ## elif button == self.__startButton:
436 if button == self.__startButton:
437 self.startTests()
438 self.__saveRecent()
439 elif button == self.__stopButton:
440 self.__stopTests()
441 # elif button == self.__startFailedButton:
442 # self.startTests(failedOnly=True)
443
444 @pyqtSlot(int)
445 def on_venvComboBox_currentIndexChanged(self, index):
446 """
447 Private slot handling the selection of a virtual environment.
448
449 @param index index of the selected environment
450 @type int
451 """
452 self.__populateTestFrameworkComboBox()
453 self.__updateButtonBoxButtons()
454
455 self.versionsButton.setEnabled(bool(self.venvComboBox.currentText()))
456
457 @pyqtSlot()
458 def on_versionsButton_clicked(self):
459 """
460 Private slot to show the versions of available plugins.
461 """
462 venvName = self.venvComboBox.currentText()
463 if venvName:
464 headerText = self.tr("<h3>Versions of Frameworks and their"
465 " Plugins</h3>")
466 versionsText = ""
467 interpreter = self.__venvManager.getVirtualenvInterpreter(venvName)
468 for framework in sorted(
469 self.__frameworkRegistry.getFrameworks().keys()
470 ):
471 executor = self.__frameworkRegistry.createExecutor(
472 framework, self)
473 versions = executor.getVersions(interpreter)
474 if versions:
475 txt = "<p><strong>{0} {1}</strong>".format(
476 versions["name"], versions["version"])
477
478 if versions["plugins"]:
479 txt += "<table>"
480 for pluginVersion in versions["plugins"]:
481 txt += self.tr(
482 "<tr><td>{0}</td><td>{1}</td></tr>"
483 ).format(
484 pluginVersion["name"], pluginVersion["version"]
485 )
486 txt += "</table>"
487 txt += "</p>"
488
489 versionsText += txt
490
491 if not versionsText:
492 versionsText = self.tr("No version information available.")
493
494 EricMessageBox.information(
495 self,
496 self.tr("Versions"),
497 headerText + versionsText
498 )
499
500 @pyqtSlot()
501 def startTests(self, failedOnly=False):
502 """
503 Public slot to start the test run.
504
505 @param failedOnly flag indicating to run only failed tests
506 @type bool
507 """
508 if self.__mode == UnittestWidgetModes.RUNNING:
509 return
510
511 self.__recentEnvironment = self.venvComboBox.currentText()
512 self.__recentFramework = self.frameworkComboBox.currentText()
513
514 discover = self.discoverCheckBox.isChecked()
515 if discover:
516 discoveryStart = self.discoveryPicker.currentText()
517 testFileName = ""
518 testName = ""
519
520 if discoveryStart:
521 self.__insertDiscovery(discoveryStart)
522 else:
523 discoveryStart = ""
524 testFileName = self.testsuitePicker.currentText()
525 if testFileName:
526 self.__insertProg(testFileName)
527 testName = self.testComboBox.currentText()
528 if testName:
529 self.insertTestName(testName)
530 if testFileName and not testName:
531 testName = "suite"
532
533 interpreter = self.__venvManager.getVirtualenvInterpreter(
534 self.__recentEnvironment)
535 config = UTTestConfig(
536 interpreter=interpreter,
537 discover=self.discoverCheckBox.isChecked(),
538 discoveryStart=discoveryStart,
539 testFilename=testFileName,
540 testName=testName,
541 failFast=self.failfastCheckBox.isChecked(),
542 collectCoverage=self.coverageCheckBox.isChecked(),
543 eraseCoverage=self.coverageEraseCheckBox.isChecked(),
544 )
545
546 self.__resultsModel.clear()
547 self.__testExecutor = self.__frameworkRegistry.createExecutor(
548 self.__recentFramework, self)
549 self.__testExecutor.collected.connect(self.__testCollected)
550 self.__testExecutor.collectError.connect(self.__testsCollectError)
551 self.__testExecutor.startTest.connect(self.__testsStarted)
552 self.__testExecutor.testResult.connect(self.__processTestResult)
553 self.__testExecutor.testFinished.connect(self.__testProcessFinished)
554 self.__testExecutor.stop.connect(self.__testsStopped)
555 self.__testExecutor.start(config, [])
556
557 # TODO: not yet implemented
558 pass
559
560 @pyqtSlot(list)
561 def __testCollected(self, testNames):
562 """
563 Private slot handling the 'collected' signal of the executor.
564
565 @param testNames list of names of collected tests
566 @type list of str
567 """
568 # TODO: not implemented yet
569 pass
570
571 @pyqtSlot(list)
572 def __testsCollectError(self, errors):
573 """
574 Private slot handling the 'collectError' signal of the executor.
575
576 @param errors list of tuples containing the test name and a description
577 of the error
578 @type list of tuple of (str, str)
579 """
580 # TODO: not implemented yet
581 pass
582
583 @pyqtSlot(list)
584 def __testsStarted(self, testNames):
585 """
586 Private slot handling the 'startTest' signal of the executor.
587
588 @param testNames list of names of tests about to be run
589 @type list of str
590 """
591 # TODO: not implemented yet
592 pass
593
594 @pyqtSlot(UTTestResult)
595 def __processTestResult(self, result):
596 """
597 Private slot to handle the receipt of a test result object.
598
599 @param result test result object
600 @type UTTestResult
601 """
602 # TODO: not implemented yet
603 pass
604
605 @pyqtSlot(list, str)
606 def __testProcessFinished(self, results, output):
607 """
608 Private slot to handle the 'testFinished' signal of the executor.
609
610 @param results list of test result objects (if not sent via the
611 'testResult' signal
612 @type list of UTTestResult
613 @param output string containing the test process output (if any)
614 @type str
615 """
616 # TODO: not implemented yet
617 pass
618
619 @pyqtSlot()
620 def __testsStopped(self):
621 """
622 Private slot to handle the 'stop' signal of the executor.
623 """
624 # TODO: not implemented yet
625 pass
626
627
628 class UnittestWindow(EricMainWindow):
629 """
630 Main window class for the standalone dialog.
631 """
632 def __init__(self, testfile=None, parent=None):
633 """
634 Constructor
635
636 @param testfile file name of the test script to open
637 @type str
638 @param parent reference to the parent widget
639 @type QWidget
640 """
641 super().__init__(parent)
642 self.__cw = UnittestWidget(testfile=testfile, parent=self)
643 self.__cw.installEventFilter(self)
644 size = self.__cw.size()
645 self.setCentralWidget(self.__cw)
646 self.resize(size)
647
648 self.setStyle(Preferences.getUI("Style"),
649 Preferences.getUI("StyleSheet"))
650
651 self.__cw.buttonBox.accepted.connect(self.close)
652 self.__cw.buttonBox.rejected.connect(self.close)
653
654 def eventFilter(self, obj, event):
655 """
656 Public method to filter events.
657
658 @param obj reference to the object the event is meant for (QObject)
659 @param event reference to the event object (QEvent)
660 @return flag indicating, whether the event was handled (boolean)
661 """
662 if event.type() == QEvent.Type.Close:
663 QCoreApplication.exit(0)
664 return True
665
666 return False
667
668
669 def clearSavedHistories(self):
670 """
671 Function to clear the saved history lists.
672 """
673 Preferences.Prefs.rsettings.setValue(
674 recentNameUnittestDiscoverHistory, [])
675 Preferences.Prefs.rsettings.setValue(
676 recentNameUnittestFileHistory, [])
677 Preferences.Prefs.rsettings.setValue(
678 recentNameUnittestTestnameHistory, [])
679
680 Preferences.Prefs.rsettings.sync()

eric ide

mercurial