Skip to content

Commit f1c4102

Browse files
add code coverage support for ghdl (#627)
* sim_if: add supports_coverage method * Expose supports_coverage to user * Add code coverage support for ghdl Simulation now puts the compile-time gcno with the compiled .o-files, and run-time gcda folders within the output folder for each test Each test case produces a separate .gcda file. * Hopefully make ghdl coverage work when using multiple threads * Add merge_coverage method for GHDLInterface * Update coverage example for GHDL * Adapt for usage of Pathlib * Better handling of empty glob results * Correct comment * Minor fixes after review by LukasVik * Add enable_coverage for source files * Fix linting * Add coverage example to acceptance tests * Updates after review * Add documentation for the enable_coverage flags for GHDL Co-authored-by: Lukas Vik <10241915+LukasVik@users.noreply.github.com>
1 parent bb53c03 commit f1c4102

10 files changed

Lines changed: 151 additions & 12 deletions

File tree

docs/py/opts.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ The following compilation options are known.
4646
Extra arguments passed to Active HDL ``vcom`` command.
4747
Must be a list of strings.
4848

49+
``enable_coverage``
50+
Enables compilation flags needed for code coverage and tells VUnit to handle
51+
the coverage files created at compilation. Only used for coverage with GHDL.
52+
Must be a boolean value. Default is False.
53+
4954
.. note::
5055
Only affects source files added *before* the option is set.
5156

@@ -73,17 +78,22 @@ The following simulation options are known.
7378
Must be a boolean value. Default is False.
7479

7580
When coverage is enabled VUnit only takes the minimal steps required
76-
to make the simulator creates an unique coverage file for the
77-
simulation run. The VUnit users must still set :ref:`sim
81+
to make the simulator create a unique coverage file for the
82+
simulation run.
83+
84+
For RiverieraPRO and Modelsim/Questa, the VUnit users must still set :ref:`sim
7885
<sim_options>` and :ref:`compile <compile_options>` options to
7986
configure the simulator specific coverage options they want. The
8087
reason for this to allow the VUnit users maximum control of their
8188
coverage settings.
8289

90+
For GHDL with GCC backend there is less configurability for coverage, and all
91+
necessary flags are set by the the ``enable_coverage`` sim and compile options.
92+
8393
An example of a ``run.py`` file using coverage can be found
8494
:vunit_example:`here <vhdl/coverage>`.
8595

86-
.. note: Supported by RivieraPRO and Modelsim/Questa simulators.
96+
.. note: Supported by GHDL with GCC backend, RivieraPRO and Modelsim/Questa simulators.
8797
8898
8999
``pli``

examples/vhdl/coverage/run.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@
66

77
from pathlib import Path
88
from vunit import VUnit
9+
from subprocess import call
910

1011

1112
def post_run(results):
1213
results.merge_coverage(file_name="coverage_data")
14+
if VU.get_simulator_name() == "ghdl":
15+
call(["gcovr", "coverage_data"])
1316

1417

1518
VU = VUnit.from_argv()
1619

1720
LIB = VU.add_library("lib")
1821
LIB.add_source_files(Path(__file__).parent / "*.vhd")
1922

23+
LIB.set_sim_option("enable_coverage", True)
24+
2025
LIB.set_compile_option("rivierapro.vcom_flags", ["-coverage", "bs"])
2126
LIB.set_compile_option("rivierapro.vlog_flags", ["-coverage", "bs"])
2227
LIB.set_compile_option("modelsim.vcom_flags", ["+cover=bs"])
2328
LIB.set_compile_option("modelsim.vlog_flags", ["+cover=bs"])
24-
LIB.set_sim_option("enable_coverage", True)
29+
LIB.set_compile_option("enable_coverage", True)
2530

2631
VU.main(post_run=post_run)

tests/acceptance/test_external_run_scripts.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ def test_vhdl_third_party_integration_example_project(self):
121121
def test_vhdl_check_example_project(self):
122122
self.check(ROOT / "examples" / "vhdl" / "check" / "run.py")
123123

124+
@unittest.skipIf(
125+
simulator_check(lambda simclass: not simclass.supports_coverage()),
126+
"This simulator/backend does not support coverage",
127+
)
128+
def test_vhdl_coverage_example_project(self):
129+
self.check(join(ROOT, "examples", "vhdl", "coverage", "run.py"))
130+
124131
def test_vhdl_generate_tests_example_project(self):
125132
self.check(ROOT / "examples" / "vhdl" / "generate_tests" / "run.py")
126133
check_report(

vunit/sim_if/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,14 @@ def has_valid_exit_code():
172172
@staticmethod
173173
def supports_vhpi():
174174
"""
175-
Return if the simulator supports VHPI
175+
Returns True when the simulator supports VHPI
176+
"""
177+
return False
178+
179+
@staticmethod
180+
def supports_coverage():
181+
"""
182+
Returns True when the simulator supports coverage
176183
"""
177184
return False
178185

@@ -216,7 +223,7 @@ def setup_library_mapping(self, project):
216223
Implemented by specific simulators
217224
"""
218225

219-
def __compile_source_file(self, source_file, printer):
226+
def _compile_source_file(self, source_file, printer):
220227
"""
221228
Compiles a single source file and prints status information
222229
"""
@@ -297,7 +304,7 @@ def compile_source_files(
297304
printer.write("\n")
298305
continue
299306

300-
if self.__compile_source_file(source_file, printer):
307+
if self._compile_source_file(source_file, printer):
301308
project.update(source_file)
302309
else:
303310
source_files_to_skip.update(

vunit/sim_if/activehdl.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ def supports_vhdl_package_generics(cls):
6868

6969
return False
7070

71+
@staticmethod
72+
def supports_coverage():
73+
"""
74+
Returns True when the simulator supports coverage
75+
"""
76+
return True
77+
7178
def __init__(self, prefix, output_path, gui=False):
7279
SimulatorInterface.__init__(self, output_path, gui)
7380
self._library_cfg = str(Path(output_path) / "library.cfg")

vunit/sim_if/factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def _extract_compile_options(self):
3939
"""
4040
Return all supported compile options
4141
"""
42-
result = dict()
42+
result = dict((opt.name, opt) for opt in [BooleanOption("enable_coverage")])
4343
for sim_class in self.supported_simulators():
4444
for opt in sim_class.compile_options:
4545
assert hasattr(opt, "name")

vunit/sim_if/ghdl.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import subprocess
1515
import shlex
1616
import re
17+
import shutil
1718
from json import dump
1819
from sys import stdout # To avoid output catched in non-verbose mode
1920
from warnings import warn
@@ -25,7 +26,7 @@
2526
LOGGER = logging.getLogger(__name__)
2627

2728

28-
class GHDLInterface(SimulatorInterface):
29+
class GHDLInterface(SimulatorInterface): # pylint: disable=too-many-instance-attributes
2930
"""
3031
Interface for GHDL simulator
3132
"""
@@ -108,6 +109,7 @@ def __init__( # pylint: disable=too-many-arguments
108109
self._gtkwave_args = gtkwave_args
109110
self._backend = backend
110111
self._vhdl_standard = None
112+
self._coverage_test_dirs = set()
111113

112114
def has_valid_exit_code(self):
113115
"""
@@ -164,12 +166,19 @@ def determine_version(cls, prefix):
164166
@classmethod
165167
def supports_vhpi(cls):
166168
"""
167-
Return if the simulator supports VHPI
169+
Returns True when the simulator supports VHPI
168170
"""
169171
return (cls.determine_backend(cls.find_prefix_from_path()) != "mcode") or (
170172
cls.determine_version(cls.find_prefix_from_path()) > 0.36
171173
)
172174

175+
@classmethod
176+
def supports_coverage(cls):
177+
"""
178+
Returns True when the simulator supports coverage
179+
"""
180+
return cls.determine_backend(cls.find_prefix_from_path()) == "gcc"
181+
173182
def _has_output_flag(self):
174183
"""
175184
Returns if backend supports output flag
@@ -254,10 +263,18 @@ def compile_vhdl_file_command(self, source_file):
254263
a_flags += flags
255264

256265
cmd += a_flags
266+
267+
if source_file.compile_options.get("enable_coverage", False):
268+
# Add gcc compilation flags for coverage
269+
# -ftest-coverages creates .gcno notes files needed by gcov
270+
# -fprofile-arcs creates branch profiling in .gcda database files
271+
cmd += ["-fprofile-arcs", "-ftest-coverage"]
257272
cmd += [source_file.name]
258273
return cmd
259274

260-
def _get_command(self, config, output_path, elaborate_only, ghdl_e, wave_file):
275+
def _get_command( # pylint: disable=too-many-branches
276+
self, config, output_path, elaborate_only, ghdl_e, wave_file
277+
):
261278
"""
262279
Return GHDL simulation command
263280
"""
@@ -282,6 +299,9 @@ def _get_command(self, config, output_path, elaborate_only, ghdl_e, wave_file):
282299
if self._has_output_flag():
283300
cmd += ["-o", bin_path]
284301
cmd += config.sim_options.get("ghdl.elab_flags", [])
302+
if config.sim_options.get("enable_coverage", False):
303+
# Enable coverage in linker
304+
cmd += ["-Wl,-lgcov"]
285305
cmd += [config.entity_name, config.architecture_name]
286306

287307
sim = config.sim_options.get("ghdl.sim_flags", [])
@@ -347,8 +367,16 @@ def simulate( # pylint: disable=too-many-locals
347367
)
348368

349369
status = True
370+
371+
gcov_env = environ.copy()
372+
if config.sim_options.get("enable_coverage", False):
373+
# Set environment variable to put the coverage output in the test_output folder
374+
coverage_dir = str(Path(output_path) / "coverage")
375+
gcov_env["GCOV_PREFIX"] = coverage_dir
376+
self._coverage_test_dirs.add(coverage_dir)
377+
350378
try:
351-
proc = Process(cmd)
379+
proc = Process(cmd, env=gcov_env)
352380
proc.consume_output()
353381
except Process.NonZeroExitCode:
354382
status = False
@@ -364,3 +392,54 @@ def simulate( # pylint: disable=too-many-locals
364392
subprocess.call(cmd)
365393

366394
return status
395+
396+
def _compile_source_file(self, source_file, printer):
397+
"""
398+
Runs parent command for compilation, and moves any .gcno files to the compilation output
399+
"""
400+
compilation_ok = super()._compile_source_file(source_file, printer)
401+
402+
if source_file.compile_options.get("enable_coverage", False):
403+
# GCOV gcno files are output to where the command is run,
404+
# move it back to the compilation folder
405+
source_path = Path(source_file.name)
406+
gcno_file = Path(source_path.stem + ".gcno")
407+
if Path(gcno_file).exists():
408+
new_path = Path(source_file.library.directory) / gcno_file
409+
gcno_file.rename(new_path)
410+
411+
return compilation_ok
412+
413+
def merge_coverage(self, file_name, args=None):
414+
"""
415+
Merge coverage from all test cases
416+
"""
417+
output_dir = file_name
418+
419+
# Loop over each .gcda output folder and merge them two at a time
420+
first_input = True
421+
for coverage_dir in self._coverage_test_dirs:
422+
if Path(coverage_dir).exists():
423+
merge_command = [
424+
"gcov-tool",
425+
"merge",
426+
"-o",
427+
output_dir,
428+
coverage_dir if first_input else output_dir,
429+
coverage_dir,
430+
]
431+
subprocess.call(merge_command)
432+
first_input = False
433+
else:
434+
LOGGER.warning("Missing coverage directory: %s", coverage_dir)
435+
436+
# Find actual output path of the .gcda files (they are deep in hierarchy)
437+
dir_path = Path(output_dir)
438+
gcda_dirs = {x.parent for x in dir_path.glob("**/*.gcda")}
439+
assert len(gcda_dirs) == 1, "Expected exactly one folder with gcda files"
440+
gcda_dir = gcda_dirs.pop()
441+
442+
# Add compile-time .gcno files as well, they are needed for the report
443+
for library in self._project.get_libraries():
444+
for gcno_file in Path(library.directory).glob("*.gcno"):
445+
shutil.copy(gcno_file, gcda_dir)

vunit/sim_if/modelsim.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ def supports_vhdl_package_generics(cls):
8181
"""
8282
return True
8383

84+
@staticmethod
85+
def supports_coverage():
86+
"""
87+
Returns True when the simulator supports coverage
88+
"""
89+
return True
90+
8491
def __init__(self, prefix, output_path, persistent=False, gui=False):
8592
SimulatorInterface.__init__(self, output_path, gui)
8693
VsimSimulatorMixin.__init__(

vunit/sim_if/rivierapro.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ def supports_vhdl_package_generics(cls):
9595
"""
9696
return True
9797

98+
@staticmethod
99+
def supports_coverage():
100+
"""
101+
Returns True when the simulator supports coverage
102+
"""
103+
return True
104+
98105
def __init__(self, prefix, output_path, persistent=False, gui=False):
99106
SimulatorInterface.__init__(self, output_path, gui)
100107
VsimSimulatorMixin.__init__(

vunit/ui/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,3 +1099,13 @@ def get_simulator_name(self):
10991099
if self._simulator_class is None:
11001100
return None
11011101
return self._simulator_class.name
1102+
1103+
def simulator_supports_coverage(self):
1104+
"""
1105+
Returns True when the simulator supports coverage
1106+
1107+
Will return None if no simulator was found.
1108+
"""
1109+
if self._simulator_class is None:
1110+
return None
1111+
return self._simulator_class.supports_coverage

0 commit comments

Comments
 (0)