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