Skip to content

Commit 29db18f

Browse files
authored
Merge pull request #2209 from strictdoc-project/stanislaw/gcov
Feature: Reading Gcov reports natively as SDoc documents
2 parents a151acc + 3e42fc6 commit 29db18f

File tree

9 files changed

+308
-2
lines changed

9 files changed

+308
-2
lines changed

requirements.check.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pyfakefs>=4.5.5
1212
coverage>=5.4
1313
# httpx is needed for running server-related unit tests.
1414
httpx
15+
# This is needed for converting Gcov coverage files to a human-readable JSON format.
16+
gcovr
1517

1618
# Integration tests
1719
lit>=0.11.0.post1
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import hashlib
2+
import json
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
6+
from strictdoc.backend.sdoc.document_reference import DocumentReference
7+
from strictdoc.backend.sdoc.models.document import SDocDocument
8+
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
9+
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
10+
from strictdoc.backend.sdoc.models.reference import FileReference
11+
from strictdoc.backend.sdoc.models.section import SDocSection
12+
from strictdoc.backend.sdoc.models.type_system import FileEntry, FileEntryFormat
13+
from strictdoc.core.file_tree import File
14+
from strictdoc.core.project_config import ProjectConfig
15+
16+
17+
@dataclass
18+
class GCovStatsObject:
19+
total_number_of_covered_functions: int = 0
20+
total_number_of_non_covered_functions: int = 0
21+
22+
def add_function(self, covered: bool) -> None:
23+
if covered:
24+
self.total_number_of_covered_functions += 1
25+
else:
26+
self.total_number_of_non_covered_functions += 1
27+
28+
29+
class GCovJSONReader:
30+
@classmethod
31+
def read_from_file(
32+
cls: "GCovJSONReader", doc_file: File, project_config: ProjectConfig
33+
) -> SDocDocument:
34+
with open(doc_file.get_full_path(), encoding="UTF-8") as file:
35+
content = file.read()
36+
return cls.read_from_string(content, doc_file, project_config)
37+
38+
@classmethod
39+
def read_from_string(
40+
cls: "GCovJSONReader",
41+
content: str,
42+
doc_file: File, # noqa: ARG003
43+
project_config: ProjectConfig, # noqa: ARG003
44+
) -> SDocDocument:
45+
if len(content) == 0:
46+
raise RuntimeError("Document is empty")
47+
try:
48+
json_content = json.loads(content)
49+
except Exception as exception: # pylint: disable=broad-except
50+
raise RuntimeError(str(exception)) from None
51+
52+
document = SDocDocument(
53+
mid=None,
54+
title="Code coverage report",
55+
config=None,
56+
view=None,
57+
grammar=None,
58+
section_contents=[],
59+
)
60+
document.ng_including_document_reference = DocumentReference()
61+
grammar = DocumentGrammar.create_for_test_report(document)
62+
document.grammar = grammar
63+
document.config.requirement_style = "Table"
64+
65+
stats = GCovStatsObject()
66+
67+
"""
68+
Parse individual <testcase> elements.
69+
"""
70+
71+
json_files = json_content["files"]
72+
for json_file_ in json_files:
73+
json_file_name = json_file_["file"]
74+
75+
file_section = SDocSection(
76+
parent=document,
77+
mid=None,
78+
uid=None,
79+
custom_level=None,
80+
title=json_file_name,
81+
requirement_prefix=None,
82+
section_contents=[],
83+
)
84+
file_section.ng_including_document_reference = DocumentReference()
85+
file_section.ng_document_reference = DocumentReference()
86+
file_section.ng_document_reference.set_document(document)
87+
document.section_contents.append(file_section)
88+
89+
covered_functions_section = SDocSection(
90+
parent=document,
91+
mid=None,
92+
uid=None,
93+
custom_level=None,
94+
title="Covered functions",
95+
requirement_prefix=None,
96+
section_contents=[],
97+
)
98+
covered_functions_section.ng_including_document_reference = (
99+
DocumentReference()
100+
)
101+
covered_functions_section.ng_document_reference = (
102+
DocumentReference()
103+
)
104+
covered_functions_section.ng_document_reference.set_document(
105+
document
106+
)
107+
file_section.section_contents.append(covered_functions_section)
108+
109+
non_covered_functions_section = SDocSection(
110+
parent=document,
111+
mid=None,
112+
uid=None,
113+
custom_level=None,
114+
title="Non-covered functions",
115+
requirement_prefix=None,
116+
section_contents=[],
117+
)
118+
non_covered_functions_section.ng_including_document_reference = (
119+
DocumentReference()
120+
)
121+
non_covered_functions_section.ng_document_reference = (
122+
DocumentReference()
123+
)
124+
non_covered_functions_section.ng_document_reference.set_document(
125+
document
126+
)
127+
file_section.section_contents.append(non_covered_functions_section)
128+
129+
for json_function_ in json_file_["functions"]:
130+
json_function_name = json_function_["demangled_name"]
131+
is_function_covered = json_function_["execution_count"] > 0
132+
stats.add_function(is_function_covered)
133+
134+
testcase_node = SDocNode(
135+
parent=document,
136+
node_type="TEST_RESULT",
137+
fields=[],
138+
relations=[],
139+
)
140+
testcase_node.ng_document_reference = DocumentReference()
141+
testcase_node.ng_document_reference.set_document(document)
142+
testcase_node.ng_including_document_reference = (
143+
DocumentReference()
144+
)
145+
testcase_node.set_field_value(
146+
field_name="UID",
147+
form_field_index=0,
148+
value=SDocNodeField(
149+
parent=testcase_node,
150+
field_name="UID",
151+
parts=[
152+
"GCOV:" + json_file_name + ":" + json_function_name
153+
],
154+
multiline__=None,
155+
),
156+
)
157+
158+
path = "src/main.c"
159+
gcov_path_hash = hashlib.md5(path.encode("utf-8")).hexdigest()
160+
# FIXME: Resolve the relative path from a project config.
161+
link = (
162+
"../../../../" # noqa: ISC003
163+
+ "coverage."
164+
+ Path(json_file_name).name
165+
+ ". "
166+
+ gcov_path_hash
167+
+ ".html"
168+
)
169+
testcase_node.set_field_value(
170+
field_name="STATEMENT",
171+
form_field_index=0,
172+
value=SDocNodeField(
173+
parent=testcase_node,
174+
field_name="STATEMENT",
175+
parts=[
176+
f"""
177+
`LINK <{link}>`_
178+
"""
179+
],
180+
multiline__=None,
181+
),
182+
)
183+
testcase_node.set_field_value(
184+
field_name="TITLE",
185+
form_field_index=0,
186+
value=SDocNodeField(
187+
parent=testcase_node,
188+
field_name="TITLE",
189+
parts=[json_function_name],
190+
multiline__=None,
191+
),
192+
)
193+
testcase_node.relations.append(
194+
FileReference(
195+
parent=testcase_node,
196+
g_file_entry=FileEntry(
197+
parent=None,
198+
g_file_format=FileEntryFormat.SOURCECODE,
199+
g_file_path=json_file_name,
200+
g_line_range=None,
201+
function=json_function_name,
202+
clazz=None,
203+
),
204+
)
205+
)
206+
if is_function_covered:
207+
covered_functions_section.section_contents.append(
208+
testcase_node
209+
)
210+
else:
211+
non_covered_functions_section.section_contents.append(
212+
testcase_node
213+
)
214+
215+
summary_table = f"""\
216+
.. list-table:: Coverage summary
217+
:widths: 25 10
218+
:header-rows: 0
219+
220+
* - **Number of files:**
221+
- {len(json_content["files"])}
222+
* - **Total covered functions:**
223+
- {stats.total_number_of_covered_functions}
224+
* - **Total non-covered functions:**
225+
- {stats.total_number_of_non_covered_functions}
226+
"""
227+
228+
testcase_node = SDocNode(
229+
parent=document,
230+
node_type="TEXT",
231+
fields=[],
232+
relations=[],
233+
)
234+
testcase_node.ng_document_reference = DocumentReference()
235+
testcase_node.ng_document_reference.set_document(document)
236+
testcase_node.ng_including_document_reference = DocumentReference()
237+
testcase_node.set_field_value(
238+
field_name="STATEMENT",
239+
form_field_index=0,
240+
value=SDocNodeField(
241+
parent=testcase_node,
242+
field_name="STATEMENT",
243+
parts=[summary_table],
244+
multiline__="True",
245+
),
246+
)
247+
document.section_contents.insert(0, testcase_node)
248+
249+
return document

