Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add code coverage support for ghdl #627

Merged
merged 15 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion examples/vhdl/coverage/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,29 @@

from pathlib import Path
from vunit import VUnit
from subprocess import call


def post_run(results):
results.merge_coverage(file_name="coverage_data")
if VU.get_simulator_name() == "ghdl":
call(["gcovr", "coverage_data"])


VU = VUnit.from_argv()

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

LIB.set_sim_option("enable_coverage", True)

LIB.set_compile_option("rivierapro.vcom_flags", ["-coverage", "bs"])
LIB.set_compile_option("rivierapro.vlog_flags", ["-coverage", "bs"])
LIB.set_compile_option("modelsim.vcom_flags", ["+cover=bs"])
LIB.set_compile_option("modelsim.vlog_flags", ["+cover=bs"])
LIB.set_sim_option("enable_coverage", True)
LIB.set_compile_option(
"ghdl.a_flags", ["-g", "-O2", "-fprofile-arcs", "-ftest-coverage"]
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
)
LIB.set_sim_option("ghdl.elab_flags", ["-Wl,-lgcov"])
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To follow the convention in e.g. modelsim.py:258 this option should be moved to the ghdl interface class and be enabled based on the enable_coverage sim option.

Maybe ghdl.a_flags should be handled the same way. For riviera and modelsim the user has to manually set compile option since they have to make a decision on what coverage to enable. For ghdl that choice is not there, so ghdl.a_flags could be set in the class solely based on enable_coverage sim option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my original idea, but I opted not to do it as it seems like the enable_coverage sim option should not affect compilation.

But it would be more convenient, I'll change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sim option -Wl,-lgcov, has now been moved into ghdl.py

The compilation function only works on the SourceFile object, which is neat but means that the enable_coverage simulation option is not conveniently available. I've added a new enable_coverage compilation option. This also means that coverage may be enabled only for the desired files. Proof of concept on my tsfpga branch


VU.main(post_run=post_run)
13 changes: 10 additions & 3 deletions vunit/sim_if/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,14 @@ def has_valid_exit_code():
@staticmethod
def supports_vhpi():
"""
Return if the simulator supports VHPI
Returns True when the simulator supports VHPI
"""
return False

@staticmethod
def supports_coverage():
"""
Returns True when the simulator supports coverage
"""
return False

Expand Down Expand Up @@ -216,7 +223,7 @@ def setup_library_mapping(self, project):
Implemented by specific simulators
"""

def __compile_source_file(self, source_file, printer):
def _compile_source_file(self, source_file, printer):
"""
Compiles a single source file and prints status information
"""
Expand Down Expand Up @@ -297,7 +304,7 @@ def compile_source_files(
printer.write("\n")
continue

if self.__compile_source_file(source_file, printer):
if self._compile_source_file(source_file, printer):
project.update(source_file)
else:
source_files_to_skip.update(
Expand Down
7 changes: 7 additions & 0 deletions vunit/sim_if/activehdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ def supports_vhdl_package_generics(cls):

return False

@staticmethod
def supports_coverage():
"""
Returns True when the simulator supports coverage
"""
return True

def __init__(self, prefix, output_path, gui=False):
SimulatorInterface.__init__(self, output_path, gui)
self._library_cfg = str(Path(output_path) / "library.cfg")
Expand Down
89 changes: 85 additions & 4 deletions vunit/sim_if/ghdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@
import subprocess
import shlex
import re
import shutil
from json import dump
from sys import stdout # To avoid output catched in non-verbose mode
from warnings import warn
from ..exceptions import CompileError
from ..ostools import Process
from ..ostools import Process, file_exists
from . import SimulatorInterface, ListOfStringOption, StringOption, BooleanOption
from ..vhdl_standard import VHDL

LOGGER = logging.getLogger(__name__)


class GHDLInterface(SimulatorInterface):
class GHDLInterface(SimulatorInterface): # pylint: disable=too-many-instance-attributes
"""
Interface for GHDL simulator
"""
Expand Down Expand Up @@ -108,6 +109,7 @@ def __init__( # pylint: disable=too-many-arguments
self._gtkwave_args = gtkwave_args
self._backend = backend
self._vhdl_standard = None
self._coverage_dirs = set()
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved

def has_valid_exit_code(self):
"""
Expand Down Expand Up @@ -164,12 +166,19 @@ def determine_version(cls, prefix):
@classmethod
def supports_vhpi(cls):
"""
Return if the simulator supports VHPI
Returns True when the simulator supports VHPI
"""
return (cls.determine_backend(cls.find_prefix_from_path()) != "mcode") or (
cls.determine_version(cls.find_prefix_from_path()) > 0.36
)

