eric7/Unittest/UTTestResultsTree.py

branch
unittest
changeset 9062
7f27bf3b50c3
parent 9059
e7fd342f8bfc
child 9063
f1d7dd7ae471
equal deleted inserted replaced
9061:22dab1be7953 9062:7f27bf3b50c3
6 """ 6 """
7 Module implementing a tree view and associated model to show the test result 7 Module implementing a tree view and associated model to show the test result
8 data. 8 data.
9 """ 9 """
10 10
11 import contextlib
12 import copy
13 import locale
14 from operator import attrgetter
15
11 from PyQt6.QtCore import ( 16 from PyQt6.QtCore import (
12 pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex 17 pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex
13 ) 18 )
19 from PyQt6.QtGui import QBrush, QColor
14 from PyQt6.QtWidgets import QTreeView 20 from PyQt6.QtWidgets import QTreeView
21
22 from EricWidgets.EricApplication import ericApp
23
24 import Preferences
25
26 from .Interfaces.UTExecutorBase import ResultCategory
15 27
16 TopLevelId = 2 ** 32 - 1 28 TopLevelId = 2 ** 32 - 1
17 29
18 30
19 class TestResultsModel(QAbstractItemModel): 31 class TestResultsModel(QAbstractItemModel):
25 QCoreApplication.translate("TestResultsModel", "Name"), 37 QCoreApplication.translate("TestResultsModel", "Name"),
26 QCoreApplication.translate("TestResultsModel", "Message"), 38 QCoreApplication.translate("TestResultsModel", "Message"),
27 QCoreApplication.translate("TestResultsModel", "Duration (ms)"), 39 QCoreApplication.translate("TestResultsModel", "Duration (ms)"),
28 ] 40 ]
29 41
42 StatusColumn = 0
43 NameColumn = 1
44 MessageColumn = 2
45 DurationColumn = 3
46
30 def __init__(self, parent=None): 47 def __init__(self, parent=None):
31 """ 48 """
32 Constructor 49 Constructor
33 50
34 @param parent reference to the parent object (defaults to None) 51 @param parent reference to the parent object (defaults to None)
35 @type QObject (optional) 52 @type QObject (optional)
36 """ 53 """
37 super().__init__(parent) 54 super().__init__(parent)
38 55
56 if ericApp().usesDarkPalette():
57 self.__backgroundColors = {
58 ResultCategory.RUNNING: None,
59 ResultCategory.FAIL: QBrush(QColor("#880000")),
60 ResultCategory.OK: QBrush(QColor("#005500")),
61 ResultCategory.SKIP: QBrush(QColor("#3f3f3f")),
62 ResultCategory.PENDING: QBrush(QColor("#004768")),
63 }
64 else:
65 self.__backgroundColors = {
66 ResultCategory.RUNNING: None,
67 ResultCategory.FAIL: QBrush(QColor("#ff8080")),
68 ResultCategory.OK: QBrush(QColor("#c1ffba")),
69 ResultCategory.SKIP: QBrush(QColor("#c5c5c5")),
70 ResultCategory.PENDING: QBrush(QColor("#6fbaff")),
71 }
72
39 self.__testResults = [] 73 self.__testResults = []
74
75 def index(self, row, column, parent=QModelIndex()):
76 """
77 Public method to generate an index for the given row and column to
78 identify the item.
79
80 @param row row for the index
81 @type int
82 @param column column for the index
83 @type int
84 @param parent index of the parent item (defaults to QModelIndex())
85 @type QModelIndex (optional)
86 @return index for the item
87 @rtype QModelIndex
88 """
89 if not self.hasIndex(row, column, parent): # check bounds etc.
90 return QModelIndex()
91
92 if not parent.isValid():
93 # top level item
94 return self.createIndex(row, column, TopLevelId)
95 else:
96 testResultIndex = parent.row()
97 return self.createIndex(row, column, testResultIndex)
98
99 def data(self, index, role):
100 """
101 Public method to get the data for the various columns and roles.
102
103 @param index index of the data to be returned
104 @type QModelIndex
105 @param role role designating the data to return
106 @type Qt.ItemDataRole
107 @return requested data item
108 @rtype Any
109 """
110 if not index.isValid():
111 return None
112
113 row = index.row()
114 column = index.column()
115 idx = index.internalId()
116
117 if role == Qt.ItemDataRole.DisplayRole:
118 if idx != TopLevelId:
119 if bool(self.__testResults[idx].extra):
120 return self.__testResults[idx].extra[index.row()]
121 else:
122 return None
123 elif column == TestResultsModel.StatusColumn:
124 return self.__testResults[row].status
125 elif column == TestResultsModel.NameColumn:
126 return self.__testResults[row].name
127 elif column == TestResultsModel.MessageColumn:
128 return self.__testResults[row].message
129 elif column == TestResultsModel.DurationColumn:
130 duration = self.__testResults[row].duration
131 return (
132 ''
133 if duration is None else
134 locale.format_string("%.2f", duration, grouping=True)
135 )
136 elif role == Qt.ItemDataRole.ToolTipRole:
137 if idx == TopLevelId and column == TestResultsModel.NameColumn:
138 return self.testresults[row].name
139 elif role == Qt.ItemDataRole.FontRole:
140 if idx != TopLevelId:
141 return Preferences.getEditorOtherFonts("MonospacedFont")
142 elif role == Qt.ItemDataRole.BackgroundRole:
143 if idx == TopLevelId:
144 testResult = self.__testResults[row]
145 with contextlib.suppress(KeyError):
146 return self.__backgroundColors[testResult.category]
147 elif role == Qt.ItemDataRole.TextAlignmentRole:
148 if idx == TopLevelId and column == TestResultsModel.DurationColumn:
149 return Qt.AlignmentFlag.AlignRight
150 elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__
151 if idx == TopLevelId:
152 testresult = self.testresults[row]
153 return (testresult.filename, testresult.lineno)
154
155 return None
40 156
41 def headerData(self, section, orientation, 157 def headerData(self, section, orientation,
42 role=Qt.ItemDataRole.DisplayRole): 158 role=Qt.ItemDataRole.DisplayRole):
43 """ 159 """
44 Public method to get the header string for the various sections. 160 Public method to get the header string for the various sections.
58 ): 174 ):
59 return TestResultsModel.Headers[section] 175 return TestResultsModel.Headers[section]
60 else: 176 else:
61 return None 177 return None
62 178
179 def parent(self, index):
180 """
181 Public method to get the parent of the item pointed to by index.
182
183 @param index index of the item
184 @type QModelIndex
185 @return index of the parent item
186 @rtype QModelIndex
187 """
188 if not index.isValid():
189 return QModelIndex()
190
191 idx = index.internalId()
192 if idx == TopLevelId:
193 return QModelIndex()
194 else:
195 return self.index(idx, 0)
196
63 def rowCount(self, parent=QModelIndex()): 197 def rowCount(self, parent=QModelIndex()):
64 """ 198 """
65 Public method to get the number of row for a given parent index. 199 Public method to get the number of row for a given parent index.
66 200
67 @param parent index of the parent item (defaults to QModelIndex()) 201 @param parent index of the parent item (defaults to QModelIndex())
70 @rtype int 204 @rtype int
71 """ 205 """
72 if not parent.isValid(): 206 if not parent.isValid():
73 return len(self.__testResults) 207 return len(self.__testResults)
74 208
75 if parent.internalId() == TopLevelId and parent.column() == 0: 209 if (
210 parent.internalId() == TopLevelId and
211 parent.column() == 0 and
212 self.__testResults[parent.row()].extra is not None
213 ):
76 return len(self.__testResults[parent.row()].extra) 214 return len(self.__testResults[parent.row()].extra)
77 215
78 return 0 216 return 0
79 217
80 def columnCount(self, parent=QModelIndex()): 218 def columnCount(self, parent=QModelIndex()):
96 Public method to clear the model data. 234 Public method to clear the model data.
97 """ 235 """
98 self.beginResetModel() 236 self.beginResetModel()
99 self.__testResults.clear() 237 self.__testResults.clear()
100 self.endResetModel() 238 self.endResetModel()
239
240 def sort(self, column, order):
241 """
242 Public method to sort the model data by column in order.
243
244 @param column sort column number
245 @type int
246 @param order sort order
247 @type Qt.SortOrder
248 """ # __IGNORE_WARNING_D234r__
249 def durationKey(result):
250 """
251 Function to generate a key for duration sorting
252
253 @param result result object
254 @type UTTestResult
255 @return sort key
256 @rtype float
257 """
258 return result.duration or -1.0
259
260 self.beginResetModel()
261 reverse = order == Qt.SortOrder.DescendingOrder
262 if column == TestResultsModel.StatusColumn:
263 self.__testResults.sort(key=attrgetter('category', 'status'),
264 reverse=reverse)
265 elif column == TestResultsModel.NameColumn:
266 self.__testResults.sort(key=attrgetter('name'), reverse=reverse)
267 elif column == TestResultsModel.MessageColumn:
268 self.__testResults.sort(key=attrgetter('message'), reverse=reverse)
269 elif column == TestResultsModel.DurationColumn:
270 self.__testResults.sort(key=durationKey, reverse=reverse)
271 self.endResetModel()
272
273 def getTestResults(self):
274 """
275 Public method to get the list of test results managed by the model.
276
277 @return list of test results managed by the model
278 @rtype list of UTTestResult
279 """
280 return copy.deepcopy(self.__testResults)
281
282 def setTestResults(self, testResults):
283 """
284 Public method to set the list of test results of the model.
285
286 @param testResults test results to be managed by the model
287 @type list of UTTestResult
288 """
289 self.beginResetModel()
290 self.__testResults = copy.deepcopy(testResults)
291 self.endResetModel()
292
293 def addTestResults(self, testResults):
294 """
295 Public method to add test results to the ones already managed by the
296 model.
297
298 @param testResults test results to be added to the model
299 @type list of UTTestResult
300 """
301 firstRow = len(self.__testResults)
302 lastRow = firstRow + len(testResults) - 1
303 self.beginInsertRows(QModelIndex(), firstRow, lastRow)
304 self.__testResults.extend(testResults)
305 self.endInsertRows()
306
307 def updateTestResults(self, testResults):
308 """
309 Public method to update the data of managed test result items.
310
311 @param testResults test results to be updated
312 @type list of UTTestResult
313 """
314 minIndex = None
315 maxIndex = None
316
317 for testResult in testResults:
318 for (index, currentResult) in enumerate(self.__testResults):
319 if currentResult.id == testResult.id:
320 self.__testResults[index] = testResult
321 if minIndex is None:
322 minIndex = index
323 maxIndex = index
324 else:
325 minIndex = min(minIndex, index)
326 maxIndex = max(maxIndex, index)
327
328 if minIndex is not None:
329 self.dataChanged.emit(
330 self.index(minIndex, 0),
331 self.index(maxIndex, len(TestResultsModel.Headers) - 1)
332 )
101 333
102 334
103 class TestResultsTreeView(QTreeView): 335 class TestResultsTreeView(QTreeView):
104 """ 336 """
105 Class implementing a tree view to show the test result data. 337 Class implementing a tree view to show the test result data.
130 362
131 self.header().sortIndicatorChanged.connect(self.sortByColumn) 363 self.header().sortIndicatorChanged.connect(self.sortByColumn)
132 self.header().sortIndicatorChanged.connect( 364 self.header().sortIndicatorChanged.connect(
133 lambda column, order: self.header().setSortIndicatorShown(True)) 365 lambda column, order: self.header().setSortIndicatorShown(True))
134 366
367 def reset(self):
368 """
369 Public method to reset the internal state of the view.
370 """
371 super().reset()
372
373 self.resizeColumns()
374 self.spanFirstColumn(0, self.model().rowCount() - 1)
375
376 def rowsInserted(self, parent, startRow, endRow):
377 """
378 Public method called when rows are inserted.
379
380 @param parent model index of the parent item
381 @type QModelIndex
382 @param startRow first row been inserted
383 @type int
384 @param endRow last row been inserted
385 @type int
386 """
387 super().rowsInserted(parent, startRow, endRow)
388
389 self.resizeColumns()
390 self.spanFirstColumn(startRow, endRow)
391
392 def dataChanged(self, topLeft, bottomRight, roles=[]):
393 """
394 Public method called when the model data has changed.
395
396 @param topLeft index of the top left element
397 @type QModelIndex
398 @param bottomRight index of the bottom right element
399 @type QModelIndex
400 @param roles list of roles changed (defaults to [])
401 @type list of Qt.ItemDataRole (optional)
402 """
403 super().dataChanged(topLeft, bottomRight, roles)
404
405 self.resizeColumns()
406 while topLeft.parent().isValid():
407 topLeft = topLeft.parent()
408 while bottomRight.parent().isValid():
409 bottomRight = bottomRight.parent()
410 self.spanFirstColumn(topLeft.row(), bottomRight.row())
411
135 @pyqtSlot(QModelIndex) 412 @pyqtSlot(QModelIndex)
136 def __gotoTestDefinition(self, index): 413 def __gotoTestDefinition(self, index):
137 """ 414 """
138 Private slot to show the test definition. 415 Private slot to show the test definition.
139 416
140 @param index index for the double-clicked item 417 @param index index for the double-clicked item
141 @type QModelIndex 418 @type QModelIndex
142 """ 419 """
143 # TODO: not implemented yet 420 # TODO: not implemented yet (__gotoTestDefinition)
144 pass 421 pass
422
423 def resizeColumns(self):
424 """
425 Public method to resize the columns to their contents.
426 """
427 for column in range(self.model().columnCount()):
428 self.resizeColumnToContents(column)
429
430 def spanFirstColumn(self, startRow, endRow):
431 """
432 Public method to make the first column span the row for second level
433 items.
434
435 These items contain the test results.
436
437 @param startRow index of the first row to span
438 @type QModelIndex
439 @param endRow index of the last row (including) to span
440 @type QModelIndex
441 """
442 model = self.model()
443 for row in range(startRow, endRow + 1):
444 index = model.index(row, 0)
445 for i in range(model.rowCount(index)):
446 self.setFirstColumnSpanned(i, index, True)
145 447
146 # 448 #
147 # eflag: noqa = M822 449 # eflag: noqa = M821, M822

eric ide

mercurial