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")