@classmethod
def supports_coverage(cls):
"""
Returns True when the simulator supports coverage
"""
return cls.determine_backend(cls.find_prefix_from_path()) == "gcc"

def _has_output_flag(self):
"""
Returns if backend supports output flag
Expand Down Expand Up @@ -347,8 +356,16 @@ def simulate( # pylint: disable=too-many-locals
)

status = True

gcov_env = environ.copy()
eine marked this conversation as resolved.
Show resolved Hide resolved
if config.sim_options.get("enable_coverage", False):
# Set environment variable to put the coverage output in the test_output folder
coverage_dir = str(Path(output_path) / "coverage")
gcov_env["GCOV_PREFIX"] = coverage_dir
self._coverage_dirs.add(coverage_dir)

try:
proc = Process(cmd)
proc = Process(cmd, env=gcov_env)
proc.consume_output()
except Process.NonZeroExitCode:
status = False
Expand All @@ -364,3 +381,67 @@ def simulate( # pylint: disable=too-many-locals
subprocess.call(cmd)

return status

def _compile_source_file(self, source_file, printer):
"""
Runs parent command for compilation, and moves the .gcno file (if any) to the compilation output folder
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
"""
compilation_ok = super()._compile_source_file(source_file, printer)

# GCOV gcno files are output to where the command is run,
# move it back to the compilation folder
source_path = Path(source_file.name)
gcno_file = Path(source_path.stem + ".gcno")
if Path(gcno_file).exists():
new_path = Path(source_file.library.directory) / gcno_file
gcno_file.rename(new_path)

return compilation_ok

def merge_coverage(self, file_name, args=None):
"""
Merge coverage from all test cases
"""

LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
dir_name = file_name
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved

# Loop over each .gcda output folder and merge them two at a time
first_input = True
for coverage_dir in self._coverage_dirs:
if file_exists(coverage_dir):
eine marked this conversation as resolved.
Show resolved Hide resolved
if first_input:
eine marked this conversation as resolved.
Show resolved Hide resolved
# There is no output to merge with, so merge with itself to produce first output
first_dir = coverage_dir
else:
first_dir = dir_name
merge_command = [
"gcov-tool",
"merge",
"-o",
dir_name,
first_dir,
coverage_dir,
]
subprocess.call(merge_command)
first_input = False
else:
LOGGER.warning("Missing coverage directory: %s", coverage_dir)

# Find actual output path of the .gcda files (they are deep in hierarchy)
dir_path = Path(dir_name)
gcda_files = dir_path.glob("**/*.gcda")
gcda_dirs = [x.parent for x in gcda_files]
eine marked this conversation as resolved.
Show resolved Hide resolved
gcda_dirs = set(gcda_dirs)
assert len(gcda_dirs) == 1, "Expected exactly one folder with gcda files"
gcda_dir = str(gcda_dirs.pop())
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved

# Add compile-time .gcno files as well, they are needed or the report
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
if file_exists(dir_name):
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
for library in self._project.get_libraries():
lib_dir_path = Path(library.directory)
if lib_dir_path.exists():
gcno_files = lib_dir_path.glob("*.gcno")
for gcno_file in gcno_files:
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
# gcda files are output to the the coverage directory, but within subdirectories
# taken directly from where the command was run, so appdn cwd to output here
LudvigVidlid marked this conversation as resolved.
Show resolved Hide resolved
shutil.copy(gcno_file, gcda_dir)
7 changes: 7 additions & 0 deletions vunit/sim_if/modelsim.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ def supports_vhdl_package_generics(cls):
"""
return True

@staticmethod
def supports_coverage():
"""
Returns True when the simulator supports coverage
"""
return True

def __init__(self, prefix, output_path, persistent=False, gui=False):
SimulatorInterface.__init__(self, output_path, gui)
VsimSimulatorMixin.__init__(
Expand Down
7 changes: 7 additions & 0 deletions vunit/sim_if/rivierapro.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ def supports_vhdl_package_generics(cls):
"""
return True

@staticmethod
def supports_coverage():
"""
Returns True when the simulator supports coverage
"""
return True

def __init__(self, prefix, output_path, persistent=False, gui=False):
SimulatorInterface.__init__(self, output_path, gui)
VsimSimulatorMixin.__init__(
Expand Down
10 changes: 10 additions & 0 deletions vunit/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,3 +1099,13 @@ def get_simulator_name(self):
if self._simulator_class is None:
return None
return self._simulator_class.name

def simulator_supports_coverage(self):
"""
Returns True if the simulator supports coverage
eine marked this conversation as resolved.
Show resolved Hide resolved

Will return None if no simulator was found.
"""
if self._simulator_class is None:
return None
return self._simulator_class.supports_coverage