eric7/DataViews/PyCoverageDialog.py

branch
eric7-maintenance
changeset 9111
4ac66b6c33a4
parent 8943
23f9c7b9e18e
child 9192
a763d57e23bc
--- a/eric7/DataViews/PyCoverageDialog.py	Mon May 02 15:53:05 2022 +0200
+++ b/eric7/DataViews/PyCoverageDialog.py	Wed Jun 01 13:48:49 2022 +0200
@@ -7,11 +7,11 @@
 Module implementing a Python code coverage dialog.
 """
 
-import contextlib
 import os
 import time
 
-from PyQt6.QtCore import pyqtSlot, Qt
+from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
+from PyQt6.QtGui import QDesktopServices
 from PyQt6.QtWidgets import (
     QDialog, QDialogButtonBox, QMenu, QHeaderView, QTreeWidgetItem,
     QApplication
@@ -19,7 +19,6 @@
 
 from EricWidgets import EricMessageBox
 from EricWidgets.EricApplication import ericApp
-from EricWidgets.EricProgressDialog import EricProgressDialog
 
 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
 
@@ -31,12 +30,17 @@
 class PyCoverageDialog(QDialog, Ui_PyCoverageDialog):
     """
     Class implementing a dialog to display the collected code coverage data.
+    
+    @signal openFile(str) emitted to open the given file in an editor
     """
+    openFile = pyqtSignal(str)
+    
     def __init__(self, parent=None):
         """
         Constructor
         
-        @param parent parent widget (QWidget)
+        @param parent parent widget
+        @type QWidget
         """
         super().__init__(parent)
         self.setupUi(self)
@@ -57,16 +61,18 @@
         
         self.excludeList = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]']
         
+        self.__reportsMenu = QMenu(self.tr("Create Report"), self)
+        self.__reportsMenu.addAction(self.tr("HTML Report"), self.__htmlReport)
+        self.__reportsMenu.addSeparator()
+        self.__reportsMenu.addAction(self.tr("JSON Report"), self.__jsonReport)
+        self.__reportsMenu.addAction(self.tr("LCOV Report"), self.__lcovReport)
+        
         self.__menu = QMenu(self)
         self.__menu.addSeparator()
         self.openAct = self.__menu.addAction(
             self.tr("Open"), self.__openFile)
         self.__menu.addSeparator()
-        self.annotate = self.__menu.addAction(
-            self.tr('Annotate'), self.__annotate)
-        self.__menu.addAction(self.tr('Annotate all'), self.__annotateAll)
-        self.__menu.addAction(
-            self.tr('Delete annotated files'), self.__deleteAnnotated)
+        self.__menu.addMenu(self.__reportsMenu)
         self.__menu.addSeparator()
         self.__menu.addAction(self.tr('Erase Coverage Info'), self.__erase)
         self.resultList.setContextMenuPolicy(
@@ -80,7 +86,9 @@
         groups.
         
         @param lines list of integers
+        @type list of int
         @return string representing the list
+        @rtype str
         """
         pairs = []
         lines.sort()
@@ -110,6 +118,9 @@
             pair.
             
             @param pair pair of integers
