sanitycheck: Add option to use gcovr for coverage
gcovr is already a dependency in scripts/requirements.txt. The visualization is different, but the functionality should be the same. Tested with gcovr 4.2. Relates to #17626. Signed-off-by: Christian Taedcke <hacking@taedcke.com>
This commit is contained in:
parent
132130d17a
commit
ed22c5efe9
1 changed files with 168 additions and 77 deletions
|
@ -3420,6 +3420,9 @@ structure in the main Zephyr tree: boards/<arch>/<board_name>/""")
|
|||
help="Path to the gcov tool to use for code coverage "
|
||||
"reports")
|
||||
|
||||
parser.add_argument("--coverage-tool", choices=['lcov', 'gcovr'], default='lcov',
|
||||
help="Tool to use to generate coverage report.")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
@ -3470,110 +3473,194 @@ def size_report(sc):
|
|||
(sc.rom_size, sc.ram_size))
|
||||
info("")
|
||||
|
||||
def retrieve_gcov_data(intput_file):
|
||||
if VERBOSE:
|
||||
print("Working on %s" %intput_file)
|
||||
extracted_coverage_info = {}
|
||||
capture_data = False
|
||||
capture_complete = False
|
||||
with open(intput_file, 'r') as fp:
|
||||
for line in fp.readlines():
|
||||
if re.search("GCOV_COVERAGE_DUMP_START", line):
|
||||
capture_data = True
|
||||
continue
|
||||
if re.search("GCOV_COVERAGE_DUMP_END", line):
|
||||
capture_complete = True
|
||||
break
|
||||
# Loop until the coverage data is found.
|
||||
if not capture_data:
|
||||
continue
|
||||
if line.startswith("*"):
|
||||
sp = line.split("<")
|
||||
if len(sp) > 1:
|
||||
# Remove the leading delimiter "*"
|
||||
file_name = sp[0][1:]
|
||||
# Remove the trailing new line char
|
||||
hex_dump = sp[1][:-1]
|
||||
class CoverageTool:
|
||||
""" Base class for every supported coverage tool
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.gcov_tool = options.gcov_tool
|
||||
|
||||
@staticmethod
|
||||
def factory(tool):
|
||||
if tool == 'lcov':
|
||||
return Lcov()
|
||||
if tool == 'gcovr':
|
||||
return Gcovr()
|
||||
error("Unsupported coverage tool specified: {}".format(tool))
|
||||
|
||||
@staticmethod
|
||||
def retrieve_gcov_data(intput_file):
|
||||
if VERBOSE:
|
||||
print("Working on %s" %intput_file)
|
||||
extracted_coverage_info = {}
|
||||
capture_data = False
|
||||
capture_complete = False
|
||||
with open(intput_file, 'r') as fp:
|
||||
for line in fp.readlines():
|
||||
if re.search("GCOV_COVERAGE_DUMP_START", line):
|
||||
capture_data = True
|
||||
continue
|
||||
if re.search("GCOV_COVERAGE_DUMP_END", line):
|
||||
capture_complete = True
|
||||
break
|
||||
# Loop until the coverage data is found.
|
||||
if not capture_data:
|
||||
continue
|
||||
if line.startswith("*"):
|
||||
sp = line.split("<")
|
||||
if len(sp) > 1:
|
||||
# Remove the leading delimiter "*"
|
||||
file_name = sp[0][1:]
|
||||
# Remove the trailing new line char
|
||||
hex_dump = sp[1][:-1]
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
extracted_coverage_info.update({file_name:hex_dump})
|
||||
if not capture_data:
|
||||
capture_complete = True
|
||||
return {'complete': capture_complete, 'data': extracted_coverage_info}
|
||||
|
||||
@staticmethod
|
||||
def create_gcda_files(extracted_coverage_info):
|
||||
if VERBOSE:
|
||||
print("Generating gcda files")
|
||||
for filename, hexdump_val in extracted_coverage_info.items():
|
||||
# if kobject_hash is given for coverage gcovr fails
|
||||
# hence skipping it problem only in gcovr v4.1
|
||||
if "kobject_hash" in filename:
|
||||
filename = (filename[:-4]) +"gcno"
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
extracted_coverage_info.update({file_name:hex_dump})
|
||||
if not capture_data:
|
||||
capture_complete = True
|
||||
return {'complete': capture_complete, 'data': extracted_coverage_info}
|
||||
|
||||
def create_gcda_files(extracted_coverage_info):
|
||||
if VERBOSE:
|
||||
print("Generating gcda files")
|
||||
for filename, hexdump_val in extracted_coverage_info.items():
|
||||
# if kobject_hash is given for coverage gcovr fails
|
||||
# hence skipping it problem only in gcovr v4.1
|
||||
if "kobject_hash" in filename:
|
||||
filename = (filename[:-4]) +"gcno"
|
||||
try:
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
with open(filename, 'wb') as fp:
|
||||
fp.write(bytes.fromhex(hexdump_val))
|
||||
|
||||
with open(filename, 'wb') as fp:
|
||||
fp.write(bytes.fromhex(hexdump_val))
|
||||
|
||||
def generate_coverage(outdir, ignores):
|
||||
def generate(self, outdir):
|
||||
for filename in glob.glob("%s/**/handler.log" % outdir, recursive=True):
|
||||
gcov_data = self.__class__.retrieve_gcov_data(filename)
|
||||
capture_complete = gcov_data['complete']
|
||||
extracted_coverage_info = gcov_data['data']
|
||||
if capture_complete:
|
||||
self.__class__.create_gcda_files(extracted_coverage_info)
|
||||
verbose("Gcov data captured: {}".format(filename))
|
||||
else:
|
||||
error("Gcov data capture incomplete: {}".format(filename))
|
||||
|
||||
for filename in glob.glob("%s/**/handler.log" %outdir, recursive=True):
|
||||
gcov_data = retrieve_gcov_data(filename)
|
||||
capture_complete = gcov_data['complete']
|
||||
extracted_coverage_info = gcov_data['data']
|
||||
if capture_complete:
|
||||
create_gcda_files(extracted_coverage_info)
|
||||
verbose("Gcov data captured: {}".format(filename))
|
||||
else:
|
||||
error("Gcov data capture incomplete: {}".format(filename))
|
||||
with open(os.path.join(outdir, "coverage.log"), "a") as coveragelog:
|
||||
ret = self._generate(outdir, coveragelog)
|
||||
if ret == 0:
|
||||
info("HTML report generated: {}".format(
|
||||
os.path.join(outdir, "coverage", "index.html")))
|
||||
|
||||
gcov_tool = options.gcov_tool
|
||||
|
||||
with open(os.path.join(outdir, "coverage.log"), "a") as coveragelog:
|
||||
class Lcov(CoverageTool):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ignores = []
|
||||
|
||||
def add_ignore_file(self, pattern):
|
||||
self.ignores.append('*' + pattern + '*')
|
||||
|
||||
def add_ignore_directory(self, pattern):
|
||||
self.ignores.append(pattern + '/*')
|
||||
|
||||
def _generate(self, outdir, coveragelog):
|
||||
coveragefile = os.path.join(outdir, "coverage.info")
|
||||
ztestfile = os.path.join(outdir, "ztest.info")
|
||||
subprocess.call(["lcov", "--gcov-tool", gcov_tool,
|
||||
"--capture", "--directory", outdir,
|
||||
"--rc", "lcov_branch_coverage=1",
|
||||
"--output-file", coveragefile], stdout=coveragelog)
|
||||
subprocess.call(["lcov", "--gcov-tool", self.gcov_tool,
|
||||
"--capture", "--directory", outdir,
|
||||
"--rc", "lcov_branch_coverage=1",
|
||||
"--output-file", coveragefile], stdout=coveragelog)
|
||||
# We want to remove tests/* and tests/ztest/test/* but save tests/ztest
|
||||
subprocess.call(["lcov", "--gcov-tool", gcov_tool, "--extract", coveragefile,
|
||||
subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--extract",
|
||||
coveragefile,
|
||||
os.path.join(ZEPHYR_BASE, "tests", "ztest", "*"),
|
||||
"--output-file", ztestfile,
|
||||
"--rc", "lcov_branch_coverage=1"], stdout=coveragelog)
|
||||
|
||||
if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
|
||||
subprocess.call(["lcov", "--gcov-tool", gcov_tool, "--remove", ztestfile,
|
||||
subprocess.call(["lcov", "--gcov-tool", self.gcov_tool, "--remove",
|
||||
ztestfile,
|
||||
os.path.join(ZEPHYR_BASE, "tests/ztest/test/*"),
|
||||
"--output-file", ztestfile,
|
||||
"--rc", "lcov_branch_coverage=1"],
|
||||
stdout=coveragelog)
|
||||
stdout=coveragelog)
|
||||
files = [coveragefile, ztestfile]
|
||||
else:
|
||||
files = [coveragefile]
|
||||
|
||||
for i in ignores:
|
||||
for i in self.ignores:
|
||||
subprocess.call(
|
||||
["lcov", "--gcov-tool", gcov_tool, "--remove",
|
||||
coveragefile, i, "--output-file",
|
||||
coveragefile, "--rc", "lcov_branch_coverage=1"],
|
||||
["lcov", "--gcov-tool", self.gcov_tool, "--remove",
|
||||
coveragefile, i, "--output-file",
|
||||
coveragefile, "--rc", "lcov_branch_coverage=1"],
|
||||
stdout=coveragelog)
|
||||
|
||||
#The --ignore-errors source option is added to avoid it exiting due to
|
||||
#samples/application_development/external_lib/
|
||||
ret = subprocess.call(["genhtml", "--legend", "--branch-coverage",
|
||||
"--ignore-errors", "source",
|
||||
"-output-directory",
|
||||
os.path.join(outdir, "coverage")] + files,
|
||||
# The --ignore-errors source option is added to avoid it exiting due to
|
||||
# samples/application_development/external_lib/
|
||||
return subprocess.call(["genhtml", "--legend", "--branch-coverage",
|
||||
"--ignore-errors", "source",
|
||||
"-output-directory",
|
||||
os.path.join(outdir, "coverage")] + files,
|
||||
stdout=coveragelog)
|
||||
if ret==0:
|
||||
info("HTML report generated: %s"%
|
||||
os.path.join(outdir, "coverage","index.html"))
|
||||
|
||||
|
||||
class Gcovr(CoverageTool):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ignores = []
|
||||
|
||||
def add_ignore_file(self, pattern):
|
||||
self.ignores.append('.*' + pattern + '.*')
|
||||
|
||||
def add_ignore_directory(self, pattern):
|
||||
self.ignores.append(pattern + '/.*')
|
||||
|
||||
@staticmethod
|
||||
def _interleave_list(prefix, list):
|
||||
tuple_list = [(prefix, item) for item in list]
|
||||
return [item for sublist in tuple_list for item in sublist]
|
||||
|
||||
def _generate(self, outdir, coveragelog):
|
||||
coveragefile = os.path.join(outdir, "coverage.json")
|
||||
ztestfile = os.path.join(outdir, "ztest.json")
|
||||
|
||||
excludes = Gcovr._interleave_list("-e", self.ignores)
|
||||
|
||||
# We want to remove tests/* and tests/ztest/test/* but save tests/ztest
|
||||
subprocess.call(["gcovr", "-r", ZEPHYR_BASE, "--gcov-executable",
|
||||
self.gcov_tool, "-e", "tests/*"] + excludes +
|
||||
["--json", "-o", coveragefile, outdir],
|
||||
stdout=coveragelog)
|
||||
|
||||
subprocess.call(["gcovr", "-r", ZEPHYR_BASE, "--gcov-executable",
|
||||
self.gcov_tool, "-f", "tests/ztest", "-e",
|
||||
"tests/ztest/test/*", "--json", "-o", ztestfile,
|
||||
outdir], stdout=coveragelog)
|
||||
|
||||
if os.path.exists(ztestfile) and os.path.getsize(ztestfile) > 0:
|
||||
files = [coveragefile, ztestfile]
|
||||
else:
|
||||
files = [coveragefile]
|
||||
|
||||
subdir = os.path.join(outdir, "coverage")
|
||||
os.makedirs(subdir, exist_ok=True)
|
||||
|
||||
tracefiles = self._interleave_list("--add-tracefile", files)
|
||||
|
||||
return subprocess.call(["gcovr", "-r", ZEPHYR_BASE, "--html",
|
||||
"--html-details"] + tracefiles +
|
||||
["-o", os.path.join(subdir, "index.html")],
|
||||
stdout=coveragelog)
|
||||
|
||||
|
||||
def get_generator():
|
||||
if options.ninja:
|
||||
|
@ -3990,7 +4077,11 @@ def main():
|
|||
"i586-zephyr-elf/bin/i586-zephyr-elf-gcov")
|
||||
|
||||
info("Generating coverage files...")
|
||||
generate_coverage(options.outdir, ["*generated*", "tests/*", "samples/*"])
|
||||
coverage_tool = CoverageTool.factory(options.coverage_tool)
|
||||
coverage_tool.add_ignore_file('generated')
|
||||
coverage_tool.add_ignore_directory('tests')
|
||||
coverage_tool.add_ignore_directory('samples')
|
||||
coverage_tool.generate(options.outdir)
|
||||
|
||||
if options.device_testing:
|
||||
print("\nHardware distribution summary:\n")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue