src/eric7/Testing/TestResultsTree.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9264
18a7312cfdb3
child 9413
80c06d472826
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
14 14
15 from collections import Counter 15 from collections import Counter
16 from operator import attrgetter 16 from operator import attrgetter
17 17
18 from PyQt6.QtCore import ( 18 from PyQt6.QtCore import (
19 pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, 19 pyqtSignal,
20 QModelIndex, QPoint 20 pyqtSlot,
21 Qt,
22 QAbstractItemModel,
23 QCoreApplication,
24 QModelIndex,
25 QPoint,
21 ) 26 )
22 from PyQt6.QtGui import QBrush, QColor 27 from PyQt6.QtGui import QBrush, QColor
23 from PyQt6.QtWidgets import QMenu, QTreeView 28 from PyQt6.QtWidgets import QMenu, QTreeView
24 29
25 from EricWidgets.EricApplication import ericApp 30 from EricWidgets.EricApplication import ericApp
26 31
27 import Preferences 32 import Preferences
28 33
29 from .Interfaces.TestExecutorBase import TestResultCategory 34 from .Interfaces.TestExecutorBase import TestResultCategory
30 35
31 TopLevelId = 2 ** 32 - 1 36 TopLevelId = 2**32 - 1
32 37
33 38
34 class TestResultsModel(QAbstractItemModel): 39 class TestResultsModel(QAbstractItemModel):
35 """ 40 """
36 Class implementing the item model containing the test data. 41 Class implementing the item model containing the test data.
37 42
38 @signal summary(str) emitted whenever the model data changes. The element 43 @signal summary(str) emitted whenever the model data changes. The element
39 is a summary of the test results of the model. 44 is a summary of the test results of the model.
40 """ 45 """
46
41 summary = pyqtSignal(str) 47 summary = pyqtSignal(str)
42 48
43 Headers = [ 49 Headers = [
44 QCoreApplication.translate("TestResultsModel", "Status"), 50 QCoreApplication.translate("TestResultsModel", "Status"),
45 QCoreApplication.translate("TestResultsModel", "Name"), 51 QCoreApplication.translate("TestResultsModel", "Name"),
46 QCoreApplication.translate("TestResultsModel", "Message"), 52 QCoreApplication.translate("TestResultsModel", "Message"),
47 QCoreApplication.translate("TestResultsModel", "Duration [ms]"), 53 QCoreApplication.translate("TestResultsModel", "Duration [ms]"),
48 ] 54 ]
49 55
50 StatusColumn = 0 56 StatusColumn = 0
51 NameColumn = 1 57 NameColumn = 1
52 MessageColumn = 2 58 MessageColumn = 2
53 DurationColumn = 3 59 DurationColumn = 3
54 60
55 def __init__(self, parent=None): 61 def __init__(self, parent=None):
56 """ 62 """
57 Constructor 63 Constructor
58 64
59 @param parent reference to the parent object (defaults to None) 65 @param parent reference to the parent object (defaults to None)
60 @type QObject (optional) 66 @type QObject (optional)
61 """ 67 """
62 super().__init__(parent) 68 super().__init__(parent)
63 69
64 if ericApp().usesDarkPalette(): 70 if ericApp().usesDarkPalette():
65 self.__backgroundColors = { 71 self.__backgroundColors = {
66 TestResultCategory.RUNNING: None, 72 TestResultCategory.RUNNING: None,
67 TestResultCategory.FAIL: QBrush(QColor("#880000")), 73 TestResultCategory.FAIL: QBrush(QColor("#880000")),
68 TestResultCategory.OK: QBrush(QColor("#005500")), 74 TestResultCategory.OK: QBrush(QColor("#005500")),
75 TestResultCategory.FAIL: QBrush(QColor("#ff8080")), 81 TestResultCategory.FAIL: QBrush(QColor("#ff8080")),
76 TestResultCategory.OK: QBrush(QColor("#c1ffba")), 82 TestResultCategory.OK: QBrush(QColor("#c1ffba")),
77 TestResultCategory.SKIP: QBrush(QColor("#c5c5c5")), 83 TestResultCategory.SKIP: QBrush(QColor("#c5c5c5")),
78 TestResultCategory.PENDING: QBrush(QColor("#6fbaff")), 84 TestResultCategory.PENDING: QBrush(QColor("#6fbaff")),
79 } 85 }
80 86
81 self.__testResults = [] 87 self.__testResults = []
82 88
83 def index(self, row, column, parent=QModelIndex()): 89 def index(self, row, column, parent=QModelIndex()):
84 """ 90 """
85 Public method to generate an index for the given row and column to 91 Public method to generate an index for the given row and column to
86 identify the item. 92 identify the item.
87 93
88 @param row row for the index 94 @param row row for the index
89 @type int 95 @type int
90 @param column column for the index 96 @param column column for the index
91 @type int 97 @type int
92 @param parent index of the parent item (defaults to QModelIndex()) 98 @param parent index of the parent item (defaults to QModelIndex())
94 @return index for the item 100 @return index for the item
95 @rtype QModelIndex 101 @rtype QModelIndex
96 """ 102 """
97 if not self.hasIndex(row, column, parent): # check bounds etc. 103 if not self.hasIndex(row, column, parent): # check bounds etc.
98 return QModelIndex() 104 return QModelIndex()
99 105
100 if not parent.isValid(): 106 if not parent.isValid():
101 # top level item 107 # top level item
102 return self.createIndex(row, column, TopLevelId) 108 return self.createIndex(row, column, TopLevelId)
103 else: 109 else:
104 testResultIndex = parent.row() 110 testResultIndex = parent.row()
105 return self.createIndex(row, column, testResultIndex) 111 return self.createIndex(row, column, testResultIndex)
106 112
107 def data(self, index, role): 113 def data(self, index, role):
108 """ 114 """
109 Public method to get the data for the various columns and roles. 115 Public method to get the data for the various columns and roles.
110 116
111 @param index index of the data to be returned 117 @param index index of the data to be returned
112 @type QModelIndex 118 @type QModelIndex
113 @param role role designating the data to return 119 @param role role designating the data to return
114 @type Qt.ItemDataRole 120 @type Qt.ItemDataRole
115 @return requested data item 121 @return requested data item
116 @rtype Any 122 @rtype Any
117 """ 123 """
118 if not index.isValid(): 124 if not index.isValid():
119 return None 125 return None
120 126
121 row = index.row() 127 row = index.row()
122 column = index.column() 128 column = index.column()
123 idx = index.internalId() 129 idx = index.internalId()
124 130
125 if role == Qt.ItemDataRole.DisplayRole: 131 if role == Qt.ItemDataRole.DisplayRole:
126 if idx != TopLevelId: 132 if idx != TopLevelId:
127 if bool(self.__testResults[idx].extra): 133 if bool(self.__testResults[idx].extra):
128 return self.__testResults[idx].extra[index.row()] 134 return self.__testResults[idx].extra[index.row()]
129 else: 135 else:
136 return self.__testResults[row].message 142 return self.__testResults[row].message
137 elif column == TestResultsModel.DurationColumn: 143 elif column == TestResultsModel.DurationColumn:
138 duration = self.__testResults[row].duration 144 duration = self.__testResults[row].duration
139 return ( 145 return (
140 "" 146 ""
141 if duration is None else 147 if duration is None
142 locale.format_string("%.2f", duration, grouping=True) 148 else locale.format_string("%.2f", duration, grouping=True)
143 ) 149 )
144 elif role == Qt.ItemDataRole.ToolTipRole: 150 elif role == Qt.ItemDataRole.ToolTipRole:
145 if idx == TopLevelId and column == TestResultsModel.NameColumn: 151 if idx == TopLevelId and column == TestResultsModel.NameColumn:
146 return self.__testResults[row].name 152 return self.__testResults[row].name
147 elif role == Qt.ItemDataRole.FontRole: 153 elif role == Qt.ItemDataRole.FontRole:
153 with contextlib.suppress(KeyError): 159 with contextlib.suppress(KeyError):
154 return self.__backgroundColors[testResult.category] 160 return self.__backgroundColors[testResult.category]
155 elif role == Qt.ItemDataRole.TextAlignmentRole: 161 elif role == Qt.ItemDataRole.TextAlignmentRole:
156 if idx == TopLevelId and column == TestResultsModel.DurationColumn: 162 if idx == TopLevelId and column == TestResultsModel.DurationColumn:
157 return Qt.AlignmentFlag.AlignRight.value 163 return Qt.AlignmentFlag.AlignRight.value
158 elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ 164 elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__
159 if idx == TopLevelId: 165 if idx == TopLevelId:
160 testresult = self.__testResults[row] 166 testresult = self.__testResults[row]
161 return (testresult.filename, testresult.lineno) 167 return (testresult.filename, testresult.lineno)
162 168
163 return None 169 return None
164 170
165 def headerData(self, section, orientation, 171 def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
166 role=Qt.ItemDataRole.DisplayRole):
167 """ 172 """
168 Public method to get the header string for the various sections. 173 Public method to get the header string for the various sections.
169 174
170 @param section section number 175 @param section section number
171 @type int 176 @type int
172 @param orientation orientation of the header 177 @param orientation orientation of the header
173 @type Qt.Orientation 178 @type Qt.Orientation
174 @param role data role (defaults to Qt.ItemDataRole.DisplayRole) 179 @param role data role (defaults to Qt.ItemDataRole.DisplayRole)
175 @type Qt.ItemDataRole (optional) 180 @type Qt.ItemDataRole (optional)
176 @return header string of the section 181 @return header string of the section
177 @rtype str 182 @rtype str
178 """ 183 """
179 if ( 184 if (
180 orientation == Qt.Orientation.Horizontal and 185 orientation == Qt.Orientation.Horizontal
181 role == Qt.ItemDataRole.DisplayRole 186 and role == Qt.ItemDataRole.DisplayRole
182 ): 187 ):
183 return TestResultsModel.Headers[section] 188 return TestResultsModel.Headers[section]
184 else: 189 else:
185 return None 190 return None
186 191
187 def parent(self, index): 192 def parent(self, index):
188 """ 193 """
189 Public method to get the parent of the item pointed to by index. 194 Public method to get the parent of the item pointed to by index.
190 195
191 @param index index of the item 196 @param index index of the item
192 @type QModelIndex 197 @type QModelIndex
193 @return index of the parent item 198 @return index of the parent item
194 @rtype QModelIndex 199 @rtype QModelIndex
195 """ 200 """
196 if not index.isValid(): 201 if not index.isValid():
197 return QModelIndex() 202 return QModelIndex()
198 203
199 idx = index.internalId() 204 idx = index.internalId()
200 if idx == TopLevelId: 205 if idx == TopLevelId:
201 return QModelIndex() 206 return QModelIndex()
202 else: 207 else:
203 return self.index(idx, 0) 208 return self.index(idx, 0)
204 209
205 def rowCount(self, parent=QModelIndex()): 210 def rowCount(self, parent=QModelIndex()):
206 """ 211 """
207 Public method to get the number of row for a given parent index. 212 Public method to get the number of row for a given parent index.
208 213
209 @param parent index of the parent item (defaults to QModelIndex()) 214 @param parent index of the parent item (defaults to QModelIndex())
210 @type QModelIndex (optional) 215 @type QModelIndex (optional)
211 @return number of rows 216 @return number of rows
212 @rtype int 217 @rtype int
213 """ 218 """
214 if not parent.isValid(): 219 if not parent.isValid():
215 return len(self.__testResults) 220 return len(self.__testResults)
216 221
217 if ( 222 if (
218 parent.internalId() == TopLevelId and 223 parent.internalId() == TopLevelId
219 parent.column() == 0 and 224 and parent.column() == 0
220 self.__testResults[parent.row()].extra is not None 225 and self.__testResults[parent.row()].extra is not None
221 ): 226 ):
222 return len(self.__testResults[parent.row()].extra) 227 return len(self.__testResults[parent.row()].extra)
223 228
224 return 0 229 return 0
225 230
226 def columnCount(self, parent=QModelIndex()): 231 def columnCount(self, parent=QModelIndex()):
227 """ 232 """
228 Public method to get the number of columns. 233 Public method to get the number of columns.
229 234
230 @param parent index of the parent item (defaults to QModelIndex()) 235 @param parent index of the parent item (defaults to QModelIndex())
231 @type QModelIndex (optional) 236 @type QModelIndex (optional)
232 @return number of columns 237 @return number of columns
233 @rtype int 238 @rtype int
234 """ 239 """
235 if not parent.isValid(): 240 if not parent.isValid():
236 return len(TestResultsModel.Headers) 241 return len(TestResultsModel.Headers)
237 else: 242 else:
238 return 1 243 return 1
239 244
240 def clear(self): 245 def clear(self):
241 """ 246 """
242 Public method to clear the model data. 247 Public method to clear the model data.
243 """ 248 """
244 self.beginResetModel() 249 self.beginResetModel()
245 self.__testResults.clear() 250 self.__testResults.clear()
246 self.endResetModel() 251 self.endResetModel()
247 252
248 self.summary.emit("") 253 self.summary.emit("")
249 254
250 def sort(self, column, order): 255 def sort(self, column, order):
251 """ 256 """
252 Public method to sort the model data by column in order. 257 Public method to sort the model data by column in order.
253 258
254 @param column sort column number 259 @param column sort column number
255 @type int 260 @type int
256 @param order sort order 261 @param order sort order
257 @type Qt.SortOrder 262 @type Qt.SortOrder
258 """ # __IGNORE_WARNING_D234r__ 263 """ # __IGNORE_WARNING_D234r__
264
259 def durationKey(result): 265 def durationKey(result):
260 """ 266 """
261 Function to generate a key for duration sorting 267 Function to generate a key for duration sorting
262 268
263 @param result result object 269 @param result result object
264 @type TestResult 270 @type TestResult
265 @return sort key 271 @return sort key
266 @rtype float 272 @rtype float
267 """ 273 """
268 return result.duration or -1.0 274 return result.duration or -1.0
269 275
270 self.beginResetModel() 276 self.beginResetModel()
271 reverse = order == Qt.SortOrder.DescendingOrder 277 reverse = order == Qt.SortOrder.DescendingOrder
272 if column == TestResultsModel.StatusColumn: 278 if column == TestResultsModel.StatusColumn:
273 self.__testResults.sort(key=attrgetter('category', 'status'), 279 self.__testResults.sort(
274 reverse=reverse) 280 key=attrgetter("category", "status"), reverse=reverse
281 )
275 elif column == TestResultsModel.NameColumn: 282 elif column == TestResultsModel.NameColumn:
276 self.__testResults.sort(key=attrgetter('name'), reverse=reverse) 283 self.__testResults.sort(key=attrgetter("name"), reverse=reverse)
277 elif column == TestResultsModel.MessageColumn: 284 elif column == TestResultsModel.MessageColumn:
278 self.__testResults.sort(key=attrgetter('message'), reverse=reverse) 285 self.__testResults.sort(key=attrgetter("message"), reverse=reverse)
279 elif column == TestResultsModel.DurationColumn: 286 elif column == TestResultsModel.DurationColumn:
280 self.__testResults.sort(key=durationKey, reverse=reverse) 287 self.__testResults.sort(key=durationKey, reverse=reverse)
281 self.endResetModel() 288 self.endResetModel()
282 289
283 def getTestResults(self): 290 def getTestResults(self):
284 """ 291 """
285 Public method to get the list of test results managed by the model. 292 Public method to get the list of test results managed by the model.
286 293
287 @return list of test results managed by the model 294 @return list of test results managed by the model
288 @rtype list of TestResult 295 @rtype list of TestResult
289 """ 296 """
290 return copy.deepcopy(self.__testResults) 297 return copy.deepcopy(self.__testResults)
291 298
292 def setTestResults(self, testResults): 299 def setTestResults(self, testResults):
293 """ 300 """
294 Public method to set the list of test results of the model. 301 Public method to set the list of test results of the model.
295 302
296 @param testResults test results to be managed by the model 303 @param testResults test results to be managed by the model
297 @type list of TestResult 304 @type list of TestResult
298 """ 305 """
299 self.beginResetModel() 306 self.beginResetModel()
300 self.__testResults = copy.deepcopy(testResults) 307 self.__testResults = copy.deepcopy(testResults)
301 self.endResetModel() 308 self.endResetModel()
302 309
303 self.summary.emit(self.__summary()) 310 self.summary.emit(self.__summary())
304 311
305 def addTestResults(self, testResults): 312 def addTestResults(self, testResults):
306 """ 313 """
307 Public method to add test results to the ones already managed by the 314 Public method to add test results to the ones already managed by the
308 model. 315 model.
309 316
310 @param testResults test results to be added to the model 317 @param testResults test results to be added to the model
311 @type list of TestResult 318 @type list of TestResult
312 """ 319 """
313 firstRow = len(self.__testResults) 320 firstRow = len(self.__testResults)
314 lastRow = firstRow + len(testResults) - 1 321 lastRow = firstRow + len(testResults) - 1
315 self.beginInsertRows(QModelIndex(), firstRow, lastRow) 322 self.beginInsertRows(QModelIndex(), firstRow, lastRow)
316 self.__testResults.extend(testResults) 323 self.__testResults.extend(testResults)
317 self.endInsertRows() 324 self.endInsertRows()
318 325
319 self.summary.emit(self.__summary()) 326 self.summary.emit(self.__summary())
320 327
321 def updateTestResults(self, testResults): 328 def updateTestResults(self, testResults):
322 """ 329 """
323 Public method to update the data of managed test result items. 330 Public method to update the data of managed test result items.
324 331
325 @param testResults test results to be updated 332 @param testResults test results to be updated
326 @type list of TestResult 333 @type list of TestResult
327 """ 334 """
328 minIndex = None 335 minIndex = None
329 maxIndex = None 336 maxIndex = None
330 337
331 testResultsToBeAdded = [] 338 testResultsToBeAdded = []
332 339
333 for testResult in testResults: 340 for testResult in testResults:
334 for (index, currentResult) in enumerate(self.__testResults): 341 for (index, currentResult) in enumerate(self.__testResults):
335 if currentResult.id == testResult.id: 342 if currentResult.id == testResult.id:
336 self.__testResults[index] = testResult 343 self.__testResults[index] = testResult
337 if minIndex is None: 344 if minIndex is None:
338 minIndex = index 345 minIndex = index
339 maxIndex = index 346 maxIndex = index
340 else: 347 else:
341 minIndex = min(minIndex, index) 348 minIndex = min(minIndex, index)
342 maxIndex = max(maxIndex, index) 349 maxIndex = max(maxIndex, index)
343 350
344 break 351 break
345 else: 352 else:
346 # Test result with given id was not found. 353 # Test result with given id was not found.
347 # Just add it to the list (could be a sub test) 354 # Just add it to the list (could be a sub test)
348 testResultsToBeAdded.append(testResult) 355 testResultsToBeAdded.append(testResult)
349 356
350 if minIndex is not None: 357 if minIndex is not None:
351 self.dataChanged.emit( 358 self.dataChanged.emit(
352 self.index(minIndex, 0), 359 self.index(minIndex, 0),
353 self.index(maxIndex, len(TestResultsModel.Headers) - 1) 360 self.index(maxIndex, len(TestResultsModel.Headers) - 1),
354 ) 361 )
355 362
356 self.summary.emit(self.__summary()) 363 self.summary.emit(self.__summary())
357 364
358 if testResultsToBeAdded: 365 if testResultsToBeAdded:
359 self.addTestResults(testResultsToBeAdded) 366 self.addTestResults(testResultsToBeAdded)
360 367
361 def getFailedTests(self): 368 def getFailedTests(self):
362 """ 369 """
363 Public method to extract the test ids of all failed tests. 370 Public method to extract the test ids of all failed tests.
364 371
365 @return test ids of all failed tests 372 @return test ids of all failed tests
366 @rtype list of str 373 @rtype list of str
367 """ 374 """
368 failedIds = [ 375 failedIds = [
369 res.id for res in self.__testResults if ( 376 res.id
370 res.category == TestResultCategory.FAIL and 377 for res in self.__testResults
371 not res.subtestResult 378 if (res.category == TestResultCategory.FAIL and not res.subtestResult)
372 )
373 ] 379 ]
374 return failedIds 380 return failedIds
375 381
376 def __summary(self): 382 def __summary(self):
377 """ 383 """
378 Private method to generate a test results summary text. 384 Private method to generate a test results summary text.
379 385
380 @return test results summary text 386 @return test results summary text
381 @rtype str 387 @rtype str
382 """ 388 """
383 if len(self.__testResults) == 0: 389 if len(self.__testResults) == 0:
384 return self.tr("No results to show") 390 return self.tr("No results to show")
385 391
386 counts = Counter(res.category for res in self.__testResults) 392 counts = Counter(res.category for res in self.__testResults)
387 if all( 393 if all(
388 counts[category] == 0 394 counts[category] == 0
389 for category in (TestResultCategory.FAIL, TestResultCategory.OK, 395 for category in (
390 TestResultCategory.SKIP) 396 TestResultCategory.FAIL,
397 TestResultCategory.OK,
398 TestResultCategory.SKIP,
399 )
391 ): 400 ):
392 return self.tr("Collected %n test(s)", "", len(self.__testResults)) 401 return self.tr("Collected %n test(s)", "", len(self.__testResults))
393 402
394 return self.tr( 403 return self.tr(
395 "%n test(s)/subtest(s) total, {0} failed, {1} passed," 404 "%n test(s)/subtest(s) total, {0} failed, {1} passed,"
396 " {2} skipped, {3} pending", 405 " {2} skipped, {3} pending",
397 "", len(self.__testResults) 406 "",
407 len(self.__testResults),
398 ).format( 408 ).format(
399 counts[TestResultCategory.FAIL], 409 counts[TestResultCategory.FAIL],
400 counts[TestResultCategory.OK], 410 counts[TestResultCategory.OK],
401 counts[TestResultCategory.SKIP], 411 counts[TestResultCategory.SKIP],
402 counts[TestResultCategory.PENDING] 412 counts[TestResultCategory.PENDING],
403 ) 413 )
404 414
405 415
406 class TestResultsTreeView(QTreeView): 416 class TestResultsTreeView(QTreeView):
407 """ 417 """
408 Class implementing a tree view to show the test result data. 418 Class implementing a tree view to show the test result data.
409 419
410 @signal goto(str, int) emitted to go to the position given by file name 420 @signal goto(str, int) emitted to go to the position given by file name
411 and line number 421 and line number
412 """ 422 """
423
413 goto = pyqtSignal(str, int) 424 goto = pyqtSignal(str, int)
414 425
415 def __init__(self, parent=None): 426 def __init__(self, parent=None):
416 """ 427 """
417 Constructor 428 Constructor
418 429
419 @param parent reference to the parent widget (defaults to None) 430 @param parent reference to the parent widget (defaults to None)
420 @type QWidget (optional) 431 @type QWidget (optional)
421 """ 432 """
422 super().__init__(parent) 433 super().__init__(parent)
423 434
424 self.setItemsExpandable(True) 435 self.setItemsExpandable(True)
425 self.setExpandsOnDoubleClick(False) 436 self.setExpandsOnDoubleClick(False)
426 self.setSortingEnabled(True) 437 self.setSortingEnabled(True)
427 438
428 self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) 439 self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter)
429 self.header().setSortIndicatorShown(False) 440 self.header().setSortIndicatorShown(False)
430 441
431 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 442 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
432 443
433 # connect signals and slots 444 # connect signals and slots
434 self.doubleClicked.connect(self.__gotoTestDefinition) 445 self.doubleClicked.connect(self.__gotoTestDefinition)
435 self.customContextMenuRequested.connect(self.__showContextMenu) 446 self.customContextMenuRequested.connect(self.__showContextMenu)
436 447
437 self.header().sortIndicatorChanged.connect(self.sortByColumn) 448 self.header().sortIndicatorChanged.connect(self.sortByColumn)
438 self.header().sortIndicatorChanged.connect( 449 self.header().sortIndicatorChanged.connect(
439 lambda column, order: self.header().setSortIndicatorShown(True)) 450 lambda column, order: self.header().setSortIndicatorShown(True)
440 451 )
452
441 def reset(self): 453 def reset(self):
442 """ 454 """
443 Public method to reset the internal state of the view. 455 Public method to reset the internal state of the view.
444 """ 456 """
445 super().reset() 457 super().reset()
446 458
447 self.resizeColumns() 459 self.resizeColumns()
448 self.spanFirstColumn(0, self.model().rowCount() - 1) 460 self.spanFirstColumn(0, self.model().rowCount() - 1)
449 461
450 def rowsInserted(self, parent, startRow, endRow): 462 def rowsInserted(self, parent, startRow, endRow):
451 """ 463 """
452 Public method called when rows are inserted. 464 Public method called when rows are inserted.
453 465
454 @param parent model index of the parent item 466 @param parent model index of the parent item
455 @type QModelIndex 467 @type QModelIndex
456 @param startRow first row been inserted 468 @param startRow first row been inserted
457 @type int 469 @type int
458 @param endRow last row been inserted 470 @param endRow last row been inserted
459 @type int 471 @type int
460 """ 472 """
461 super().rowsInserted(parent, startRow, endRow) 473 super().rowsInserted(parent, startRow, endRow)
462 474
463 self.resizeColumns() 475 self.resizeColumns()
464 self.spanFirstColumn(startRow, endRow) 476 self.spanFirstColumn(startRow, endRow)
465 477
466 def dataChanged(self, topLeft, bottomRight, roles=[]): 478 def dataChanged(self, topLeft, bottomRight, roles=[]):
467 """ 479 """
468 Public method called when the model data has changed. 480 Public method called when the model data has changed.
469 481
470 @param topLeft index of the top left element 482 @param topLeft index of the top left element
471 @type QModelIndex 483 @type QModelIndex
472 @param bottomRight index of the bottom right element 484 @param bottomRight index of the bottom right element
473 @type QModelIndex 485 @type QModelIndex
474 @param roles list of roles changed (defaults to []) 486 @param roles list of roles changed (defaults to [])
475 @type list of Qt.ItemDataRole (optional) 487 @type list of Qt.ItemDataRole (optional)
476 """ 488 """
477 super().dataChanged(topLeft, bottomRight, roles) 489 super().dataChanged(topLeft, bottomRight, roles)
478 490
479 self.resizeColumns() 491 self.resizeColumns()
480 while topLeft.parent().isValid(): 492 while topLeft.parent().isValid():
481 topLeft = topLeft.parent() 493 topLeft = topLeft.parent()
482 while bottomRight.parent().isValid(): 494 while bottomRight.parent().isValid():
483 bottomRight = bottomRight.parent() 495 bottomRight = bottomRight.parent()
484 self.spanFirstColumn(topLeft.row(), bottomRight.row()) 496 self.spanFirstColumn(topLeft.row(), bottomRight.row())
485 497
486 def resizeColumns(self): 498 def resizeColumns(self):
487 """ 499 """
488 Public method to resize the columns to their contents. 500 Public method to resize the columns to their contents.
489 """ 501 """
490 for column in range(self.model().columnCount()): 502 for column in range(self.model().columnCount()):
491 self.resizeColumnToContents(column) 503 self.resizeColumnToContents(column)
492 504
493 def spanFirstColumn(self, startRow, endRow): 505 def spanFirstColumn(self, startRow, endRow):
494 """ 506 """
495 Public method to make the first column span the row for second level 507 Public method to make the first column span the row for second level
496 items. 508 items.
497 509
498 These items contain the test results. 510 These items contain the test results.
499 511
500 @param startRow index of the first row to span 512 @param startRow index of the first row to span
501 @type QModelIndex 513 @type QModelIndex
502 @param endRow index of the last row (including) to span 514 @param endRow index of the last row (including) to span
503 @type QModelIndex 515 @type QModelIndex
504 """ 516 """
505 model = self.model() 517 model = self.model()
506 for row in range(startRow, endRow + 1): 518 for row in range(startRow, endRow + 1):
507 index = model.index(row, 0) 519 index = model.index(row, 0)
508 for i in range(model.rowCount(index)): 520 for i in range(model.rowCount(index)):
509 self.setFirstColumnSpanned(i, index, True) 521 self.setFirstColumnSpanned(i, index, True)
510 522
511 def __canonicalIndex(self, index): 523 def __canonicalIndex(self, index):
512 """ 524 """
513 Private method to create the canonical index for a given index. 525 Private method to create the canonical index for a given index.
514 526
515 The canonical index is the index of the first column of the test 527 The canonical index is the index of the first column of the test
516 result entry (i.e. the top-level item). If the index is invalid, 528 result entry (i.e. the top-level item). If the index is invalid,
517 None is returned. 529 None is returned.
518 530
519 @param index index to determine the canonical index for 531 @param index index to determine the canonical index for
520 @type QModelIndex 532 @type QModelIndex
521 @return index of the firt column of the associated top-level item index 533 @return index of the firt column of the associated top-level item index
522 @rtype QModelIndex 534 @rtype QModelIndex
523 """ 535 """
524 if not index.isValid(): 536 if not index.isValid():
525 return None 537 return None
526 538
527 while index.parent().isValid(): # find the top-level node 539 while index.parent().isValid(): # find the top-level node
528 index = index.parent() 540 index = index.parent()
529 index = index.sibling(index.row(), 0) # go to first column 541 index = index.sibling(index.row(), 0) # go to first column
530 return index 542 return index
531 543
532 @pyqtSlot(QModelIndex) 544 @pyqtSlot(QModelIndex)
533 def __gotoTestDefinition(self, index): 545 def __gotoTestDefinition(self, index):
534 """ 546 """
535 Private slot to show the test definition. 547 Private slot to show the test definition.
536 548
537 @param index index for the double-clicked item 549 @param index index for the double-clicked item
538 @type QModelIndex 550 @type QModelIndex
539 """ 551 """
540 cindex = self.__canonicalIndex(index) 552 cindex = self.__canonicalIndex(index)
541 filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole) 553 filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole)
542 if filename is not None: 554 if filename is not None:
543 if lineno is None: 555 if lineno is None:
544 lineno = 1 556 lineno = 1
545 self.goto.emit(filename, lineno) 557 self.goto.emit(filename, lineno)
546 558
547 @pyqtSlot(QPoint) 559 @pyqtSlot(QPoint)
548 def __showContextMenu(self, pos): 560 def __showContextMenu(self, pos):
549 """ 561 """
550 Private slot to show the context menu. 562 Private slot to show the context menu.
551 563
552 @param pos relative position for the context menu 564 @param pos relative position for the context menu
553 @type QPoint 565 @type QPoint
554 """ 566 """
555 index = self.indexAt(pos) 567 index = self.indexAt(pos)
556 cindex = self.__canonicalIndex(index) 568 cindex = self.__canonicalIndex(index)
557 569
558 contextMenu = ( 570 contextMenu = (
559 self.__createContextMenu(cindex) 571 self.__createContextMenu(cindex)
560 if cindex else 572 if cindex
561 self.__createBackgroundContextMenu() 573 else self.__createBackgroundContextMenu()
562 ) 574 )
563 contextMenu.exec(self.mapToGlobal(pos)) 575 contextMenu.exec(self.mapToGlobal(pos))
564 576
565 def __createContextMenu(self, index): 577 def __createContextMenu(self, index):
566 """ 578 """
567 Private method to create a context menu for the item pointed to by the 579 Private method to create a context menu for the item pointed to by the
568 given index. 580 given index.
569 581
570 @param index index of the item 582 @param index index of the item
571 @type QModelIndex 583 @type QModelIndex
572 @return created context menu 584 @return created context menu
573 @rtype QMenu 585 @rtype QMenu
574 """ 586 """
575 menu = QMenu(self) 587 menu = QMenu(self)
576 if self.isExpanded(index): 588 if self.isExpanded(index):
577 menu.addAction(self.tr("Collapse"), 589 menu.addAction(self.tr("Collapse"), lambda: self.collapse(index))
578 lambda: self.collapse(index))
579 else: 590 else:
580 act = menu.addAction(self.tr("Expand"), 591 act = menu.addAction(self.tr("Expand"), lambda: self.expand(index))
581 lambda: self.expand(index))
582 act.setEnabled(self.model().hasChildren(index)) 592 act.setEnabled(self.model().hasChildren(index))
583 menu.addSeparator() 593 menu.addSeparator()
584 594
585 act = menu.addAction(self.tr("Show Source"), 595 act = menu.addAction(
586 lambda: self.__gotoTestDefinition(index)) 596 self.tr("Show Source"), lambda: self.__gotoTestDefinition(index)
587 act.setEnabled(
588 self.model().data(index, Qt.ItemDataRole.UserRole) is not None
589 ) 597 )
598 act.setEnabled(self.model().data(index, Qt.ItemDataRole.UserRole) is not None)
590 menu.addSeparator() 599 menu.addSeparator()
591 600
592 menu.addAction(self.tr("Collapse All"), self.collapseAll) 601 menu.addAction(self.tr("Collapse All"), self.collapseAll)
593 menu.addAction(self.tr("Expand All"), self.expandAll) 602 menu.addAction(self.tr("Expand All"), self.expandAll)
594 603
595 return menu 604 return menu
596 605
597 def __createBackgroundContextMenu(self): 606 def __createBackgroundContextMenu(self):
598 """ 607 """
599 Private method to create a context menu for the background. 608 Private method to create a context menu for the background.
600 609
601 @return created context menu 610 @return created context menu
602 @rtype QMenu 611 @rtype QMenu
603 """ 612 """
604 menu = QMenu(self) 613 menu = QMenu(self)
605 menu.addAction(self.tr("Collapse All"), self.collapseAll) 614 menu.addAction(self.tr("Collapse All"), self.collapseAll)
606 menu.addAction(self.tr("Expand All"), self.expandAll) 615 menu.addAction(self.tr("Expand All"), self.expandAll)
607 616
608 return menu 617 return menu
618
609 619
610 # 620 #
611 # eflag: noqa = M821, M822 621 # eflag: noqa = M821, M822

eric ide

mercurial