pyEDAA.Reports.CLI.Coverage

pyEDAA/Reports/CLI/Coverage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# ==================================================================================================================== #
#              _____ ____    _        _      ____                       _                                              #
#  _ __  _   _| ____|  _ \  / \      / \    |  _ \ ___ _ __   ___  _ __| |_ ___                                        #
# | '_ \| | | |  _| | | | |/ _ \    / _ \   | |_) / _ \ '_ \ / _ \| '__| __/ __|                                       #
# | |_) | |_| | |___| |_| / ___ \  / ___ \ _|  _ <  __/ |_) | (_) | |  | |_\__ \                                       #
# | .__/ \__, |_____|____/_/   \_\/_/   \_(_)_| \_\___| .__/ \___/|_|   \__|___/                                       #
# |_|    |___/                                        |_|                                                              #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2026-2026 Electronic Design Automation Abstraction (EDA²)                                                  #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
The CLI command ``coverage``: read a code coverage report of any supported format, convert it to the common model of
:mod:`pyEDAA.Reports.CodeCoverage`, show its figures, and write it as Cobertura XML.
"""
from __future__ import annotations

from argparse import Namespace
from pathlib  import Path
from typing   import Dict, Type

from pyTooling.Attributes.ArgParse            import CommandHandler, splitFormat
from pyTooling.Attributes.ArgParse.ValuedFlag import LongValuedFlag
from pyTooling.Common                         import StringEnum
from pyTooling.Decorators                     import export
from pyTooling.MetaClasses                    import ExtendedType

from pyEDAA.Reports.CodeCoverage                                 import CodeCoverageError, CoverageSummary
from pyEDAA.Reports.CodeCoverage                                 import Document as cc_Document
from pyEDAA.Reports.CodeCoverage.AldecUCDB                       import Document as AldecUCDBDocument
from pyEDAA.Reports.CodeCoverage.Cobertura                       import Document as CoberturaDocument
from pyEDAA.Reports.CodeCoverage.Cobertura.CoveragePyCobertura   import Document as CoveragePyCoberturaDocument
from pyEDAA.Reports.CodeCoverage.Cobertura.NVCCobertura          import Document as NVCCoberturaDocument
from pyEDAA.Reports.CodeCoverage.CoveragePy                      import Document as CoveragePyDocument
from pyEDAA.Reports.CodeCoverage.Gcov                            import Document as GcovDocument
from pyEDAA.Reports.CodeCoverage.GHDL                            import Document as GHDLDocument
from pyEDAA.Reports.CodeCoverage.JaCoCo                          import Document as JaCoCoDocument
from pyEDAA.Reports.CodeCoverage.LCOV                            import Document as LCOVDocument
from pyEDAA.Reports.CodeCoverage.LLVM                            import Document as LLVMDocument
from pyEDAA.Reports.CodeCoverage.QuestaSim                       import Document as QuestaSimDocument
from pyEDAA.Reports.CodeCoverage.UCIS                            import Document as UCISDocument
from pyEDAA.Reports.CodeCoverage.UCIS.PyUCIS                     import Document as PyUCISDocument


__all__ = ["INPUT_FORMATS"]


@export
class InputFormat(StringEnum):
	"""The code coverage formats ``--input`` reads, by their name on the command line."""

	AldecUCDBXML =        "Aldec-UCDB-XML"        #: Aldec's UCDB XML export of Riviera-PRO or Active-HDL (``acdb2xml``).
	AnyCobertura =        "Any-Cobertura"         #: Cobertura XML of any tool, read leniently.
	CoveragePyCobertura = "CoveragePy-Cobertura"  #: Cobertura XML as coverage.py writes it.
	CoveragePyJSON =      "CoveragePy-JSON"       #: coverage.py's JSON report.
	GcovJSON =            "Gcov-JSON"             #: GCC's gcov JSON report.
	GHDLJSON =            "GHDL-JSON"             #: GHDL's coverage file.
	JaCoCoXML =           "JaCoCo-XML"            #: JaCoCo's XML report.
	LCOV =                "LCOV"                  #: lcov's tracefile.
	LLVMJSON =            "LLVM-JSON"             #: LLVM's JSON export (``llvm-cov export``).
	NVCCobertura =        "NVC-Cobertura"         #: Cobertura XML as NVC writes it.
	PyUCISXML =           "PyUCIS-XML"            #: UCIS XML as pyucis writes it: the elements in no namespace.
	QuestaSimXML =        "QuestaSim-XML"         #: QuestaSim's coverage report XML (``vcover report -xml -details``).
	UCISXML =             "UCIS-XML"              #: The XML interchange format of the Accellera UCIS standard.

	DEFAULT = AnyCobertura                        #: A file without format is read as Cobertura XML.


@export
class OutputFormat(StringEnum):
	"""The code coverage formats ``--output`` writes, by their name on the command line."""

	Cobertura = "Cobertura"  #: Cobertura XML, after Cobertura's DTD ``coverage-04.dtd``.

	DEFAULT = Cobertura      #: A file without format is written as Cobertura XML.


#: The document class reading each input format.
INPUT_FORMATS: Dict[InputFormat, Type[cc_Document]] = {
	InputFormat.AldecUCDBXML:        AldecUCDBDocument,
	InputFormat.AnyCobertura:        CoberturaDocument,
	InputFormat.CoveragePyCobertura: CoveragePyCoberturaDocument,
	InputFormat.CoveragePyJSON:      CoveragePyDocument,
	InputFormat.GcovJSON:            GcovDocument,
	InputFormat.GHDLJSON:            GHDLDocument,
	InputFormat.JaCoCoXML:           JaCoCoDocument,
	InputFormat.LCOV:                LCOVDocument,
	InputFormat.LLVMJSON:            LLVMDocument,
	InputFormat.NVCCobertura:        NVCCoberturaDocument,
	InputFormat.PyUCISXML:           PyUCISDocument,
	InputFormat.QuestaSimXML:        QuestaSimDocument,
	InputFormat.UCISXML:             UCISDocument
}


@export
class CoverageHandlers(metaclass=ExtendedType, mixin=True):
	"""A mixin-class adding the CLI command ``coverage``."""

	@CommandHandler(
		"coverage",
		help="Transform code coverage reports.",
		description="Read a code coverage report and convert it to Cobertura XML."
	)
	@LongValuedFlag(
		"--input", dest="input", metaName="[Format:]File", optional=True,
		help="Code coverage report to read, e.g. 'Gcov-JSON:main.gcov.json.gz'; without format: Any-Cobertura."
	)
	@LongValuedFlag(
		"--output", dest="output", metaName="[Format:]File", optional=True,
		help="Code coverage report to write, e.g. 'Cobertura:coverage.xml'; without format: Cobertura."
	)
	def HandleCoverage(self, args: Namespace) -> None:
		"""Handle program calls with command ``coverage``."""
		self._PrintHeadline()

		if args.input is None:
			self.WriteError("Option '--input=[<Format>:]<File>' is missing.")
			self.Exit(3)

		try:
			summary = self._ReadCoverage(args.input)
		except CodeCoverageError as ex:
			self.WriteFatal(str(ex), immediateExit=False)
			for note in getattr(ex, "__notes__", ()):
				self.WriteErrorNote(note)
			self.Exit(1)

		lines =    f"{summary.CoveredLines} of {summary.TotalLines}"
		branches = f"{summary.CoveredBranches} of {summary.TotalBranches}"
		self.WriteNormal(f"Lines:    {lines} covered ({summary.LineCoverage:.1%})")
		if summary.TotalBranches > 0:
			self.WriteNormal(f"Branches: {branches} covered ({summary.BranchCoverage:.1%})")

		for file in summary.IterateFiles():
			lines = f"{file.CoveredLines} of {file.TotalLines}"
			self.WriteVerbose(f"  {file.Path.as_posix()}: {lines} lines ({file.LineCoverage:.1%})")

		if args.output is not None:
			self._WriteCoverage(summary, args.output)

		self.ExitOnPreviousErrors()

	def _ReadCoverage(self, task: str) -> CoverageSummary:
		"""
		Read a code coverage report and convert it to the common model.

		:param task:               The option's value: ``[<Format>:]<File>``, e.g. ``Gcov-JSON:main.gcov.json.gz``.
		:returns:                  The report's summary in the common model.
		:raises CodeCoverageError: If the format isn't supported. |br|
		                           The exception notes the supported formats.
		:raises CodeCoverageError: If the report can't be read.
		"""
		try:
			inputFormat, file = splitFormat(task, InputFormat)
		except ValueError as ex:
			error = CodeCoverageError(f"Unsupported code coverage format for input: '{task}'.")
			error.add_note(f"Supported formats: {', '.join(InputFormat)}; without format: {InputFormat.DEFAULT}.")
			raise error from ex

		self.WriteNormal(f"Reading code coverage report '{file}' ({inputFormat}) ...")
		document = INPUT_FORMATS[inputFormat](file, analyzeAndConvert=True)
		return document.ToCoverageSummary()

	def _WriteCoverage(self, summary: CoverageSummary, task: str) -> None:
		"""
		Write the common model as a code coverage report.

		:param summary: The summary to write.
		:param task:    The option's value: ``[<Format>:]<File>``, e.g. ``Cobertura:coverage.xml``.
		"""
		try:
			_, file = splitFormat(task, OutputFormat)
		except ValueError:
			self.WriteError(f"Unsupported code coverage format for output: '{task}'.")
			formats = ", ".join(OutputFormat)
			self.WriteErrorNote(f"Supported formats: {formats}; without format: {OutputFormat.DEFAULT}.")
			return

		self.WriteNormal(f"Writing Cobertura XML report '{file}' ...")
		try:
			CoberturaDocument.FromCoverageSummary(file, summary).Write(overwrite=True, regenerate=True)
		except CodeCoverageError as ex:
			self.WriteError(str(ex))