+            @type tuple of (int, int
+            @return representation of the pair
+            @rtype str
             """
             start, end = pair
             if start == end:
@@ -124,12 +135,18 @@
         """
         Private method to create an entry in the result list.
         
-        @param file filename of file (string)
-        @param statements amount of statements (integer)
-        @param executed amount of executed statements (integer)
-        @param coverage percent of coverage (integer)
-        @param excluded list of excluded lines (string)
-        @param missing list of lines without coverage (string)
+        @param file filename of file
+        @type str
+        @param statements number of statements
+        @type int
+        @param executed number of executed statements
+        @type int
+        @param coverage percent of coverage
+        @type int
+        @param excluded list of excluded lines
+        @type str
+        @param missing list of lines without coverage
+        @type str
         """
         itm = QTreeWidgetItem(self.resultList, [
             file,
@@ -146,21 +163,35 @@
             font.setBold(True)
             for col in range(itm.columnCount()):
                 itm.setFont(col, font)
-        
+    
     def start(self, cfn, fn):
         """
         Public slot to start the coverage data evaluation.
         
-        @param cfn basename of the coverage file (string)
+        @param cfn basename of the coverage file
+        @type str
         @param fn file or list of files or directory to be checked
-                (string or list of strings)
+        @type str or list of str
         """
+        # initialize the dialog
+        self.resultList.clear()
+        self.summaryList.clear()
+        self.cancelled = False
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Close).setEnabled(False)
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
+        self.buttonBox.button(
+            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
+        
         self.__cfn = cfn
         self.__fn = fn
         
-        self.basename = os.path.splitext(cfn)[0]
-        
-        self.cfn = "{0}.coverage".format(self.basename)
+        self.cfn = (
+            cfn
+            if cfn.endswith(".coverage") else
+            "{0}.coverage".format(os.path.splitext(cfn)[0])
+        )
         
         if isinstance(fn, list):
             files = fn
@@ -251,7 +282,7 @@
                         total_exceptions))
         
         self.__finish()
-        
+    
     def __finish(self):
         """
         Private slot called when the action finished or the user pressed the
@@ -271,12 +302,13 @@
         self.summaryList.header().resizeSections(
             QHeaderView.ResizeMode.ResizeToContents)
         self.summaryList.header().setStretchLastSection(True)
-        
+    
     def on_buttonBox_clicked(self, button):
         """
         Private slot called by a button of the button box clicked.
         
-        @param button button that was clicked (QAbstractButton)
+        @param button button that was clicked
+        @type QAbstractButton
         """
         if button == self.buttonBox.button(
             QDialogButtonBox.StandardButton.Close
@@ -286,88 +318,122 @@
             QDialogButtonBox.StandardButton.Cancel
         ):
             self.__finish()
-        
+    
     def __showContextMenu(self, coord):
         """
         Private slot to show the context menu of the listview.
         
-        @param coord the position of the mouse pointer (QPoint)
+        @param coord position of the mouse pointer
+        @type QPoint
         """
         itm = self.resultList.itemAt(coord)
         if itm:
-            self.annotate.setEnabled(True)
             self.openAct.setEnabled(True)
         else:
-            self.annotate.setEnabled(False)
             self.openAct.setEnabled(False)
+        self.__reportsMenu.setEnabled(
+            bool(self.resultList.topLevelItemCount()))
         self.__menu.popup(self.mapToGlobal(coord))
-        
+    
     def __openFile(self, itm=None):
         """
         Private slot to open the selected file.
         
-        @param itm reference to the item to be opened (QTreeWidgetItem)
+        @param itm reference to the item to be opened
+        @type QTreeWidgetItem
         """
         if itm is None:
             itm = self.resultList.currentItem()
         fn = itm.text(0)
         
-        vm = ericApp().getObject("ViewManager")
-        vm.openSourceFile(fn)
-        editor = vm.getOpenEditor(fn)
-        editor.codeCoverageShowAnnotations()
-        
-    def __annotate(self):
+        try:
+            vm = ericApp().getObject("ViewManager")
+            vm.openSourceFile(fn)
+            editor = vm.getOpenEditor(fn)
+            editor.codeCoverageShowAnnotations(coverageFile=self.cfn)
+        except KeyError:
+            self.openFile.emit(fn)
+    
+    def __prepareReportGeneration(self):
         """
-        Private slot to handle the annotate context menu action.
-        
-        This method produce an annotated coverage file of the
-        selected file.
-        """
-        itm = self.resultList.currentItem()
-        fn = itm.text(0)
+        Private method to prepare a report generation.
         
-        cover = Coverage(data_file=self.cfn)
-        cover.exclude(self.excludeList[0])
-        cover.load()
-        cover.annotate([fn], None, True)
-        
-    def __annotateAll(self):
+        @return tuple containing a reference to the Coverage object and the
+            list of files to report
+        @rtype tuple of (Coverage, list of str)
         """
-        Private slot to handle the annotate all context menu action.
-        
-        This method produce an annotated coverage file of every
-        file listed in the listview.
-        """
-        amount = self.resultList.topLevelItemCount()
-        if amount == 0:
-            return
+        count = self.resultList.topLevelItemCount()
+        if count == 0:
+            return None, []
         
         # get list of all filenames
-        files = []
-        for index in range(amount):
-            itm = self.resultList.topLevelItem(index)
-            files.append(itm.text(0))
+        files = [
+            self.resultList.topLevelItem(index).text(0)
+            for index in range(count)
+        ]
         
         cover = Coverage(data_file=self.cfn)
         cover.exclude(self.excludeList[0])
         cover.load()
         
-        # now process them
-        progress = EricProgressDialog(
-            self.tr("Annotating files..."), self.tr("Abort"),
-            0, len(files), self.tr("%v/%m Files"), self)
-        progress.setMinimumDuration(0)
-        progress.setWindowTitle(self.tr("Coverage"))
+        return cover, files
+    
+    @pyqtSlot()
+    def __htmlReport(self):
+        """
+        Private slot to generate a HTML report of the shown data.
+        """
+        from .PyCoverageHtmlReportDialog import PyCoverageHtmlReportDialog
+        
+        dlg = PyCoverageHtmlReportDialog(os.path.dirname(self.cfn), self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            title, outputDirectory, extraCSS, openReport = dlg.getData()
+            
+            cover, files = self.__prepareReportGeneration()
+            cover.html_report(morfs=files, directory=outputDirectory,
+                              ignore_errors=True, extra_css=extraCSS,
+                              title=title)
+            
+            if openReport:
+                QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.join(
+                    outputDirectory, "index.html")))
+    
+    @pyqtSlot()
+    def __jsonReport(self):
+        """
+        Private slot to generate a JSON report of the shown data.
+        """
+        from .PyCoverageJsonReportDialog import PyCoverageJsonReportDialog
         
