--- 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)