forked from github/verilator
416 lines
14 KiB
Python
Executable File
416 lines
14 KiB
Python
Executable File
#!/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=<hidden>")
|
|
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:
|