-        for count, file in enumerate(files):
-            progress.setValue(count)
-            if progress.wasCanceled():
-                break
-            cover.annotate([file], None)  # , True)
+        dlg = PyCoverageJsonReportDialog(os.path.dirname(self.cfn), self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            filename, compact = dlg.getData()
+            cover, files = self.__prepareReportGeneration()
+            cover.json_report(morfs=files, outfile=filename,
+                              ignore_errors=True, pretty_print=not compact)
+    
+    @pyqtSlot()
+    def __lcovReport(self):
+        """
+        Private slot to generate a LCOV report of the shown data.
+        """
+        from EricWidgets import EricPathPickerDialog
+        from EricWidgets.EricPathPicker import EricPathPickerModes
         
-        progress.setValue(len(files))
-        
+        filename, ok = EricPathPickerDialog.getPath(
+            self,
+            self.tr("LCOV Report"),
+            self.tr("Enter the path of the output file:"),
+            mode=EricPathPickerModes.SAVE_FILE_ENSURE_EXTENSION_MODE,
+            path=os.path.join(os.path.dirname(self.cfn), "coverage.lcov"),
+            defaultDirectory=os.path.dirname(self.cfn),
+            filters=self.tr("LCOV Files (*.lcov);;All Files (*)")
+        )
+        if ok:
+            cover, files = self.__prepareReportGeneration()
+            cover.lcov_report(morfs=files, outfile=filename,
+                              ignore_errors=True)
+    
     def __erase(self):
         """
         Private slot to handle the erase context menu action.
@@ -382,38 +448,17 @@
         self.reloadButton.setEnabled(False)
         self.resultList.clear()
         self.summaryList.clear()
-        
-    def __deleteAnnotated(self):
-        """
-        Private slot to handle the delete annotated context menu action.
-        
-        This method deletes all annotated files. These are files
-        ending with ',cover'.
-        """
-        files = Utilities.direntries(self.path, True, '*,cover', False)
-        for file in files:
-            with contextlib.suppress(OSError):
-                os.remove(file)
-
+    
     @pyqtSlot()
     def on_reloadButton_clicked(self):
         """
         Private slot to reload the coverage info.
         """
-        self.resultList.clear()
-        self.summaryList.clear()
         self.reload = True
         excludePattern = self.excludeCombo.currentText()
         if excludePattern in self.excludeList:
             self.excludeList.remove(excludePattern)
         self.excludeList.insert(0, excludePattern)
-        self.cancelled = False
-        self.buttonBox.button(
-            QDialogButtonBox.StandardButton.Close).setEnabled(False)
-        self.buttonBox.button(
-            QDialogButtonBox.StandardButton.Cancel).setEnabled(True)
-        self.buttonBox.button(
-            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
         self.start(self.__cfn, self.__fn)
     
     @pyqtSlot(QTreeWidgetItem, int)

eric ide

mercurial