#!/usr/bin/env python3 # pylint: disable=C0103,C0114,C0115,C0116,C0209,R0912,R0914,R0915,W0125,W0621,exec-used ###################################################################### import argparse import glob import multiprocessing import os import re import subprocess import sys RealPath = os.path.dirname(os.path.realpath(__file__)) Exclude_Branch_Regexps = [] Exclude_Line_Regexps = [] Remove_Gcda_Regexps = [] Remove_Sources = [] Source_Globs = [] if 'VERILATOR_ROOT' not in os.environ: os.environ['VERILATOR_ROOT'] = os.getcwd() ###################################################################### def test(): if not os.path.exists("nodist/code_coverage.dat"): sys.exit("%Error: Run code_coverage from the top of the verilator kit") exec(open("./nodist/code_coverage.dat", "r", encoding="utf8").read()) # pylint: disable=consider-using-with if Args.stage_enabled[0]: ci_fold_start("distclean") print("Stage 0: distclean") run("make distclean || true") ci_fold_end() if Args.stage_enabled[1]: ci_fold_start("configure") print("Stage 1: configure (coverage on)") run("autoconf") run("./configure --enable-longtests --enable-coverage CXX=g++") ci_fold_end() if Args.stage_enabled[2]: ci_fold_start("build") print("Stage 2: build") nproc = multiprocessing.cpu_count() run("make -k -j " + str(nproc) + " VERILATOR_NO_OPT_BUILD=1") # The optimized versions will not collect good coverage, overwrite them run("cp bin/verilator_bin_dbg bin/verilator_bin") run("cp bin/verilator_coverage_bin_dbg bin/verilator_coverage_bin") ci_fold_end() if Args.stage_enabled[3]: ci_fold_start("test") print("Stage 3: make tests (with coverage on)") if not Args.tests: if not Args.scenarios or re.match('dist', Args.scenarios): run("make examples VERILATOR_NO_OPT_BUILD=1") run("make test_regress VERILATOR_NO_OPT_BUILD=1" + (" SCENARIOS='" + Args.scenarios + "'" if Args.scenarios else "") + (" DRIVER_HASHSET='--hashset=" + Args.hashset + "'" if Args.hashset else "") + ('' if Args.stop else ' || true')) else: for test in Args.tests: if not os.path.exists(test) and os.path.exists( "test_regress/t/" + test): test = "test_regress/t/" + test run(test) ci_fold_end() cc_dir = "nodist/obj_dir/coverage" if Args.stage_enabled[4]: ci_fold_start("gcno") print("Stage 4: Create gcno files under " + cc_dir) os.makedirs(cc_dir, exist_ok=True) os.makedirs(cc_dir + "/info", exist_ok=True) with subprocess.Popen("find . -print | grep .gcda", shell=True, stdout=subprocess.PIPE) as sp: datout = sp.stdout.read() dats = {} for dat in datout.splitlines(): dat = dat.decode('utf-8') dats[dat] = 1 for dat in sorted(dats.keys()): gcno = re.sub(r'\.gcda$', '.gcno', dat) for regexp in Remove_Gcda_Regexps: if re.search(regexp, dat): # Remove .gcda/.gcno for files we don't care about before we slowly # read them unlink_ok(dat) unlink_ok(gcno) del dats[dat] break with subprocess.Popen("find . -print | grep .gcno", shell=True, stdout=subprocess.PIPE) as sp: datout = sp.stdout.read() gcnos = {} for gcno in datout.splitlines(): gcno = gcno.decode('utf-8') gbase = re.sub(r'.*/', '', gcno, count=1) gcnos[gbase] = os.path.abspath(gcno) # We need a matching .gcno for every .gcda, try to find a matching file elsewhere for dat in sorted(dats): gcno = re.sub(r'\.gcda$', '.gcno', dat) gbase = re.sub(r'.*/', '', gcno, count=1) if not os.path.exists(gcno): if gbase in gcnos: os.symlink(gcnos[gbase], gcno) else: print("MISSING .gcno for a .gcda: " + gcno, file=sys.stderr) ci_fold_end() if Args.stage_enabled[5]: ci_fold_start("fastcov") # Must run in root directory to find all files os.makedirs(cc_dir, exist_ok=True) run(RealPath + "/fastcov.py -b -c src/obj_dbg -X --lcov" + # " --exclude /usr --exclude test_regress" + " -o " + cc_dir + " -o " + cc_dir + "/app_total.info") # For debug to convert single .gcna/.gcno in a directory to cov.info: # lcov -c -d . -o cov.info ci_fold_end() if Args.stage_enabled[6]: ci_fold_start("clone") # No control file to override single lines, so replicate the sources # Also lets us see the insertion markers in the HTML source res print("Stage 6: Clone sources under " + cc_dir) clone_sources(cc_dir) ci_fold_end() if Args.stage_enabled[11]: ci_fold_start("dirs") print("Stage 11: Cleanup paths") cleanup_abs_paths_info(cc_dir, cc_dir + "/app_total.info", cc_dir + "/app_total.info") ci_fold_end() if Args.stage_enabled[12]: ci_fold_start("filter") print("Stage 12: Filter processed source files") inc = '' for globf in Source_Globs: for infile in glob.glob(globf): inc += " '" + infile + "'" exc = '' for globf in Remove_Sources: # Fastcov does exact match not globbing at present # Lcov requires whole path match so needs the glob globf = re.sub(r'^\*', '', globf) globf = re.sub(r'\*$', '', globf) exc += " '" + globf + "'" if inc != '': inc = "--include " + inc if exc != '': exc = "--exclude " + exc run("cd " + cc_dir + " ; " + RealPath + "/fastcov.py -C app_total.info " + inc + " " + exc + " -x --lcov -o app_total_f.info") ci_fold_end() if Args.stage_enabled[17]: ci_fold_start("report") print("Stage 17: Create HTML") run("cd " + cc_dir + " ; genhtml app_total_f.info --demangle-cpp" + " --rc lcov_branch_coverage=1 --rc genhtml_hi_limit=100 --output-directory html" ) ci_fold_end() if Args.stage_enabled[18]: ci_fold_start("upload") print("Stage 18: Upload") cmd = "bash <(cat ci/coverage-upload.sh) -f " + cc_dir + "/app_total.info" print("print: Not running: export CODECOV_TOKEN=") print("print: Not running: " + cmd) ci_fold_end() if Args.stage_enabled[19]: print("*-* All Finished *-*") print("") print("* See report in " + cc_dir + "/html/index.html") print( "* Remember to make distclean && ./configure before working on non-coverage" ) def clone_sources(cc_dir): excluded_lines = 0 excluded_br_lines = 0 for globf in Source_Globs: for infile in glob.glob(globf): if re.match(r'^/', infile): sys.exit( "%Error: source globs should be relative not absolute filenames, " + infile) outfile = cc_dir + "/" + infile outpath = re.sub(r'/[^/]*$', '', outfile, count=1) os.makedirs(outpath, exist_ok=True) with open(infile, "r", encoding="utf8") as fh: with open(outfile, "w", encoding="utf8") as ofh: lineno = 0 for line in fh: lineno += 1 line = line.rstrip() done = False if re.search(r'LCOV_EXCL_LINE', line): line += " LCOV_EXCL_BR_LINE" done = True elif re.search(r'LCOV_EXCL_START', line): line += " LCOV_EXCL_BR_START" done = True elif re.search(r'LCOV_EXCL_STOP', line): line += " LCOV_EXCL_BR_STOP" done = True for regexp in Exclude_Line_Regexps: if done: break if re.search(regexp, line): # print("%s:%d: %s" % (infile, lineno, line) line += " //code_coverage: // LCOV_EXCL_LINE LCOV_EXCL_BR_LINE" excluded_lines += 1 excluded_br_lines += 1 done = True for regexp in Exclude_Branch_Regexps: if done: break if re.search(regexp, line): # print("%s:%d: %s" % (infile, lineno, line) line += " //code_coverage: // LCOV_EXCL_BR_LINE" excluded_br_lines += 1 done = True ofh.write(line + "\n") print("Number of source lines automatically LCOV_EXCL_LINE'ed: %d" % excluded_lines) print("Number of source lines automatically LCOV_EXCL_BR_LINE'ed: %d" % excluded_br_lines) def cleanup_abs_paths_info(cc_dir, infile, outfile): lines = [] with open(infile, "r", encoding="utf8") as fh: for line in fh: if re.search(r'^SF:', line): line = re.sub(os.environ['VERILATOR_ROOT'] + '/', '', line, count=1) line = re.sub(cc_dir + '/', '', line, count=1) line = re.sub(r'obj_dbg/verilog.y$', 'verilog.y', line) # print("Remaining SF: "+line) lines.append(line) with open(outfile, "w", encoding="utf8") as ofh: for line in lines: ofh.write(line) def cleanup_abs_paths_json(cc_dir, infile, outfile): # Handcrafted cleanup, alternative would be to deserialize/serialize JSON, # but this is much faster lines = [] with open(infile, "r", encoding="utf8") as fh: for line in fh: line = re.sub('"' + os.environ['VERILATOR_ROOT'] + '/', '"', line) line = re.sub('"' + cc_dir + '/', '"', line) line = re.sub(r'obj_dbg/verilog.y$', 'verilog.y', line) lines.append(line) with open(outfile, "w", encoding="utf8") as ofh: for line in lines: ofh.write(line) ###################################################################### # .dat file callbacks def exclude_branch_regexp(*regexps): Exclude_Branch_Regexps.extend(regexps) def exclude_line_regexp(*regexps): Exclude_Line_Regexps.extend(regexps) def remove_gcda_regexp(*regexps): Remove_Gcda_Regexps.extend(regexps) def remove_source(*sources): Remove_Sources.extend(sources) def source_globs(*dirs): Source_Globs.extend(dirs) ####################################################################### def run(command): # run a system command, check errors print("\t%s" % command) status = subprocess.call(command, shell=True) if status < 0: raise Exception("%Error: Command failed " + command + ", stopped") def unlink_ok(filename): try: os.unlink(filename) except OSError: pass def ci_fold_start(action): print("::group::" + action, flush=True) def ci_fold_end(): print("::endgroup::\n", flush=True) ####################################################################### ####################################################################### parser = argparse.ArgumentParser( allow_abbrev=False, formatter_class=argparse.RawDescriptionHelpFormatter, description= """code_coverage builds Verilator with C++ coverage support and runs tests with coverage enabled. This will rebuild the current object files. Run as: cd $VERILATOR_ROOT nodist/code_coverage""", epilog= """Copyright 2019-2023 by Wilson Snyder. This program is free software; you can redistribute it and/or modify it under the terms of either the GNU Lesser General Public License Version 3 or the Perl Artistic License Version 2.0. SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0""") parser.add_argument('--debug', action='store_true', help='enable debug') parser.add_argument('--hashset', action='store', help='pass test hashset onto driver.pl test harness') parser.add_argument('--scenarios', action='store', help='pass test scenarios onto driver.pl test harness') parser.add_argument( '--stages', action='store', help='runs a specific stage or range of stages (see the script)') parser.add_argument( '--test', action='append', dest='tests', default=[], help= 'Instead of normal regressions, run the specified test(s), may be used multiple times' ) parser.add_argument('--no-stop', dest='stop', action='store_false', help='do not stop collecting data if tests fail') parser.set_defaults(stop=True) Args = parser.parse_args() if True: start = 0 end = 99 Args.stage_enabled = {} if Args.stages: match_one = re.match(r'^(\d+)$', Args.stages) match_range = re.match(r'^(\d+)-(\d+)$', Args.stages) match_to = re.match(r'^-(\d+)$', Args.stages) match_from = re.match(r'^(\d+)-$', Args.stages) if match_one: start = end = int(match_one.group(1)) elif match_range: start = int(match_range.group(1)) end = int(match_range.group(2)) elif match_to: end = int(match_to.group(1)) elif match_from: start = int(match_from.group(1)) else: sys.exit("%Error: --stages not understood: " + Args.stages) for n in range(0, 100): Args.stage_enabled[n] = False for n in range(start, end + 1): Args.stage_enabled[n] = True test() ###################################################################### # Local Variables: # compile-command: "cd .. ; nodist/code_coverage " # End: