1414import subprocess
1515import shlex
1616import re
17+ import shutil
1718from json import dump
1819from sys import stdout # To avoid output catched in non-verbose mode
1920from warnings import warn
2526LOGGER = 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 )
0 commit comments