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 pyqtSignal, 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 |
59 self.cancelled = False |
58 self.cancelled = False |
60 self.path = '.' |
59 self.path = '.' |
61 self.reload = False |
60 self.reload = False |
62 |
61 |
63 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.addAction(self.tr("JSON Report"), self.__jsonReport) |
|
67 self.__reportsMenu.addAction(self.tr("LCOV Report"), self.__lcovReport) |
64 |
68 |
65 self.__menu = QMenu(self) |
69 self.__menu = QMenu(self) |
66 self.__menu.addSeparator() |
70 self.__menu.addSeparator() |
67 self.openAct = self.__menu.addAction( |
71 self.openAct = self.__menu.addAction( |
68 self.tr("Open"), self.__openFile) |
72 self.tr("Open"), self.__openFile) |
69 self.__menu.addSeparator() |
73 self.__menu.addSeparator() |
70 self.annotate = self.__menu.addAction( |
74 self.__menu.addMenu(self.__reportsMenu) |
71 self.tr('Annotate'), self.__annotate) |
|
72 self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll) |
|
73 self.__menu.addAction( |
|
74 self.tr('Delete annotated files'), self.__deleteAnnotated) |
|
75 self.__menu.addSeparator() |
75 self.__menu.addSeparator() |
76 self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase) |
76 self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase) |
77 self.resultList.setContextMenuPolicy( |
77 self.resultList.setContextMenuPolicy( |
78 Qt.ContextMenuPolicy.CustomContextMenu) |
78 Qt.ContextMenuPolicy.CustomContextMenu) |
79 self.resultList.customContextMenuRequested.connect( |
79 self.resultList.customContextMenuRequested.connect( |
338 editor = vm.getOpenEditor(fn) |
338 editor = vm.getOpenEditor(fn) |
339 editor.codeCoverageShowAnnotations(coverageFile=self.cfn) |
339 editor.codeCoverageShowAnnotations(coverageFile=self.cfn) |
340 except KeyError: |
340 except KeyError: |
341 self.openFile.emit(fn) |
341 self.openFile.emit(fn) |
342 |
342 |
343 # TODO: Coverage.annotate is deprecated |
343 def __prepareReportGeneration(self): |
344 def __annotate(self): |
344 """ |
345 """ |
345 Private method to prepare a report generation. |
346 Private slot to handle the annotate context menu action. |
346 |
347 |
347 @return tuple containing a reference to the Coverage object and the |
348 This method produces an annotated coverage file of the |
348 list of files to report |
349 selected file. |
349 @rtype tuple of (Coverage, list of str) |
350 """ |
350 """ |
351 itm = self.resultList.currentItem() |
351 count = self.resultList.topLevelItemCount() |
352 fn = itm.text(0) |
352 if count == 0: |
|
353 return None, [] |
|
354 |
|
355 # get list of all filenames |
|
356 files = [ |
|
357 self.resultList.topLevelItem(index).text(0) |
|
358 for index in range(count) |
|
359 ] |
353 |
360 |
354 cover = Coverage(data_file=self.cfn) |
361 cover = Coverage(data_file=self.cfn) |
355 cover.exclude(self.excludeList[0]) |
362 cover.exclude(self.excludeList[0]) |
356 cover.load() |
363 cover.load() |
357 cover.annotate([fn], None, True) |
364 |
358 |
365 return cover, files |
359 # TODO: Coverage.annotate is deprecated |
366 |
360 def __annotateAll(self): |
367 @pyqtSlot() |
361 """ |
368 def __htmlReport(self): |
362 Private slot to handle the annotate all context menu action. |
369 """ |
363 |
370 Private slot to generate a HTML report of the shown data. |
364 This method produce an annotated coverage file of every |
371 """ |
365 file listed in the listview. |
372 from .PyCoverageHtmlReportDialog import PyCoverageHtmlReportDialog |
366 """ |
373 |
367 amount = self.resultList.topLevelItemCount() |
374 dlg = PyCoverageHtmlReportDialog(os.path.dirname(self.cfn), self) |
368 if amount == 0: |
375 if dlg.exec() == QDialog.DialogCode.Accepted: |
369 return |
376 title, outputDirectory, extraCSS, openReport = dlg.getData() |
370 |
377 |
371 # get list of all filenames |
378 cover, files = self.__prepareReportGeneration() |
372 files = [] |
379 cover.html_report(morfs=files, directory=outputDirectory, |
373 for index in range(amount): |
380 ignore_errors=True, extra_css=extraCSS, |
374 itm = self.resultList.topLevelItem(index) |
381 title=title) |
375 files.append(itm.text(0)) |
382 |
376 |
383 if openReport: |
377 cover = Coverage(data_file=self.cfn) |
384 QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.join( |
378 cover.exclude(self.excludeList[0]) |
385 outputDirectory, "index.html"))) |
379 cover.load() |
386 |
380 |
387 @pyqtSlot() |
381 # now process them |
388 def __jsonReport(self): |
382 progress = EricProgressDialog( |
389 """ |
383 self.tr("Annotating files..."), self.tr("Abort"), |
390 Private slot to generate a JSON report of the shown data. |
384 0, len(files), self.tr("%v/%m Files"), self) |
391 """ |
385 progress.setMinimumDuration(0) |
392 from .PyCoverageJsonReportDialog import PyCoverageJsonReportDialog |
386 progress.setWindowTitle(self.tr("Coverage")) |
393 |
387 |
394 dlg = PyCoverageJsonReportDialog(os.path.dirname(self.cfn), self) |
388 for count, file in enumerate(files): |
395 if dlg.exec() == QDialog.DialogCode.Accepted: |
389 progress.setValue(count) |
396 filename, compact = dlg.getData() |
390 if progress.wasCanceled(): |
397 cover, files = self.__prepareReportGeneration() |
391 break |
398 cover.json_report(morfs=files, outfile=filename, |
392 cover.annotate([file], None) # , True) |
399 ignore_errors=True, pretty_print=not compact) |
393 |
400 |
394 progress.setValue(len(files)) |
401 @pyqtSlot() |
|
402 def __lcovReport(self): |
|
403 """ |
|
404 Private slot to generate a LCOV report of the shown data. |
|
405 """ |
|
406 from EricWidgets import EricPathPickerDialog |
|
407 from EricWidgets.EricPathPicker import EricPathPickerModes |
|
408 |
|
409 filename, ok = EricPathPickerDialog.getPath( |
|
410 self, |
|
411 self.tr("LCOV Report"), |
|
412 self.tr("Enter the path of the output file:"), |
|
413 mode=EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE, |
|
414 path=os.path.join(os.path.dirname(self.cfn), "coverage.lcov"), |
|
415 defaultDirectory=os.path.dirname(self.cfn), |
|
416 filters=self.tr("LCOV Files (*.lcov);;All Files (*)") |
|
417 ) |
|
418 if ok: |
|
419 cover, files = self.__prepareReportGeneration() |
|
420 cover.lcov_report(morfs=files, outfile=filename, |
|
421 ignore_errors=True) |
395 |
422 |
396 def __erase(self): |
423 def __erase(self): |
397 """ |
424 """ |
398 Private slot to handle the erase context menu action. |
425 Private slot to handle the erase context menu action. |
399 |
426 |