strictdoc/core/document_finder.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from strictdoc.backend.sdoc.models.document import SDocDocument
88
from strictdoc.backend.sdoc.models.document_grammar import DocumentGrammar
99
from strictdoc.backend.sdoc.reader import SDReader
10+
from strictdoc.backend.sdoc_source_code.coverage_reports.gcov import (
11+
GCovJSONReader,
12+
)
1013
from strictdoc.backend.sdoc_source_code.test_reports.junit_xml_reader import (
1114
JUnitXMLReader,
1215
)
@@ -86,6 +89,12 @@ def _process_worker_parse_document(
8689
doc_file, project_config
8790
)
8891
assert isinstance(document_or_grammar, SDocDocument)
92+
elif doc_full_path.endswith(".gcov.json"):
93+
gcov_json_reader = GCovJSONReader()
94+
document_or_grammar = gcov_json_reader.read_from_file(
95+
doc_file, project_config
96+
)
97+
assert isinstance(document_or_grammar, SDocDocument)
8998
else:
9099
raise NotImplementedError # pragma: no cover
91100
drop_textx_meta(document_or_grammar)
@@ -260,7 +269,7 @@ def _build_file_tree(
260269
file_tree_structure = FileFinder.find_files_with_extensions(
261270
root_path=path_to_doc_root,
262271
ignored_dirs=[project_config.output_dir],
263-
extensions=[".sdoc", ".sgra", ".junit.xml"],
272+
extensions=[".sdoc", ".sgra", ".junit.xml", ".gcov.json"],
264273
include_paths=project_config.include_doc_paths,
265274
exclude_paths=project_config.exclude_doc_paths,
266275
)

strictdoc/export/html/generators/view_objects/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def screen_should_display_file(
5757
assert isinstance(file, File), file
5858
assert traceability_index.document_tree is not None
5959

60-
if file.has_extension(".junit.xml"):
60+
if file.has_extension(".junit.xml") or file.has_extension(".gcov.json"):
6161
return True
6262

6363
if file.has_extension(".sdoc"):

tasks.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,17 @@ def coverage_combine(context):
485485
--data-file build/coverage/.coverage.combined
486486
""",
487487
)
488+
run_invoke_with_tox(
489+
context,
490+
ToxEnvironment.CHECK,
491+
"""
492+
coverage json
493+
--rcfile .coveragerc.combined
494+
--data-file build/coverage/.coverage.combined
495+
--pretty-print
496+
--output build/coverage/coverage.combined.json
497+
""",
498+
)
488499

489500

490501
@task
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!reports/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include <stdio.h>
2+
3+
void function_covered(void) {
4+
printf("This function is covered\n");
5+
}
6+
7+
void function_not_covered(void) {
8+
printf("This function is not covered\n");
9+
}
10+
11+
int main(void) {
12+
function_covered();
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
3+
features = [
4+
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
5+
"SOURCE_FILE_LANGUAGE_PARSERS",
6+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
REQUIRES: PYTHON_39_OR_HIGHER
2+
3+
RUN: cd %S/Output && gcc --coverage -g -O0 -o main ../src/main.c
4+
5+
RUN: cd %S/Output && ./main | filecheck %s --check-prefix CHECK-C
6+
CHECK-C: This function is covered
7+
8+
RUN: cd %S/Output && gcovr --object-directory ./ --root ../ --html --html-details -o coverage.html
9+
RUN: cd %S/Output && gcovr --object-directory ./ --root ../ --json --json-pretty -o coverage.gcov.json
10+
RUN: cd %S/Output && mkdir docs/ && cp coverage.gcov.json docs/
11+
RUN: cp %S/strictdoc.toml %S/Output/docs/
12+
RUN: cp -r %S/src %S/Output/docs/
13+
RUN: cd %S/Output/docs && %strictdoc export . | filecheck %s
14+
15+
CHECK: Published: Code coverage report

0 commit comments

Comments
 (0)