eric7/DataViews/PyCoverageDialog.py

branch
eric7-maintenance
changeset 9111
4ac66b6c33a4
parent 8943
23f9c7b9e18e
child 9192
a763d57e23bc
equal deleted inserted replaced
9049:2b9bd8f97576 9111:4ac66b6c33a4
5 5
6 """ 6 """
7 Module implementing a Python code coverage dialog. 7 Module implementing a Python code coverage dialog.
8 """ 8 """
9 9
10 import contextlib
11 import os 10 import os
12 import time 11 import time
13 12
14 from PyQt6.QtCore import pyqtSlot, Qt 13 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
14 from PyQt6.QtGui import QDesktopServices
15 from PyQt6.QtWidgets import ( 15 from PyQt6.QtWidgets import (
16 QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem, 16 QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem,
17 QApplication 17 QApplication
18 ) 18 )
19 19
20 from EricWidgets import EricMessageBox 20 from EricWidgets import EricMessageBox
21 from EricWidgets.EricApplication import ericApp 21 from EricWidgets.EricApplication import ericApp
22 from EricWidgets.EricProgressDialog import EricProgressDialog
23 22
24 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog 23 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
25 24
26 import Utilities 25 import Utilities
27 from coverage import Coverage 26 from coverage import Coverage
29 28
30 29
31 class PyCoverageDialog(QDialog, Ui_PyCoverageDialog): 30 class PyCoverageDialog(QDialog, Ui_PyCoverageDialog):
32 """ 31 """
33 Class implementing a dialog to display the collected code coverage data. 32 Class implementing a dialog to display the collected code coverage data.
33
34 @signal openFile(str) emitted to open the given file in an editor
34 """ 35 """
36 openFile = pyqtSignal(str)
37
35 def __init__(self, parent=None): 38 def __init__(self, parent=None):
36 """ 39 """
37 Constructor 40 Constructor
38 41
39 @param parent parent widget (QWidget) 42 @param parent parent widget
43 @type QWidget
40 """ 44 """
41 super().__init__(parent) 45 super().__init__(parent)
42 self.setupUi(self) 46 self.setupUi(self)
43 self.setWindowFlags(Qt.WindowType.Window) 47 self.setWindowFlags(Qt.WindowType.Window)
44 48
54 self.cancelled = False 58 self.cancelled = False
55 self.path = '.' 59 self.path = '.'
56 self.reload = False 60 self.reload = False
57 61
58 self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]'] 62 self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]']
63
64 self.__reportsMenu = QMenu(self.tr("Create Report"), self)
65 self.__reportsMenu.addAction(self.tr("HTML Report"), self.__htmlReport)
66 self.__reportsMenu.addSeparator()
67 self.__reportsMenu.addAction(self.tr("JSON Report"), self.__jsonReport)
68 self.__reportsMenu.addAction(self.tr("LCOV Report"), self.__lcovReport)
59 69
60 self.__menu = QMenu(self) 70 self.__menu = QMenu(self)
61 self.__menu.addSeparator() 71 self.__menu.addSeparator()
62 self.openAct = self.__menu.addAction( 72 self.openAct = self.__menu.addAction(
63 self.tr("Open"), self.__openFile) 73 self.tr("Open"), self.__openFile)
64 self.__menu.addSeparator() 74 self.__menu.addSeparator()
65 self.annotate = self.__menu.addAction( 75 self.__menu.addMenu(self.__reportsMenu)
66 self.tr('Annotate'), self.__annotate)
67 self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll)
68 self.__menu.addAction(
69 self.tr('Delete annotated files'), self.__deleteAnnotated)
70 self.__menu.addSeparator() 76 self.__menu.addSeparator()
71 self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase) 77 self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase)
72 self.resultList.setContextMenuPolicy( 78 self.resultList.setContextMenuPolicy(
73 Qt.ContextMenuPolicy.CustomContextMenu) 79 Qt.ContextMenuPolicy.CustomContextMenu)
74 self.resultList.customContextMenuRequested.connect( 80 self.resultList.customContextMenuRequested.connect(
78 """ 84 """
79 Private method to format a list of integers into string by coalescing 85 Private method to format a list of integers into string by coalescing
80 groups. 86 groups.
81 87
82 @param lines list of integers 88 @param lines list of integers
89 @type list of int
83 @return string representing the list 90 @return string representing the list
91 @rtype str
84 """ 92 """
85 pairs = [] 93 pairs = []
86 lines.sort() 94 lines.sort()
87 maxValue = lines[-1] 95 maxValue = lines[-1]
88 start = None 96 start = None
108 """ 116 """
109 Private helper function to generate a string representation of a 117 Private helper function to generate a string representation of a
110 pair. 118 pair.
111 119
112 @param pair pair of integers 120 @param pair pair of integers
121 @type tuple of (int, int
122 @return representation of the pair
123 @rtype str
113 """ 124 """
114 start, end = pair 125 start, end = pair
115 if start == end: 126 if start == end:
116 return "{0:d}".format(start) 127 return "{0:d}".format(start)
117 else: 128 else:
122 def __createResultItem(self, file, statements, executed, coverage, 133 def __createResultItem(self, file, statements, executed, coverage,
123 excluded, missing): 134 excluded, missing):
124 """ 135 """
125 Private method to create an entry in the result list. 136 Private method to create an entry in the result list.
126 137
127 @param file filename of file (string) 138 @param file filename of file
128 @param statements amount of statements (integer) 139 @type str
129 @param executed amount of executed statements (integer) 140 @param statements number of statements
130 @param coverage percent of coverage (integer) 141 @type int
131 @param excluded list of excluded lines (string) 142 @param executed number of executed statements
132 @param missing list of lines without coverage (string) 143 @type int
144 @param coverage percent of coverage
145 @type int
146 @param excluded list of excluded lines
147 @type str
148 @param missing list of lines without coverage
149 @type str
133 """ 150 """
134 itm = QTreeWidgetItem(self.resultList, [ 151 itm = QTreeWidgetItem(self.resultList, [
135 file, 152 file,
136 str(statements), 153 str(statements),
137 str(executed), 154 str(executed),
144 if statements != executed: 161 if statements != executed:
145 font = itm.font(0) 162 font = itm.font(0)
146 font.setBold(True) 163 font.setBold(True)
147 for col in range(itm.columnCount()): 164 for col in range(itm.columnCount()):
148 itm.setFont(col, font) 165 itm.setFont(col, font)
149 166
150 def start(self, cfn, fn): 167 def start(self, cfn, fn):
151 """ 168 """
152 Public slot to start the coverage data evaluation. 169 Public slot to start the coverage data evaluation.
153 170
154 @param cfn basename of the coverage file (string) 171 @param cfn basename of the coverage file
172 @type str
155 @param fn file or list of files or directory to be checked 173 @param fn file or list of files or directory to be checked
156 (string or list of strings) 174 @type str or list of str
157 """ 175 """
176 # initialize the dialog
177 self.resultList.clear()
178 self.summaryList.clear()
179 self.cancelled = False
180 self.buttonBox.button(
181 QDialogButtonBox.StandardButton.Close).setEnabled(False)
182 self.buttonBox.button(
183 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
184 self.buttonBox.button(
185 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
186
158 self.__cfn = cfn 187 self.__cfn = cfn
159 self.__fn = fn 188 self.__fn = fn
160 189
161 self.basename = os.path.splitext(cfn)[0] 190 self.cfn = (
162 191 cfn
163 self.cfn = "{0}.coverage".format(self.basename) 192 if cfn.endswith(".coverage") else
193 "{0}.coverage".format(os.path.splitext(cfn)[0])
194 )
164 195
165 if isinstance(fn, list): 196 if isinstance(fn, list):
166 files = fn 197 files = fn
167 self.path = os.path.dirname(cfn) 198 self.path = os.path.dirname(cfn)
168 elif os.path.isdir(fn): 199 elif os.path.isdir(fn):
249 self.tr("""%n file(s) could not be parsed. Coverage""" 280 self.tr("""%n file(s) could not be parsed. Coverage"""
250 """ info for these is not available.""", "", 281 """ info for these is not available.""", "",
251 total_exceptions)) 282 total_exceptions))
252 283
253 self.__finish() 284 self.__finish()
254 285
255 def __finish(self): 286 def __finish(self):
256 """ 287 """
257 Private slot called when the action finished or the user pressed the 288 Private slot called when the action finished or the user pressed the
258 button. 289 button.
259 """ 290 """
269 QHeaderView.ResizeMode.ResizeToContents) 300 QHeaderView.ResizeMode.ResizeToContents)
270 self.resultList.header().setStretchLastSection(True) 301 self.resultList.header().setStretchLastSection(True)
271 self.summaryList.header().resizeSections( 302 self.summaryList.header().resizeSections(
272 QHeaderView.ResizeMode.ResizeToContents) 303 QHeaderView.ResizeMode.ResizeToContents)
273 self.summaryList.header().setStretchLastSection(True) 304 self.summaryList.header().setStretchLastSection(True)
274 305
275 def on_buttonBox_clicked(self, button): 306 def on_buttonBox_clicked(self, button):
276 """ 307 """
277 Private slot called by a button of the button box clicked. 308 Private slot called by a button of the button box clicked.
278 309
279 @param button button that was clicked (QAbstractButton) 310 @param button button that was clicked
311 @type QAbstractButton
280 """ 312 """
281 if button == self.buttonBox.button( 313 if button == self.buttonBox.button(
282 QDialogButtonBox.StandardButton.Close 314 QDialogButtonBox.StandardButton.Close
283 ): 315 ):
284 self.close() 316 self.close()
285 elif button == self.buttonBox.button( 317 elif button == self.buttonBox.button(
286 QDialogButtonBox.StandardButton.Cancel 318 QDialogButtonBox.StandardButton.Cancel
287 ): 319 ):
288 self.__finish() 320 self.__finish()
289 321
290 def __showContextMenu(self, coord): 322 def __showContextMenu(self, coord):
291 """ 323 """
292 Private slot to show the context menu of the listview. 324 Private slot to show the context menu of the listview.
293 325
294 @param coord the position of the mouse pointer (QPoint) 326 @param coord position of the mouse pointer
327 @type QPoint
295 """ 328 """
296 itm = self.resultList.itemAt(coord) 329 itm = self.resultList.itemAt(coord)
297 if itm: 330 if itm:
298 self.annotate.setEnabled(True)
299 self.openAct.setEnabled(True) 331 self.openAct.setEnabled(True)
300 else: 332 else:
301 self.annotate.setEnabled(False)
302 self.openAct.setEnabled(False) 333 self.openAct.setEnabled(False)
334 self.__reportsMenu.setEnabled(
335 bool(self.resultList.topLevelItemCount()))
303 self.__menu.popup(self.mapToGlobal(coord)) 336 self.__menu.popup(self.mapToGlobal(coord))
304 337
305 def __openFile(self, itm=None): 338 def __openFile(self, itm=None):
306 """ 339 """
307 Private slot to open the selected file. 340 Private slot to open the selected file.
308 341
309 @param itm reference to the item to be opened (QTreeWidgetItem) 342 @param itm reference to the item to be opened
343 @type QTreeWidgetItem
310 """ 344 """
311 if itm is None: 345 if itm is None:
312 itm = self.resultList.currentItem() 346 itm = self.resultList.currentItem()
313 fn = itm.text(0) 347 fn = itm.text(0)
314 348
315 vm = ericApp().getObject("ViewManager") 349 try:
316 vm.openSourceFile(fn) 350 vm = ericApp().getObject("ViewManager")
317 editor = vm.getOpenEditor(fn) 351 vm.openSourceFile(fn)
318 editor.codeCoverageShowAnnotations() 352 editor = vm.getOpenEditor(fn)
319 353 editor.codeCoverageShowAnnotations(coverageFile=self.cfn)
320 def __annotate(self): 354 except KeyError:
321 """ 355 self.openFile.emit(fn)
322 Private slot to handle the annotate context menu action. 356
323 357 def __prepareReportGeneration(self):
324 This method produce an annotated coverage file of the 358 """
325 selected file. 359 Private method to prepare a report generation.
326 """ 360
327 itm = self.resultList.currentItem() 361 @return tuple containing a reference to the Coverage object and the
328 fn = itm.text(0) 362 list of files to report
363 @rtype tuple of (Coverage, list of str)
364 """
365 count = self.resultList.topLevelItemCount()
366 if count == 0:
367 return None, []
368
369 # get list of all filenames
370 files = [
371 self.resultList.topLevelItem(index).text(0)
372 for index in range(count)
373 ]
329 374
330 cover = Coverage(data_file=self.cfn) 375 cover = Coverage(data_file=self.cfn)
331 cover.exclude(self.excludeList[0]) 376 cover.exclude(self.excludeList[0])
332 cover.load() 377 cover.load()
333 cover.annotate([fn], None, True) 378
334 379 return cover, files
335 def __annotateAll(self): 380
336 """ 381 @pyqtSlot()
337 Private slot to handle the annotate all context menu action. 382 def __htmlReport(self):
338 383 """
339 This method produce an annotated coverage file of every 384 Private slot to generate a HTML report of the shown data.
340 file listed in the listview. 385 """
341 """ 386 from .PyCoverageHtmlReportDialog import PyCoverageHtmlReportDialog
342 amount = self.resultList.topLevelItemCount() 387
343 if amount == 0: 388 dlg = PyCoverageHtmlReportDialog(os.path.dirname(self.cfn), self)
344 return 389 if dlg.exec() == QDialog.DialogCode.Accepted:
345 390 title, outputDirectory, extraCSS, openReport = dlg.getData()
346 # get list of all filenames 391
347 files = [] 392 cover, files = self.__prepareReportGeneration()
348 for index in range(amount): 393 cover.html_report(morfs=files, directory=outputDirectory,
349 itm = self.resultList.topLevelItem(index) 394 ignore_errors=True, extra_css=extraCSS,
350 files.append(itm.text(0)) 395 title=title)
351 396
352 cover = Coverage(data_file=self.cfn) 397 if openReport:
353 cover.exclude(self.excludeList[0]) 398 QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.join(
354 cover.load() 399 outputDirectory, "index.html")))
355 400
356 # now process them 401 @pyqtSlot()
357 progress = EricProgressDialog( 402 def __jsonReport(self):
358 self.tr("Annotating files..."), self.tr("Abort"), 403 """
359 0, len(files), self.tr("%v/%m Files"), self) 404 Private slot to generate a JSON report of the shown data.
360 progress.setMinimumDuration(0) 405 """
361 progress.setWindowTitle(self.tr("Coverage")) 406 from .PyCoverageJsonReportDialog import PyCoverageJsonReportDialog
362 407
363 for count, file in enumerate(files): 408 dlg = PyCoverageJsonReportDialog(os.path.dirname(self.cfn), self)
364 progress.setValue(count) 409 if dlg.exec() == QDialog.DialogCode.Accepted:
365 if progress.wasCanceled(): 410 filename, compact = dlg.getData()
366 break 411 cover, files = self.__prepareReportGeneration()
367 cover.annotate([file], None) # , True) 412 cover.json_report(morfs=files, outfile=filename,
368 413 ignore_errors=True, pretty_print=not compact)
369 progress.setValue(len(files)) 414
370 415 @pyqtSlot()
416 def __lcovReport(self):
417 """
418 Private slot to generate a LCOV report of the shown data.
419 """
420 from EricWidgets import EricPathPickerDialog
421 from EricWidgets.EricPathPicker import EricPathPickerModes
422
423 filename, ok = EricPathPickerDialog.getPath(
424 self,
425 self.tr("LCOV Report"),
426 self.tr("Enter the path of the output file:"),
427 mode=EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE,
428 path=os.path.join(os.path.dirname(self.cfn), "coverage.lcov"),
429 defaultDirectory=os.path.dirname(self.cfn),
430 filters=self.tr("LCOV Files (*.lcov);;All Files (*)")
431 )
432 if ok:
433 cover, files = self.__prepareReportGeneration()
434 cover.lcov_report(morfs=files, outfile=filename,
435 ignore_errors=True)
436
371 def __erase(self): 437 def __erase(self):
372 """ 438 """
373 Private slot to handle the erase context menu action. 439 Private slot to handle the erase context menu action.
374 440
375 This method erases the collected coverage data that is 441 This method erases the collected coverage data that is
380 cover.erase() 446 cover.erase()
381 447
382 self.reloadButton.setEnabled(False) 448 self.reloadButton.setEnabled(False)
383 self.resultList.clear() 449 self.resultList.clear()
384 self.summaryList.clear() 450 self.summaryList.clear()
385 451
386 def __deleteAnnotated(self):
387 """
388 Private slot to handle the delete annotated context menu action.
389
390 This method deletes all annotated files. These are files
391 ending with ',cover'.
392 """
393 files = Utilities.direntries(self.path, True, '*,cover', False)
394 for file in files:
395 with contextlib.suppress(OSError):
396 os.remove(file)
397
398 @pyqtSlot() 452 @pyqtSlot()
399 def on_reloadButton_clicked(self): 453 def on_reloadButton_clicked(self):
400 """ 454 """
401 Private slot to reload the coverage info. 455 Private slot to reload the coverage info.
402 """ 456 """
403 self.resultList.clear()
404 self.summaryList.clear()
405 self.reload = True 457 self.reload = True
406 excludePattern = self.excludeCombo.currentText() 458 excludePattern = self.excludeCombo.currentText()
407 if excludePattern in self.excludeList: 459 if excludePattern in self.excludeList:
408 self.excludeList.remove(excludePattern) 460 self.excludeList.remove(excludePattern)
409 self.excludeList.insert(0, excludePattern) 461 self.excludeList.insert(0, excludePattern)
410 self.cancelled = False
411 self.buttonBox.button(
412 QDialogButtonBox.StandardButton.Close).setEnabled(False)
413 self.buttonBox.button(
414 QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
415 self.buttonBox.button(
416 QDialogButtonBox.StandardButton.Cancel).setDefault(True)
417 self.start(self.__cfn, self.__fn) 462 self.start(self.__cfn, self.__fn)
418 463
419 @pyqtSlot(QTreeWidgetItem, int) 464 @pyqtSlot(QTreeWidgetItem, int)
420 def on_resultList_itemActivated(self, item, column): 465 def on_resultList_itemActivated(self, item, column):
421 """ 466 """

eric ide

mercurial