--- a/eric7/Unittest/UTTestResultsTree.py Mon May 16 17:22:43 2022 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,611 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a tree view and associated model to show the test result -data. -""" - -import contextlib -import copy -import locale - -from collections import Counter -from operator import attrgetter - -from PyQt6.QtCore import ( - pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, - QModelIndex, QPoint -) -from PyQt6.QtGui import QBrush, QColor -from PyQt6.QtWidgets import QMenu, QTreeView - -from EricWidgets.EricApplication import ericApp - -import Preferences - -from .Interfaces.UTExecutorBase import ResultCategory - -TopLevelId = 2 ** 32 - 1 - - -class TestResultsModel(QAbstractItemModel): - """ - Class implementing the item model containing the test data. - - @signal summary(str) emitted whenever the model data changes. The element - is a summary of the test results of the model. - """ - summary = pyqtSignal(str) - - Headers = [ - QCoreApplication.translate("TestResultsModel", "Status"), - QCoreApplication.translate("TestResultsModel", "Name"), - QCoreApplication.translate("TestResultsModel", "Message"), - QCoreApplication.translate("TestResultsModel", "Duration [ms]"), - ] - - StatusColumn = 0 - NameColumn = 1 - MessageColumn = 2 - DurationColumn = 3 - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent object (defaults to None) - @type QObject (optional) - """ - super().__init__(parent) - - if ericApp().usesDarkPalette(): - self.__backgroundColors = { - ResultCategory.RUNNING: None, - ResultCategory.FAIL: QBrush(QColor("#880000")), - ResultCategory.OK: QBrush(QColor("#005500")), - ResultCategory.SKIP: QBrush(QColor("#3f3f3f")), - ResultCategory.PENDING: QBrush(QColor("#004768")), - } - else: - self.__backgroundColors = { - ResultCategory.RUNNING: None, - ResultCategory.FAIL: QBrush(QColor("#ff8080")), - ResultCategory.OK: QBrush(QColor("#c1ffba")), - ResultCategory.SKIP: QBrush(QColor("#c5c5c5")), - ResultCategory.PENDING: QBrush(QColor("#6fbaff")), - } - - self.__testResults = [] - - def index(self, row, column, parent=QModelIndex()): - """ - Public method to generate an index for the given row and column to - identify the item. - - @param row row for the index - @type int - @param column column for the index - @type int - @param parent index of the parent item (defaults to QModelIndex()) - @type QModelIndex (optional) - @return index for the item - @rtype QModelIndex - """ - if not self.hasIndex(row, column, parent): # check bounds etc. - return QModelIndex() - - if not parent.isValid(): - # top level item - return self.createIndex(row, column, TopLevelId) - else: - testResultIndex = parent.row() - return self.createIndex(row, column, testResultIndex) - - def data(self, index, role): - """ - Public method to get the data for the various columns and roles. - - @param index index of the data to be returned - @type QModelIndex - @param role role designating the data to return - @type Qt.ItemDataRole - @return requested data item - @rtype Any - """ - if not index.isValid(): - return None - - row = index.row() - column = index.column() - idx = index.internalId() - - if role == Qt.ItemDataRole.DisplayRole: - if idx != TopLevelId: - if bool(self.__testResults[idx].extra): - return self.__testResults[idx].extra[index.row()] - else: - return None - elif column == TestResultsModel.StatusColumn: - return self.__testResults[row].status - elif column == TestResultsModel.NameColumn: - return self.__testResults[row].name - elif column == TestResultsModel.MessageColumn: - return self.__testResults[row].message - elif column == TestResultsModel.DurationColumn: - duration = self.__testResults[row].duration - return ( - "" - if duration is None else - locale.format_string("%.2f", duration, grouping=True) - ) - elif role == Qt.ItemDataRole.ToolTipRole: - if idx == TopLevelId and column == TestResultsModel.NameColumn: - return self.__testResults[row].name - elif role == Qt.ItemDataRole.FontRole: - if idx != TopLevelId: - return Preferences.getEditorOtherFonts("MonospacedFont") - elif role == Qt.ItemDataRole.BackgroundRole: - if idx == TopLevelId: - testResult = self.__testResults[row] - with contextlib.suppress(KeyError): - return self.__backgroundColors[testResult.category] - elif role == Qt.ItemDataRole.TextAlignmentRole: - if idx == TopLevelId and column == TestResultsModel.DurationColumn: - return Qt.AlignmentFlag.AlignRight - elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ - if idx == TopLevelId: - testresult = self.__testResults[row] - return (testresult.filename, testresult.lineno) - - return None - - def headerData(self, section, orientation, - role=Qt.ItemDataRole.DisplayRole): - """ - Public method to get the header string for the various sections. - - @param section section number - @type int - @param orientation orientation of the header - @type Qt.Orientation - @param role data role (defaults to Qt.ItemDataRole.DisplayRole) - @type Qt.ItemDataRole (optional) - @return header string of the section - @rtype str - """ - if ( - orientation == Qt.Orientation.Horizontal and - role == Qt.ItemDataRole.DisplayRole - ): - return TestResultsModel.Headers[section] - else: - return None - - def parent(self, index): - """ - Public method to get the parent of the item pointed to by index. - - @param index index of the item - @type QModelIndex - @return index of the parent item - @rtype QModelIndex - """ - if not index.isValid(): - return QModelIndex() - - idx = index.internalId() - if idx == TopLevelId: - return QModelIndex() - else: - return self.index(idx, 0) - - def rowCount(self, parent=QModelIndex()): - """ - Public method to get the number of row for a given parent index. - - @param parent index of the parent item (defaults to QModelIndex()) - @type QModelIndex (optional) - @return number of rows - @rtype int - """ - if not parent.isValid(): - return len(self.__testResults) - - if ( - parent.internalId() == TopLevelId and - parent.column() == 0 and - self.__testResults[parent.row()].extra is not None - ): - return len(self.__testResults[parent.row()].extra) - - return 0 - - def columnCount(self, parent=QModelIndex()): - """ - Public method to get the number of columns. - - @param parent index of the parent item (defaults to QModelIndex()) - @type QModelIndex (optional) - @return number of columns - @rtype int - """ - if not parent.isValid(): - return len(TestResultsModel.Headers) - else: - return 1 - - def clear(self): - """ - Public method to clear the model data. - """ - self.beginResetModel() - self.__testResults.clear() - self.endResetModel() - - self.summary.emit("") - - def sort(self, column, order): - """ - Public method to sort the model data by column in order. - - @param column sort column number - @type int - @param order sort order - @type Qt.SortOrder - """ # __IGNORE_WARNING_D234r__ - def durationKey(result): - """ - Function to generate a key for duration sorting - - @param result result object - @type UTTestResult - @return sort key - @rtype float - """ - return result.duration or -1.0 - - self.beginResetModel() - reverse = order == Qt.SortOrder.DescendingOrder - if column == TestResultsModel.StatusColumn: - self.__testResults.sort(key=attrgetter('category', 'status'), - reverse=reverse) - elif column == TestResultsModel.NameColumn: - self.__testResults.sort(key=attrgetter('name'), reverse=reverse) - elif column == TestResultsModel.MessageColumn: - self.__testResults.sort(key=attrgetter('message'), reverse=reverse) - elif column == TestResultsModel.DurationColumn: - self.__testResults.sort(key=durationKey, reverse=reverse) - self.endResetModel() - - def getTestResults(self): - """ - Public method to get the list of test results managed by the model. - - @return list of test results managed by the model - @rtype list of UTTestResult - """ - return copy.deepcopy(self.__testResults) - - def setTestResults(self, testResults): - """ - Public method to set the list of test results of the model. - - @param testResults test results to be managed by the model - @type list of UTTestResult - """ - self.beginResetModel() - self.__testResults = copy.deepcopy(testResults) - self.endResetModel() - - self.summary.emit(self.__summary()) - - def addTestResults(self, testResults): - """ - Public method to add test results to the ones already managed by the - model. - - @param testResults test results to be added to the model - @type list of UTTestResult - """ - firstRow = len(self.__testResults) - lastRow = firstRow + len(testResults) - 1 - self.beginInsertRows(QModelIndex(), firstRow, lastRow) - self.__testResults.extend(testResults) - self.endInsertRows() - - self.summary.emit(self.__summary()) - - def updateTestResults(self, testResults): - """ - Public method to update the data of managed test result items. - - @param testResults test results to be updated - @type list of UTTestResult - """ - minIndex = None - maxIndex = None - - testResultsToBeAdded = [] - - for testResult in testResults: - for (index, currentResult) in enumerate(self.__testResults): - if currentResult.id == testResult.id: - self.__testResults[index] = testResult - if minIndex is None: - minIndex = index - maxIndex = index - else: - minIndex = min(minIndex, index) - maxIndex = max(maxIndex, index) - - break - else: - # Test result with given id was not found. - # Just add it to the list (could be a sub test) - testResultsToBeAdded.append(testResult) - - if minIndex is not None: - self.dataChanged.emit( - self.index(minIndex, 0), - self.index(maxIndex, len(TestResultsModel.Headers) - 1) - ) - - self.summary.emit(self.__summary()) - - if testResultsToBeAdded: - self.addTestResults(testResultsToBeAdded) - - def getFailedTests(self): - """ - Public method to extract the test ids of all failed tests. - - @return test ids of all failed tests - @rtype list of str - """ - failedIds = [ - res.id for res in self.__testResults if ( - res.category == ResultCategory.FAIL and - not res.subtestResult - ) - ] - return failedIds - - def __summary(self): - """ - Private method to generate a test results summary text. - - @return test results summary text - @rtype str - """ - if len(self.__testResults) == 0: - return self.tr("No results to show") - - counts = Counter(res.category for res in self.__testResults) - if all( - counts[category] == 0 - for category in (ResultCategory.FAIL, ResultCategory.OK, - ResultCategory.SKIP) - ): - return self.tr("Collected %n test(s)", "", len(self.__testResults)) - - return self.tr( - "%n test(s)/subtest(s) total, {0} failed, {1} passed," - " {2} skipped, {3} pending", - "", len(self.__testResults) - ).format( - counts[ResultCategory.FAIL], - counts[ResultCategory.OK], - counts[ResultCategory.SKIP], - counts[ResultCategory.PENDING] - ) - - -class TestResultsTreeView(QTreeView): - """ - Class implementing a tree view to show the test result data. - - @signal goto(str, int) emitted to go to the position given by file name - and line number - """ - goto = pyqtSignal(str, int) - - def __init__(self, parent=None): - """ - Constructor - - @param parent reference to the parent widget (defaults to None) - @type QWidget (optional) - """ - super().__init__(parent) - - self.setItemsExpandable(True) - self.setExpandsOnDoubleClick(False) - self.setSortingEnabled(True) - - self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) - self.header().setSortIndicatorShown(False) - - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - - # connect signals and slots - self.doubleClicked.connect(self.__gotoTestDefinition) - self.customContextMenuRequested.connect(self.__showContextMenu) - - self.header().sortIndicatorChanged.connect(self.sortByColumn) - self.header().sortIndicatorChanged.connect( - lambda column, order: self.header().setSortIndicatorShown(True)) - - def reset(self): - """ - Public method to reset the internal state of the view. - """ - super().reset() - - self.resizeColumns() - self.spanFirstColumn(0, self.model().rowCount() - 1) - - def rowsInserted(self, parent, startRow, endRow): - """ - Public method called when rows are inserted. - - @param parent model index of the parent item - @type QModelIndex - @param startRow first row been inserted - @type int - @param endRow last row been inserted - @type int - """ - super().rowsInserted(parent, startRow, endRow) - - self.resizeColumns() - self.spanFirstColumn(startRow, endRow) - - def dataChanged(self, topLeft, bottomRight, roles=[]): - """ - Public method called when the model data has changed. - - @param topLeft index of the top left element - @type QModelIndex - @param bottomRight index of the bottom right element - @type QModelIndex - @param roles list of roles changed (defaults to []) - @type list of Qt.ItemDataRole (optional) - """ - super().dataChanged(topLeft, bottomRight, roles) - - self.resizeColumns() - while topLeft.parent().isValid(): - topLeft = topLeft.parent() - while bottomRight.parent().isValid(): - bottomRight = bottomRight.parent() - self.spanFirstColumn(topLeft.row(), bottomRight.row()) - - def resizeColumns(self): - """ - Public method to resize the columns to their contents. - """ - for column in range(self.model().columnCount()): - self.resizeColumnToContents(column) - - def spanFirstColumn(self, startRow, endRow): - """ - Public method to make the first column span the row for second level - items. - - These items contain the test results. - - @param startRow index of the first row to span - @type QModelIndex - @param endRow index of the last row (including) to span - @type QModelIndex - """ - model = self.model() - for row in range(startRow, endRow + 1): - index = model.index(row, 0) - for i in range(model.rowCount(index)): - self.setFirstColumnSpanned(i, index, True) - - def __canonicalIndex(self, index): - """ - Private method to create the canonical index for a given index. - - The canonical index is the index of the first column of the test - result entry (i.e. the top-level item). If the index is invalid, - None is returned. - - @param index index to determine the canonical index for - @type QModelIndex - @return index of the firt column of the associated top-level item index - @rtype QModelIndex - """ - if not index.isValid(): - return None - - while index.parent().isValid(): # find the top-level node - index = index.parent() - index = index.sibling(index.row(), 0) # go to first column - return index - - @pyqtSlot(QModelIndex) - def __gotoTestDefinition(self, index): - """ - Private slot to show the test definition. - - @param index index for the double-clicked item - @type QModelIndex - """ - cindex = self.__canonicalIndex(index) - filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole) - if filename is not None: - if lineno is None: - lineno = 1 - self.goto.emit(filename, lineno) - - @pyqtSlot(QPoint) - def __showContextMenu(self, pos): - """ - Private slot to show the context menu. - - @param pos relative position for the context menu - @type QPoint - """ - index = self.indexAt(pos) - cindex = self.__canonicalIndex(index) - - contextMenu = ( - self.__createContextMenu(cindex) - if cindex else - self.__createBackgroundContextMenu() - ) - contextMenu.exec(self.mapToGlobal(pos)) - - def __createContextMenu(self, index): - """ - Private method to create a context menu for the item pointed to by the - given index. - - @param index index of the item - @type QModelIndex - @return created context menu - @rtype QMenu - """ - menu = QMenu(self) - if self.isExpanded(index): - menu.addAction(self.tr("Collapse"), - lambda: self.collapse(index)) - else: - act = menu.addAction(self.tr("Expand"), - lambda: self.expand(index)) - act.setEnabled(self.model().hasChildren(index)) - menu.addSeparator() - - act = menu.addAction(self.tr("Show Source"), - lambda: self.__gotoTestDefinition(index)) - act.setEnabled( - self.model().data(index, Qt.ItemDataRole.UserRole) is not None - ) - menu.addSeparator() - - menu.addAction(self.tr("Collapse All"), self.collapseAll) - menu.addAction(self.tr("Expand All"), self.expandAll) - - return menu - - def __createBackgroundContextMenu(self): - """ - Private method to create a context menu for the background. - - @return created context menu - @rtype QMenu - """ - menu = QMenu(self) - menu.addAction(self.tr("Collapse All"), self.collapseAll) - menu.addAction(self.tr("Expand All"), self.expandAll) - - return menu - -# -# eflag: noqa = M821, M822