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>
diff --git a/scripts/sanitycheck b/scripts/sanitycheck
index 713c1b0..ad21f57 100755
--- a/scripts/sanitycheck
+++ b/scripts/sanitycheck
@@ -3420,6 +3420,9 @@
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 @@
(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 @@
"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")