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
|
# ==================================================================================================================== #
# _ _ _ #
# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ #
# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| #
# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ #
# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ #
# |_| |_| #
# ==================================================================================================================== #
# Authors: #
# Patrick Lehmann #
# #
# License: #
# ==================================================================================================================== #
# Copyright 2023-2024 Patrick Lehmann - Bötzingen, Germany #
# #
# 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 #
# ==================================================================================================================== #
#
"""
**A Sphinx extension providing code coverage details embedded in documentation pages.**
"""
from pathlib import Path
from pyTooling.Configuration.JSON import Configuration
from pyTooling.Decorators import export, readonly
from sphinx_reports.Common import ReportExtensionError
from sphinx_reports.DataModel.CodeCoverage import PackageCoverage, ModuleCoverage, AggregatedCoverage
@export
class CodeCoverageError(ReportExtensionError):
pass
@export
class Analyzer:
"""
An analyzer to read and transform code coverage data from JSON format to a generic data model.
Coverage.py can provide collected statement and branch coverage metrics as JSON data, which can be converted to a
generic code coverage model.
"""
_packageName: str
_coverageReport: Configuration
def __init__(self, packageName: str, jsonCoverageFile: Path) -> None:
"""
Read a JSON file containing code coverage metrics generated by Coverage.py.
:param packageName: Name of the Python package that was analyzed.
:param jsonCoverageFile: JSON file containing statement and/or branch coverage.
:raises CodeCoverageError: If JSON file doesn't exist.
"""
if not jsonCoverageFile.exists():
raise CodeCoverageError(f"JSON coverage report '{jsonCoverageFile}' not found.") from FileNotFoundError(jsonCoverageFile)
self._packageName = packageName
self._coverageReport = Configuration(jsonCoverageFile)
# if int(self._coverageReport["format"]) != 2:
# raise CodeCoverageError(f"File format of '{jsonCoverageFile}' is not supported.")
@readonly
def PackageName(self) -> str:
"""
Read-only property to access the analyzed package's name.
:return: Name of the analyzed package.
"""
return self._packageName
@readonly
def JSONCoverageFile(self) -> Path:
"""
Read-only property to access the parsed JSON file.
:return: Path to the parsed JSON file.
"""
return self._coverageReport.ConfigFile
@readonly
def CoverageReport(self) -> Configuration:
return self._coverageReport
def Convert(self) -> PackageCoverage:
rootPackageCoverage = PackageCoverage(self._packageName, Path("__init__.py"))
for statusRecord in self._coverageReport["files"]:
moduleFile = Path(statusRecord.Key)
coverageSummary = statusRecord["summary"]
moduleName = moduleFile.stem
modulePath = moduleFile.parent.parts[1:]
currentCoverageObject: AggregatedCoverage = rootPackageCoverage
for packageName in modulePath:
try:
currentCoverageObject = currentCoverageObject[packageName]
except KeyError:
currentCoverageObject = PackageCoverage(packageName, moduleFile, currentCoverageObject)
if moduleName != "__init__":
currentCoverageObject = ModuleCoverage(moduleName, moduleFile, currentCoverageObject)
currentCoverageObject._totalStatements = int(coverageSummary["num_statements"])
currentCoverageObject._excludedStatements = int(coverageSummary["excluded_lines"])
currentCoverageObject._coveredStatements = int(coverageSummary["covered_lines"])
currentCoverageObject._missingStatements = int(coverageSummary["missing_lines"])
currentCoverageObject._totalBranches = int(coverageSummary["num_branches"])
currentCoverageObject._coveredBranches = int(coverageSummary["covered_branches"])
currentCoverageObject._partialBranches = int(coverageSummary["num_partial_branches"])
currentCoverageObject._missingBranches = int(coverageSummary["missing_branches"])
currentCoverageObject._coverage = float(coverageSummary["percent_covered"]) / 100.0
return rootPackageCoverage
|