from getopt import GetoptError, getopt
from os import O_DIRECTORY, O_RDONLY, environ, fwalk, makedirs
from os import open as os_open
from os import rmdir, scandir, unlink, walk
from os.path import abspath, dirname, exists
from os.path import join as path_join
from re import compile as re_compile
from re import escape
from subprocess import run
from sys import argv
from sys import exit as sys_exit
from sys import platform, stderr, stdout
import toml
ROOT = dirname(abspath(__file__))
ROOT_FD = os_open(ROOT, O_RDONLY | O_DIRECTORY)
ROOT_DIRS = []
def merge_env(**kwargs):
env = dict(environ)
env.update(kwargs)
return env
class Crate:
def __init__(self, root, cargodef):
self.root = root
self.cargodef = cargodef
@property
def crate_name(self):
return self.cargodef["package"]["name"]
@property
def target_dir(self):
return path_join(ROOT, "target", "coverage", self.crate_name)
@property
def profdata_filename(self):
return path_join(self.target_dir, "cov.profdata")
@property
def html_dir(self):
return path_join(ROOT, "coverage-html", self.crate_name)
@property
def target_exec(self):
exec_name = re_compile(
r"^" + escape(self.crate_name.replace("-", "_")) + r"-[0-9a-f]{16}$"
)
for entry in scandir(path_join(self.target_dir, "debug", "deps")):
if not entry.is_file() or entry.stat().st_mode & 0o100 == 0:
continue
m = exec_name.match(entry.name)
if m is not None:
return entry.path
raise RuntimeError(f"Could not find target executable for {self.crate_name}")
@property
def ignore_filename_regex(self):
_ignore_filename_regex = getattr(self, "_ignore_filename_regex", None)
if _ignore_filename_regex is None:
self._ignore_filename_regex = (
_ignore_filename_regex
) = self.get_ignore_filename_regex()
return _ignore_filename_regex
def get_ignore_filename_regex(self):
entries = ["/.cargo", ".*thread/local.rs"]
for entry in ROOT_DIRS:
if not entry.is_dir():
continue
if entry.path.startswith(self.root):
continue
entries.append(escape(f"/{entry.name}/"))
return "|".join(entries)
def clean(self):
self.cargo("clean")
def test(self):
if exists(self.target_dir):
for entry in scandir(self.target_dir):
if entry.is_file() and (
entry.name.endswith(".profdata") or entry.name.endswith(".profraw")
):
unlink(entry.path)
self.cargo("test")
self.merge_profile_data()
self.generate_lcov()
def cargo(self, cmd, *args):
args = ["cargo", cmd, "--target-dir", self.target_dir, *args]
print("Running:", " ".join(args))
self.run(args)
def merge_profile_data(self):
args = ["llvm-profdata", "merge", "-sparse"]
for dirname, _, filenames in walk(self.target_dir):
for filename in filenames:
if filename.endswith(".profraw"):
args.append(path_join(dirname, filename))
args.append("-o")
args.append(self.profdata_filename)
self.run(args)
def generate_lcov(self):
print(f"Generating {self.crate_name}.lcov file")
print(f"Ignore filename regex: {self.ignore_filename_regex}")
args = [
"llvm-cov",
"export",
"-format=lcov",
f"-Xdemangler={environ['HOME']}/.cargo/bin/rustfilt",
f"-ignore-filename-regex={self.ignore_filename_regex}",
f"-instr-profile={self.profdata_filename}",
path_join(self.target_dir, self.target_exec),
]
with open(path_join(ROOT, f"{self.crate_name}.lcov"), "w") as fd:
self.run(args, stdout=fd)
def generate_html(self):
makedirs(self.html_dir, exist_ok=True)
for _, subdirs, filenames, dir_fd in fwalk(self.html_dir, topdown=False):
for filename in filenames:
unlink(filename, dir_fd=dir_fd)
for subdir in subdirs:
rmdir(subdir, dir_fd=dir_fd)
args = [
"llvm-cov",
"show",
"-format=html",
f"-Xdemangler={environ['HOME']}/.cargo/bin/rustfilt",
f"-ignore-filename-regex={self.ignore_filename_regex}",
f"-instr-profile={self.profdata_filename}",
f"-output-dir={self.html_dir}",
path_join(self.target_dir, self.target_exec),
]
result = self.run(args)
def open_html(self):
if platform == "darwin":
self.run(["open", path_join(self.html_dir, "index.html")])
elif platform == "linux":
self.run(["xdg-open", path_join(self.html_dir, "index.html")])
def generate_report(self):
args = [
"llvm-cov",
"report",
"-use-color",
f"-Xdemangler={environ['HOME']}/.cargo/bin/rustfilt",
f"-ignore-filename-regex={self.ignore_filename_regex}",
f"-instr-profile={self.profdata_filename}",
path_join(self.target_dir, self.target_exec),
]
print("")
print("Coverage report for", self.crate_name)
self.run(args)
def run(self, args, *, stdout=None):
subproc_env = merge_env(
LLVM_PROFILE_FILE=path_join(self.target_dir, "cov-%m.profraw")
)
run(args, cwd=self.root, check=True, env=subproc_env, stdout=stdout)
def load_workspace(members):
results = []
for member in members:
crate_root = path_join(ROOT, member)
path = path_join(crate_root, "Cargo.toml")
with open(path, "r") as cargo_fd:
cargo = toml.load(cargo_fd)
results.append(Crate(crate_root, cargo))
return results
def main(args):
global ROOT_DIRS
clean = True
test = True
html = True
html_open = True
report = True
try:
opts, args = getopt(
args,
"h",
[
"clean",
"no-clean",
"html",
"no-html",
"open",
"no-open",
"report",
"no-report",
"test",
"no-test",
],
)
for opt, val in opts:
if opt in ["-h", "--help"]:
usage(stdout)
return 0
if opt in ["--clean"]:
clean = True
if opt in ["--no-clean"]:
clean = False
if opt in ["--html"]:
html = True
if opt in ["--no-html"]:
html = False
if opt in ["--open"]:
html_open = True
if opt in ["--no-open"]:
html_open = False
if opt in ["--report"]:
report = True
if opt in ["--no-report"]:
report = False
if opt in ["--test"]:
test = True
if opt in ["--no-test"]:
test = False
except GetoptError as e:
print(e, file=stderr)
usage()
return 2
for entry in scandir(ROOT):
if entry.is_dir():
ROOT_DIRS.append(entry)
with open(path_join(ROOT, "Cargo.toml"), "r") as cargo_fd:
cargo = toml.load(cargo_fd)
if "workspace" in cargo:
crates = load_workspace(cargo["workspace"]["members"])
else:
crates = [Crate(ROOT, cargo)]
environ["CARGO_INCREMENTAL"] = "0"
environ["RUSTFLAGS"] = "-Cinstrument-coverage -Ccodegen-units=1 -Copt-level=0"
environ["RUST_LOG"] = "trace"
if clean:
for crate in crates:
if not args or crate.crate_name in args:
crate.clean()
if test:
for crate in crates:
if not args or crate.crate_name in args:
crate.test()
if html:
for crate in crates:
if not args or crate.crate_name in args:
crate.generate_html()
if report:
for crate in crates:
if not args or crate.crate_name in args:
crate.generate_report()
if html and html_open:
for crate in crates:
if not args or crate.crate_name in args:
crate.open_html()
def usage(fd=stderr):
fd.write(__doc__)
if __name__ == "__main__":
sys_exit(main(argv[1:]))