diff -r 22dab1be7953 -r 7f27bf3b50c3 eric7/Unittest/UTTestResultsTree.py --- a/eric7/Unittest/UTTestResultsTree.py Thu May 12 09:00:35 2022 +0200 +++ b/eric7/Unittest/UTTestResultsTree.py Fri May 13 17:23:21 2022 +0200 @@ -8,11 +8,23 @@ data. """ +import contextlib +import copy +import locale +from operator import attrgetter + from PyQt6.QtCore import ( pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex ) +from PyQt6.QtGui import QBrush, QColor from PyQt6.QtWidgets import QTreeView +from EricWidgets.EricApplication import ericApp + +import Preferences + +from .Interfaces.UTExecutorBase import ResultCategory + TopLevelId = 2 ** 32 - 1 @@ -27,6 +39,11 @@ QCoreApplication.translate("TestResultsModel", "Duration (ms)"), ] + StatusColumn = 0 + NameColumn = 1 + MessageColumn = 2 + DurationColumn = 3 + def __init__(self, parent=None): """ Constructor @@ -36,8 +53,107 @@ """ 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): """ @@ -60,6 +176,24 @@ 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. @@ -72,7 +206,11 @@ if not parent.isValid(): return len(self.__testResults) - if parent.internalId() == TopLevelId and parent.column() == 0: + 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 @@ -98,6 +236,100 @@ self.beginResetModel() self.__testResults.clear() self.endResetModel() + + 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() + + 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() + + 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 + + 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) + + if minIndex is not None: + self.dataChanged.emit( + self.index(minIndex, 0), + self.index(maxIndex, len(TestResultsModel.Headers) - 1) + ) class TestResultsTreeView(QTreeView): @@ -132,6 +364,51 @@ 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()) + @pyqtSlot(QModelIndex) def __gotoTestDefinition(self, index): """ @@ -140,8 +417,33 @@ @param index index for the double-clicked item @type QModelIndex """ - # TODO: not implemented yet + # TODO: not implemented yet (__gotoTestDefinition) pass + + 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) # -# eflag: noqa = M822 +# eflag: noqa = M821, M822