eric6/DataViews/PyCoverageDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a Python code coverage dialog.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import pyqtSlot, Qt
15 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMenu, QHeaderView, \
16 QTreeWidgetItem, QApplication
17
18 from E5Gui import E5MessageBox
19 from E5Gui.E5Application import e5App
20 from E5Gui.E5ProgressDialog import E5ProgressDialog
21
22 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
23
24 import Utilities
25 from coverage import Coverage
26 from coverage.misc import CoverageException
27
28
29 class PyCoverageDialog(QDialog, Ui_PyCoverageDialog):
30 """
31 Class implementing a dialog to display the collected code coverage data.
32 """
33 def __init__(self, parent=None):
34 """
35 Constructor
36
37 @param parent parent widget (QWidget)
38 """
39 super(PyCoverageDialog, self).__init__(parent)
40 self.setupUi(self)
41 self.setWindowFlags(Qt.Window)
42
43 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
44 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
45
46 self.summaryList.headerItem().setText(
47 self.summaryList.columnCount(), "")
48 self.resultList.headerItem().setText(self.resultList.columnCount(), "")
49
50 self.cancelled = False
51 self.path = '.'
52 self.reload = False
53
54 self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]']
55
56 self.__menu = QMenu(self)
57 self.__menu.addSeparator()
58 self.openAct = self.__menu.addAction(
59 self.tr("Open"), self.__openFile)
60 self.__menu.addSeparator()
61 self.annotate = self.__menu.addAction(
62 self.tr('Annotate'), self.__annotate)
63 self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll)
64 self.__menu.addAction(
65 self.tr('Delete annotated files'), self.__deleteAnnotated)
66 self.__menu.addSeparator()
67 self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase)
68 self.resultList.setContextMenuPolicy(Qt.CustomContextMenu)
69 self.resultList.customContextMenuRequested.connect(
70 self.__showContextMenu)
71
72 def __format_lines(self, lines):
73 """
74 Private method to format a list of integers into string by coalescing
75 groups.
76
77 @param lines list of integers
78 @return string representing the list
79 """
80 pairs = []
81 lines.sort()
82 maxValue = lines[-1]
83 start = None
84
85 i = lines[0]
86 while i <= maxValue:
87 try:
88 if start is None:
89 start = i
90 ind = lines.index(i)
91 end = i
92 i += 1
93 except ValueError:
94 pairs.append((start, end))
95 start = None
96 if ind + 1 >= len(lines):
97 break
98 i = lines[ind + 1]
99 if start:
100 pairs.append((start, end))
101
102 def stringify(pair):
103 """
104 Private helper function to generate a string representation of a
105 pair.
106
107 @param pair pair of integers
108 """
109 start, end = pair
110 if start == end:
111 return "{0:d}".format(start)
112 else:
113 return "{0:d}-{1:d}".format(start, end)
114
115 return ", ".join(map(stringify, pairs))
116
117 def __createResultItem(self, file, statements, executed, coverage,
118 excluded, missing):
119 """
120 Private method to create an entry in the result list.
121
122 @param file filename of file (string)
123 @param statements amount of statements (integer)
124 @param executed amount of executed statements (integer)
125 @param coverage percent of coverage (integer)
126 @param excluded list of excluded lines (string)
127 @param missing list of lines without coverage (string)
128 """
129 itm = QTreeWidgetItem(self.resultList, [
130 file,
131 str(statements),
132 str(executed),
133 "{0:.0f}%".format(coverage),
134 excluded,
135 missing
136 ])
137 for col in range(1, 4):
138 itm.setTextAlignment(col, Qt.AlignRight)
139 if statements != executed:
140 font = itm.font(0)
141 font.setBold(True)
142 for col in range(itm.columnCount()):
143 itm.setFont(col, font)
144
145 def start(self, cfn, fn):
146 """
147 Public slot to start the coverage data evaluation.
148
149 @param cfn basename of the coverage file (string)
150 @param fn file or list of files or directory to be checked
151 (string or list of strings)
152 """
153 self.__cfn = cfn
154 self.__fn = fn
155
156 self.basename = os.path.splitext(cfn)[0]
157
158 self.cfn = "{0}.coverage".format(self.basename)
159
160 if isinstance(fn, list):
161 files = fn
162 self.path = os.path.dirname(cfn)
163 elif os.path.isdir(fn):
164 files = Utilities.direntries(fn, True, '*.py', False)
165 self.path = fn
166 else:
167 files = [fn]
168 self.path = os.path.dirname(cfn)
169 files.sort()
170
171 cover = Coverage(data_file=self.cfn)
172 cover.load()
173
174 # set the exclude pattern
175 self.excludeCombo.clear()
176 self.excludeCombo.addItems(self.excludeList)
177
178 self.checkProgress.setMaximum(len(files))
179 QApplication.processEvents()
180
181 total_statements = 0
182 total_executed = 0
183 total_exceptions = 0
184
185 cover.exclude(self.excludeList[0])
186 progress = 0
187
188 try:
189 # disable updates of the list for speed
190 self.resultList.setUpdatesEnabled(False)
191 self.resultList.setSortingEnabled(False)
192
193 # now go through all the files
194 for file in files:
195 if self.cancelled:
196 return
197
198 try:
199 statements, excluded, missing, readable = \
200 cover.analysis2(file)[1:]
201 readableEx = (excluded and self.__format_lines(excluded) or
202 '')
203 n = len(statements)
204 m = n - len(missing)
205 if n > 0:
206 pc = 100.0 * m / n
207 else:
208 pc = 100.0
209 self.__createResultItem(
210 file, str(n), str(m), pc, readableEx, readable)
211
212 total_statements = total_statements + n
213 total_executed = total_executed + m
214 except CoverageException:
215 total_exceptions += 1
216
217 progress += 1
218 self.checkProgress.setValue(progress)
219 QApplication.processEvents()
220 finally:
221 # reenable updates of the list
222 self.resultList.setSortingEnabled(True)
223 self.resultList.setUpdatesEnabled(True)
224 self.checkProgress.reset()
225
226 # show summary info
227 if len(files) > 1:
228 if total_statements > 0:
229 pc = 100.0 * total_executed / total_statements
230 else:
231 pc = 100.0
232 itm = QTreeWidgetItem(self.summaryList, [
233 str(total_statements),
234 str(total_executed),
235 "{0:.0f}%".format(pc)
236 ])
237 for col in range(0, 3):
238 itm.setTextAlignment(col, Qt.AlignRight)
239 else:
240 self.summaryGroup.hide()
241
242 if total_exceptions:
243 E5MessageBox.warning(
244 self,
245 self.tr("Parse Error"),
246 self.tr("""%n file(s) could not be parsed. Coverage"""
247 """ info for these is not available.""", "",
248 total_exceptions))
249
250 self.__finish()
251
252 def __finish(self):
253 """
254 Private slot called when the action finished or the user pressed the
255 button.
256 """
257 self.cancelled = True
258 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
259 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
260 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
261 QApplication.processEvents()
262 self.resultList.header().resizeSections(QHeaderView.ResizeToContents)
263 self.resultList.header().setStretchLastSection(True)
264 self.summaryList.header().resizeSections(QHeaderView.ResizeToContents)
265 self.summaryList.header().setStretchLastSection(True)
266
267 def on_buttonBox_clicked(self, button):
268 """
269 Private slot called by a button of the button box clicked.
270
271 @param button button that was clicked (QAbstractButton)
272 """
273 if button == self.buttonBox.button(QDialogButtonBox.Close):
274 self.close()
275 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
276 self.__finish()
277
278 def __showContextMenu(self, coord):
279 """
280 Private slot to show the context menu of the listview.
281
282 @param coord the position of the mouse pointer (QPoint)
283 """
284 itm = self.resultList.itemAt(coord)
285 if itm:
286 self.annotate.setEnabled(True)
287 self.openAct.setEnabled(True)
288 else:
289 self.annotate.setEnabled(False)
290 self.openAct.setEnabled(False)
291 self.__menu.popup(self.mapToGlobal(coord))
292
293 def __openFile(self, itm=None):
294 """
295 Private slot to open the selected file.
296
297 @param itm reference to the item to be opened (QTreeWidgetItem)
298 """
299 if itm is None:
300 itm = self.resultList.currentItem()
301 fn = itm.text(0)
302
303 vm = e5App().getObject("ViewManager")
304 vm.openSourceFile(fn)
305 editor = vm.getOpenEditor(fn)
306 editor.codeCoverageShowAnnotations()
307
308 def __annotate(self):
309 """
310 Private slot to handle the annotate context menu action.
311
312 This method produce an annotated coverage file of the
313 selected file.
314 """
315 itm = self.resultList.currentItem()
316 fn = itm.text(0)
317
318 cover = Coverage(data_file=self.cfn)
319 cover.exclude(self.excludeList[0])
320 cover.load()
321 cover.annotate([fn], None, True)
322
323 def __annotateAll(self):
324 """
325 Private slot to handle the annotate all context menu action.
326
327 This method produce an annotated coverage file of every
328 file listed in the listview.
329 """
330 amount = self.resultList.topLevelItemCount()
331 if amount == 0:
332 return
333
334 # get list of all filenames
335 files = []
336 for index in range(amount):
337 itm = self.resultList.topLevelItem(index)
338 files.append(itm.text(0))
339
340 cover = Coverage(data_file=self.cfn)
341 cover.exclude(self.excludeList[0])
342 cover.load()
343
344 # now process them
345 progress = E5ProgressDialog(
346 self.tr("Annotating files..."), self.tr("Abort"),
347 0, len(files), self.tr("%v/%m Files"), self)
348 progress.setMinimumDuration(0)
349 progress.setWindowTitle(self.tr("Coverage"))
350 count = 0
351
352 for file in files:
353 progress.setValue(count)
354 if progress.wasCanceled():
355 break
356 cover.annotate([file], None) # , True)
357 count += 1
358
359 progress.setValue(len(files))
360
361 def __erase(self):
362 """
363 Private slot to handle the erase context menu action.
364
365 This method erases the collected coverage data that is
366 stored in the .coverage file.
367 """
368 cover = Coverage(data_file=self.cfn)
369 cover.load()
370 cover.erase()
371
372 self.reloadButton.setEnabled(False)
373 self.resultList.clear()
374 self.summaryList.clear()
375
376 def __deleteAnnotated(self):
377 """
378 Private slot to handle the delete annotated context menu action.
379
380 This method deletes all annotated files. These are files
381 ending with ',cover'.
382 """
383 files = Utilities.direntries(self.path, True, '*,cover', False)
384 for file in files:
385 try:
386 os.remove(file)
387 except EnvironmentError:
388 pass
389
390 @pyqtSlot()
391 def on_reloadButton_clicked(self):
392 """
393 Private slot to reload the coverage info.
394 """
395 self.resultList.clear()
396 self.summaryList.clear()
397 self.reload = True
398 excludePattern = self.excludeCombo.currentText()
399 if excludePattern in self.excludeList:
400 self.excludeList.remove(excludePattern)
401 self.excludeList.insert(0, excludePattern)
402 self.cancelled = False
403 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
404 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
405 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
406 self.start(self.__cfn, self.__fn)
407
408 @pyqtSlot(QTreeWidgetItem, int)
409 def on_resultList_itemActivated(self, item, column):
410 """
411 Private slot to handle the activation of an item.
412
413 @param item reference to the activated item (QTreeWidgetItem)
414 @param column column the item was activated in (integer)
415 """
416 self.__openFile(item)

eric ide

mercurial