src/eric7/Testing/TestingWidget.py

branch
eric7
changeset 10405
df7e1694d0eb
parent 10404
f7d9c31f0c38
child 10413
2ecbe43a8e88
equal deleted inserted replaced
10404:f7d9c31f0c38 10405:df7e1694d0eb
11 import enum 11 import enum
12 import locale 12 import locale
13 import os 13 import os
14 14
15 from PyQt6.QtCore import QCoreApplication, QEvent, Qt, pyqtSignal, pyqtSlot 15 from PyQt6.QtCore import QCoreApplication, QEvent, Qt, pyqtSignal, pyqtSlot
16 from PyQt6.QtWidgets import QAbstractButton, QComboBox, QDialogButtonBox, QWidget 16 from PyQt6.QtWidgets import (
17 QAbstractButton,
18 QComboBox,
19 QDialogButtonBox,
20 QTreeWidgetItem,
21 QWidget,
22 )
17 23
18 from eric7 import Preferences 24 from eric7 import Preferences
19 from eric7.DataViews.PyCoverageDialog import PyCoverageDialog 25 from eric7.DataViews.PyCoverageDialog import PyCoverageDialog
20 from eric7.EricGui import EricPixmapCache 26 from eric7.EricGui import EricPixmapCache
21 from eric7.EricWidgets import EricMessageBox 27 from eric7.EricWidgets import EricMessageBox
31 ) 37 )
32 38
33 from .Interfaces import Frameworks 39 from .Interfaces import Frameworks
34 from .Interfaces.TestExecutorBase import TestConfig, TestResult, TestResultCategory 40 from .Interfaces.TestExecutorBase import TestConfig, TestResult, TestResultCategory
35 from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry 41 from .Interfaces.TestFrameworkRegistry import TestFrameworkRegistry
36 from .TestResultsTree import TestResultsFilterModel, TestResultsModel, TestResultsTreeView 42 from .TestResultsTree import (
43 TestResultsFilterModel,
44 TestResultsModel,
45 TestResultsTreeView,
46 )
37 from .Ui_TestingWidget import Ui_TestingWidget 47 from .Ui_TestingWidget import Ui_TestingWidget
38 48
39 49
40 class TestingWidgetModes(enum.Enum): 50 class TestingWidgetModes(enum.Enum):
41 """ 51 """
43 """ 53 """
44 54
45 IDLE = 0 # idle, no test were run yet 55 IDLE = 0 # idle, no test were run yet
46 RUNNING = 1 # test run being performed 56 RUNNING = 1 # test run being performed
47 STOPPED = 2 # test run finished 57 STOPPED = 2 # test run finished
58 DISCOVERY = 3 # discovery of tests being performed
48 59
49 60
50 class TestingWidget(QWidget, Ui_TestingWidget): 61 class TestingWidget(QWidget, Ui_TestingWidget):
51 """ 62 """
52 Class implementing a widget to orchestrate unit test execution. 63 Class implementing a widget to orchestrate unit test execution.
56 @signal testRunStopped() emitted after a test run has finished 67 @signal testRunStopped() emitted after a test run has finished
57 """ 68 """
58 69
59 testFile = pyqtSignal(str, int, bool) 70 testFile = pyqtSignal(str, int, bool)
60 testRunStopped = pyqtSignal() 71 testRunStopped = pyqtSignal()
72
73 TestCaseNameRole = Qt.ItemDataRole.UserRole
74 TestCaseFileRole = Qt.ItemDataRole.UserRole + 1
75 TestCaseLinenoRole = Qt.ItemDataRole.UserRole + 2
76 TestCaseIdRole = Qt.ItemDataRole.UserRole + 3
61 77
62 def __init__(self, testfile=None, parent=None): 78 def __init__(self, testfile=None, parent=None):
63 """ 79 """
64 Constructor 80 Constructor
65 81
130 """<p>This button opens a dialog containing the collected code""" 146 """<p>This button opens a dialog containing the collected code"""
131 """ coverage data.</p>""" 147 """ coverage data.</p>"""
132 ) 148 )
133 ) 149 )
134 150
151 self.__discoverButton = self.buttonBox.addButton(
152 self.tr("Discover"), QDialogButtonBox.ButtonRole.ActionRole
153 )
154 self.__discoverButton.setToolTip(self.tr("Discover Tests"))
155 self.__discoverButton.setWhatsThis(
156 self.tr(
157 """<b>Discover Tests</b>"""
158 """<p>This button starts a discovery of available tests.</p>"""
159 )
160 )
161
135 self.__startButton = self.buttonBox.addButton( 162 self.__startButton = self.buttonBox.addButton(
136 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole 163 self.tr("Start"), QDialogButtonBox.ButtonRole.ActionRole
137 ) 164 )
138 165
139 self.__startButton.setToolTip(self.tr("Start the selected testsuite")) 166 self.__startButton.setToolTip(self.tr("Start the selected test suite"))
140 self.__startButton.setWhatsThis( 167 self.__startButton.setWhatsThis(
141 self.tr("""<b>Start Test</b><p>This button starts the test run.</p>""") 168 self.tr("""<b>Start Test</b><p>This button starts the test run.</p>""")
142 ) 169 )
143 170
144 self.__startFailedButton = self.buttonBox.addButton( 171 self.__startFailedButton = self.buttonBox.addButton(
498 """ 525 """
499 Private slot to update the state of the buttons of the button box. 526 Private slot to update the state of the buttons of the button box.
500 """ 527 """
501 failedAvailable = bool(self.__resultsModel.getFailedTests()) 528 failedAvailable = bool(self.__resultsModel.getFailedTests())
502 529
530 # Discover button
531 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED):
532 self.__discoverButton.setEnabled(
533 bool(self.venvComboBox.currentText())
534 and bool(self.frameworkComboBox.currentText())
535 and (
536 (
537 self.discoverCheckBox.isChecked()
538 and bool(self.discoveryPicker.currentText())
539 )
540 )
541 )
542 else:
543 self.__discoverButton.setEnabled(False)
544 self.__discoverButton.setDefault(False)
545
503 # Start button 546 # Start button
504 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED): 547 if self.__mode in (TestingWidgetModes.IDLE, TestingWidgetModes.STOPPED):
505 self.__startButton.setEnabled( 548 self.__startButton.setEnabled(
506 bool(self.venvComboBox.currentText()) 549 bool(self.venvComboBox.currentText())
507 and bool(self.frameworkComboBox.currentText()) 550 and bool(self.frameworkComboBox.currentText())
571 self.__mode = TestingWidgetModes.IDLE 614 self.__mode = TestingWidgetModes.IDLE
572 self.__updateButtonBoxButtons() 615 self.__updateButtonBoxButtons()
573 self.progressGroupBox.hide() 616 self.progressGroupBox.hide()
574 self.tabWidget.setCurrentIndex(0) 617 self.tabWidget.setCurrentIndex(0)
575 618
619 self.raise_()
620 self.activateWindow()
621
622 @pyqtSlot()
623 def __setDiscoverMode(self):
624 """
625 Private slot to switch the widget to test discovery mode.
626 """
627 self.__mode = TestingWidgetModes.DISCOVERY
628
629 self.__totalCount = 0
630
631 self.sbLabel.setText("Discovering Tests")
632 self.tabWidget.setCurrentIndex(0)
633 self.__updateButtonBoxButtons()
634
576 @pyqtSlot() 635 @pyqtSlot()
577 def __setRunningMode(self): 636 def __setRunningMode(self):
578 """ 637 """
579 Private slot to switch the widget to running mode. 638 Private slot to switch the widget to running mode.
580 """ 639 """
625 self.__insertDiscovery(self.__project.getProjectPath()) 684 self.__insertDiscovery(self.__project.getProjectPath())
626 else: 685 else:
627 self.__insertDiscovery(Preferences.getMultiProject("Workspace")) 686 self.__insertDiscovery(Preferences.getMultiProject("Workspace"))
628 687
629 self.__resetResults() 688 self.__resetResults()
689
690 self.discoveryList.clear()
691
692 @pyqtSlot(str)
693 def on_discoveryPicker_editTextChanged(self, txt):
694 """
695 Private slot to handle a change of the discovery start directory.
696
697 @param txt new discovery start directory
698 @type str
699 """
700 self.discoveryList.clear()
630 701
631 @pyqtSlot() 702 @pyqtSlot()
632 def on_testsuitePicker_aboutToShowPathPickerDialog(self): 703 def on_testsuitePicker_aboutToShowPathPickerDialog(self):
633 """ 704 """
634 Private slot called before the test file selection dialog is shown. 705 Private slot called before the test file selection dialog is shown.
666 Private slot called by a button of the button box clicked. 737 Private slot called by a button of the button box clicked.
667 738
668 @param button button that was clicked 739 @param button button that was clicked
669 @type QAbstractButton 740 @type QAbstractButton
670 """ 741 """
742 if button == self.__discoverButton:
743 self.__discoverTests()
671 if button == self.__startButton: 744 if button == self.__startButton:
672 self.startTests() 745 self.startTests()
673 self.__saveRecent() 746 self.__saveRecent()
674 elif button == self.__stopButton: 747 elif button == self.__stopButton:
675 self.__stopTests() 748 self.__stopTests()
834 EricMessageBox.information( 907 EricMessageBox.information(
835 self, self.tr("Versions"), headerText + versionsText 908 self, self.tr("Versions"), headerText + versionsText
836 ) 909 )
837 910
838 @pyqtSlot() 911 @pyqtSlot()
912 def __discoverTests(self):
913 """
914 Private slot to discover tests but don't execute them.
915 """
916 if self.__mode in (TestingWidgetModes.RUNNING, TestingWidgetModes.DISCOVERY):
917 return
918
919 self.__recentLog = ""
920
921 environment = self.venvComboBox.currentText()
922 framework = self.frameworkComboBox.currentText()
923
924 discoveryStart = self.discoveryPicker.currentText()
925
926 self.sbLabel.setText(self.tr("Discovering Tests"))
927 QCoreApplication.processEvents()
928
929 interpreter = self.__determineInterpreter(environment)
930 config = TestConfig(
931 interpreter=interpreter,
932 discover=True,
933 discoveryStart=discoveryStart,
934 discoverOnly=True,
935 testNamePattern=self.testNamePatternEdit.text(),
936 testMarkerExpression=self.markerExpressionEdit.text(),
937 failFast=self.failfastCheckBox.isChecked(),
938 )
939
940 self.__testExecutor = self.__frameworkRegistry.createExecutor(framework, self)
941 self.__testExecutor.collected.connect(self.__testsDiscovered)
942 self.__testExecutor.collectError.connect(self.__testDiscoveryError)
943 self.__testExecutor.testFinished.connect(self.__testDiscoveryProcessFinished)
944 self.__testExecutor.discoveryAboutToBeStarted.connect(
945 self.__testDiscoveryAboutToBeStarted
946 )
947
948 self.__setDiscoverMode()
949 self.__testExecutor.discover(config, [])
950
951 @pyqtSlot()
839 def startTests(self, failedOnly=False): 952 def startTests(self, failedOnly=False):
840 """ 953 """
841 Public slot to start the test run. 954 Public slot to start the test run.
842 955
843 @param failedOnly flag indicating to run only failed tests 956 @param failedOnly flag indicating to run only failed tests
844 @type bool 957 @type bool
845 """ 958 """
846 if self.__mode == TestingWidgetModes.RUNNING: 959 if self.__mode in (TestingWidgetModes.RUNNING, TestingWidgetModes.DISCOVERY):
847 return 960 return
848 961
849 self.__recentLog = "" 962 self.__recentLog = ""
850 963
851 self.__recentEnvironment = self.venvComboBox.currentText() 964 self.__recentEnvironment = self.venvComboBox.currentText()
878 os.path.splitext(mainScript)[0] + ".coverage" if mainScript else "" 991 os.path.splitext(mainScript)[0] + ".coverage" if mainScript else ""
879 ) 992 )
880 else: 993 else:
881 coverageFile = "" 994 coverageFile = ""
882 interpreter = self.__determineInterpreter(self.__recentEnvironment) 995 interpreter = self.__determineInterpreter(self.__recentEnvironment)
996 testCases = self.__selectedTestCases()
997 if not testCases and self.discoveryList.topLevelItemCount() > 0:
998 ok = EricMessageBox.yesNo(
999 self,
1000 self.tr("Running Tests"),
1001 self.tr("No test case has been selected. Shall all test cases be run?"),
1002 )
1003 if not ok:
1004 return
1005
883 config = TestConfig( 1006 config = TestConfig(
884 interpreter=interpreter, 1007 interpreter=interpreter,
885 discover=discover, 1008 discover=discover,
886 discoveryStart=discoveryStart, 1009 discoveryStart=discoveryStart,
1010 testCases=testCases,
887 testFilename=testFileName, 1011 testFilename=testFileName,
888 testName=testName, 1012 testName=testName,
889 testNamePattern=self.testNamePatternEdit.text(), 1013 testNamePattern=self.testNamePatternEdit.text(),
890 testMarkerExpression=self.markerExpressionEdit.text(), 1014 testMarkerExpression=self.markerExpressionEdit.text(),
891 failFast=self.failfastCheckBox.isChecked(), 1015 failFast=self.failfastCheckBox.isChecked(),
924 def __testsCollected(self, testNames): 1048 def __testsCollected(self, testNames):
925 """ 1049 """
926 Private slot handling the 'collected' signal of the executor. 1050 Private slot handling the 'collected' signal of the executor.
927 1051
928 @param testNames list of tuples containing the test id, the test name 1052 @param testNames list of tuples containing the test id, the test name
929 and a description of collected tests 1053 a description, the file name, the line number and the test path as a list
930 @type list of tuple of (str, str, str) 1054 of collected tests
1055 @type list of tuple of (str, str, str, str, int, list)
931 """ 1056 """
932 testResults = [ 1057 testResults = [
933 TestResult( 1058 TestResult(
934 category=TestResultCategory.PENDING, 1059 category=TestResultCategory.PENDING,
935 status=self.tr("pending"), 1060 status=self.tr("pending"),
937 id=id, 1062 id=id,
938 message=desc, 1063 message=desc,
939 filename=filename, 1064 filename=filename,
940 lineno=lineno, 1065 lineno=lineno,
941 ) 1066 )
942 for id, name, desc, filename, lineno in testNames 1067 for id, name, desc, filename, lineno, _ in testNames
943 ] 1068 ]
944 self.__resultsModel.addTestResults(testResults) 1069 self.__resultsModel.addTestResults(testResults)
945 self.__resultsTree.resizeColumns() 1070 self.__resultsTree.resizeColumns()
946 1071
947 self.__totalCount += len(testResults) 1072 self.__totalCount += len(testResults)
1174 if self.__project: 1299 if self.__project:
1175 # running as part of eric IDE 1300 # running as part of eric IDE
1176 self.testFile.emit(filename, lineno, True) 1301 self.testFile.emit(filename, lineno, True)
1177 else: 1302 else:
1178 self.__openEditor(filename, lineno) 1303 self.__openEditor(filename, lineno)
1304 self.__resultsTree.resizeColumns()
1179 1305
1180 def __openEditor(self, filename, linenumber=1): 1306 def __openEditor(self, filename, linenumber=1):
1181 """ 1307 """
1182 Private method to open an editor window for the given file. 1308 Private method to open an editor window for the given file.
1183 1309
1216 Private slot handling the selection of a status for items to be shown. 1342 Private slot handling the selection of a status for items to be shown.
1217 1343
1218 @param status selected status 1344 @param status selected status
1219 @type str 1345 @type str
1220 """ 1346 """
1221 # TODO: not yet implemented
1222 if status == self.__allFilter: 1347 if status == self.__allFilter:
1223 status = "" 1348 status = ""
1224 1349
1225 self.__resultFilterModel.setStatusFilterString(status) 1350 self.__resultFilterModel.setStatusFilterString(status)
1226 1351
1352 if not self.__project:
1353 # running in standalone mode
1354 self.__resultsTree.resizeColumns()
1355
1227 def __updateStatusFilterComboBox(self): 1356 def __updateStatusFilterComboBox(self):
1357 """
1358 Private method to update the status filter dialog box.
1359 """
1228 statusFilters = self.__resultsModel.getStatusFilterList() 1360 statusFilters = self.__resultsModel.getStatusFilterList()
1229 self.statusFilterComboBox.clear() 1361 self.statusFilterComboBox.clear()
1230 self.statusFilterComboBox.addItem(self.__allFilter) 1362 self.statusFilterComboBox.addItem(self.__allFilter)
1231 self.statusFilterComboBox.addItems(sorted(statusFilters)) 1363 self.statusFilterComboBox.addItems(sorted(statusFilters))
1364
1365 ############################################################################
1366 ## Methods below are handling the discovery only mode.
1367 ############################################################################
1368
1369 def __findDiscoveryItem(self, modulePath):
1370 """
1371 Private method to find an item given the module path.
1372
1373 @param modulePath path of the module in dotted notation
1374 @type str
1375 @return reference to the item or None
1376 @rtype QTreeWidgetItem or None
1377 """
1378 itm = self.discoveryList.topLevelItem(0)
1379 while itm is not None:
1380 if itm.data(0, TestingWidget.TestCaseNameRole) == modulePath:
1381 return itm
1382
1383 itm = self.discoveryList.itemBelow(itm)
1384
1385 return None
1386
1387 @pyqtSlot(list)
1388 def __testsDiscovered(self, testNames):
1389 """
1390 Private slot handling the 'collected' signal of the executor in discovery
1391 mode.
1392
1393 @param testNames list of tuples containing the test id, the test name
1394 a description, the file name, the line number and the test path as a list
1395 of collected tests
1396 @type list of tuple of (str, str, str, str, int, list)
1397 """
1398 for tid, _name, _desc, filename, lineno, testPath in testNames:
1399 parent = None
1400 for index in range(1, len(testPath) + 1):
1401 modulePath = ".".join(testPath[:index])
1402 itm = self.__findDiscoveryItem(modulePath)
1403 if itm is not None:
1404 parent = itm
1405 else:
1406 if parent is None:
1407 itm = QTreeWidgetItem(self.discoveryList, [testPath[index - 1]])
1408 else:
1409 itm = QTreeWidgetItem(parent, [testPath[index - 1]])
1410 parent.setExpanded(True)
1411 itm.setFlags(itm.flags() | Qt.ItemFlag.ItemIsUserCheckable)
1412 itm.setCheckState(0, Qt.CheckState.Unchecked)
1413 itm.setData(0, TestingWidget.TestCaseNameRole, modulePath)
1414 itm.setData(0, TestingWidget.TestCaseLinenoRole, 0)
1415 if os.path.splitext(os.path.basename(filename))[0] == itm.text(0):
1416 itm.setData(0, TestingWidget.TestCaseFileRole, filename)
1417 elif parent:
1418 fn = parent.data(0, TestingWidget.TestCaseFileRole)
1419 if fn:
1420 itm.setData(0, TestingWidget.TestCaseFileRole, fn)
1421 parent = itm
1422
1423 if parent:
1424 parent.setData(0, TestingWidget.TestCaseLinenoRole, lineno)
1425 parent.setData(0, TestingWidget.TestCaseIdRole, tid)
1426
1427 self.__totalCount += len(testNames)
1428
1429 self.sbLabel.setText(self.tr("Discovered %n Test(s)", "", self.__totalCount))
1430
1431 def __testDiscoveryError(self, errors):
1432 """
1433 Private slot handling the 'collectError' signal of the executor.
1434
1435 @param errors list of tuples containing the test name and a description
1436 of the error
1437 @type list of tuple of (str, str)
1438 """
1439 for _testFile, error in errors:
1440 EricMessageBox.critical(
1441 self,
1442 self.tr("Discovery Error"),
1443 self.tr(
1444 "<p>There was an error while discovering tests in <b>{0}</b>.</p>"
1445 "<p>{1}</p>"
1446 ).format("<br/>".join(error.splitlines())),
1447 )
1448
1449 def __testDiscoveryProcessFinished(self, results, output): # noqa: U100
1450 """
1451 Private slot to handle the 'testFinished' signal of the executor in
1452 discovery mode.
1453
1454 @param results list of test result objects (if not sent via the
1455 'testResult' signal)
1456 @type list of TestResult
1457 @param output string containing the test process output (if any)
1458 @type str
1459 """
1460 self.__recentLog = output
1461
1462 self.__setIdleMode()
1463
1464 def __testDiscoveryAboutToBeStarted(self):
1465 """
1466 Private slot to handle the 'testDiscoveryAboutToBeStarted' signal of the
1467 executor.
1468 """
1469 self.discoveryList.clear()
1470
1471 @pyqtSlot(QTreeWidgetItem, int)
1472 def on_discoveryList_itemChanged(self, item, column):
1473 """
1474 Private slot handling the user checking or unchecking an item.
1475
1476 @param item reference to the item
1477 @type QTreeWidgetItem
1478 @param column changed column
1479 @type int
1480 """
1481 if column == 0:
1482 for index in range(item.childCount()):
1483 item.child(index).setCheckState(0, item.checkState(0))
1484
1485 @pyqtSlot(QTreeWidgetItem, int)
1486 def on_discoveryList_itemActivated(self, item, column):
1487 """
1488 Private slot handling the user activating an item.
1489
1490 @param item reference to the item
1491 @type QTreeWidgetItem
1492 @param column column of the double click
1493 @type int
1494 """
1495 if item:
1496 filename = item.data(0, TestingWidget.TestCaseFileRole)
1497 if filename:
1498 self.__showSource(
1499 filename, item.data(0, TestingWidget.TestCaseLinenoRole) + 1
1500 )
1501
1502 def __selectedTestCases(self, parent=None):
1503 """
1504 Private method to assemble the list of selected test cases and suites.
1505
1506 @param parent reference to the parent item
1507 @type QTreeWidgetItem
1508 @return list of selected test cases
1509 @rtype list of str
1510 """
1511 selectedTests = []
1512 itemsList = (
1513 [
1514 # top level
1515 self.discoveryList.topLevelItem(index)
1516 for index in range(self.discoveryList.topLevelItemCount())
1517 ]
1518 if parent is None
1519 else [parent.child(index) for index in range(parent.childCount())]
1520 )
1521
1522 for itm in itemsList:
1523 if itm.checkState(0) == Qt.CheckState.Checked and itm.childCount() == 0:
1524 selectedTests.append(itm.data(0, TestingWidget.TestCaseIdRole))
1525 if itm.childCount():
1526 # recursively check children
1527 selectedTests.extend(self.__selectedTestCases(itm))
1528
1529 return selectedTests
1232 1530
1233 1531
1234 class TestingWindow(EricMainWindow): 1532 class TestingWindow(EricMainWindow):
1235 """ 1533 """
1236 Main window class for the standalone dialog. 1534 Main window class for the standalone dialog.

eric ide

mercurial