use cargo_metadata::MetadataCommand;
use cargo_metadata::Package;
use clap::Parser;
use radix_engine_interface::types::Level;
use regex::Regex;
use sbor::prelude::*;
use scrypto_compiler::is_scrypto_cargo_locked_env_var_active;
use scrypto_compiler::RustFlags;
use scrypto_compiler::ScryptoCompiler;
use scrypto_compiler::DEFAULT_ENVIRONMENT_VARIABLES;
use std::env::current_dir;
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use std::string::FromUtf8Error;
use std::sync::LazyLock;
use walkdir::WalkDir;
use crate::utils::*;
#[derive(Parser, Debug)]
pub struct Coverage {
arguments: Vec<String>,
#[clap(long)]
locked: bool,
#[clap(long)]
path: Option<PathBuf>,
}
impl Coverage {
pub fn run(self) -> Result<(), CoverageError> {
static LLVM_IR_CORRECTIONS_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?ms)^(define[^\n]*\n).*?^}\s*$").unwrap());
let paths = Paths::new(self.path)?;
let llvm_toolchain = LLVMToolchain::new()?;
let build_environment_variables = construct_build_environment_variables();
ScryptoCompiler::builder()
.manifest_path(paths.manifest_path.as_path())
.log_level(Level::Trace)
.optimize_with_wasm_opt(None)
.target_directory(paths.coverage_dir_path.as_path())
.envs(build_environment_variables)
.coverage()
.compile()
.map_err(BuildError::ScryptoCompilerError)
.map_err(CoverageError::BuildError)?;
paths.reinitialize_required_directories()?;
test_package(
paths.package_directory_path.as_path(),
self.arguments.clone(),
true,
is_scrypto_cargo_locked_env_var_active() || self.locked,
indexmap! {
"COVERAGE_DIRECTORY" => paths.coverage_data_dir_path.as_path()
},
)
.map_err(CoverageError::TestError)?;
let llvm_ir_file_pre_correction_contents =
std::fs::read_to_string(paths.llvm_ir_pre_corrections_file_path.as_path())?;
let llvm_ir_file_post_correction_contents = LLVM_IR_CORRECTIONS_REGEX
.replace_all(
llvm_ir_file_pre_correction_contents.as_str(),
"${1}start:\n unreachable\n}\n",
)
.to_string();
std::fs::write(
paths.llvm_ir_post_corrections_file_path.as_path(),
llvm_ir_file_post_correction_contents,
)?;
let object_file_conversion_output = llvm_toolchain
.new_clang_command()
.arg(paths.llvm_ir_post_corrections_file_path.as_path())
.arg("-Wno-override-module")
.arg("-c")
.arg("-o")
.arg(paths.object_file_path.as_path())
.arg("--target=aarch64-unknown-linux-gnu")
.output()
.map_err(CoverageError::CommandFailedToRun)?;
if !object_file_conversion_output.status.success() {
let error = String::from_utf8_lossy(&object_file_conversion_output.stderr);
eprintln!("clang failed: {}", error);
return Err(CoverageError::ClangFailed(error.to_string()));
}
let profraw_files_iterator = WalkDir::new(paths.coverage_data_dir_path.as_path())
.into_iter()
.filter_map(|entry| entry.ok())
.map(|entry| entry.into_path())
.filter(|path| {
path.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("profraw"))
});
let profraw_merge_output = llvm_toolchain
.new_llvm_profdata_command()
.arg("merge")
.arg("-sparse")
.args(profraw_files_iterator)
.arg("-o")
.arg(paths.profdata_file_path.as_path())
.output()
.map_err(CoverageError::CommandFailedToRun)?;
if !profraw_merge_output.status.success() {
let error = String::from_utf8_lossy(&profraw_merge_output.stderr);
eprintln!("clang failed: {}", error);
return Err(CoverageError::LlvmProfdataFailed(error.to_string()));
}
let report_generation_output = llvm_toolchain
.new_llvm_cov_command()
.arg("show")
.arg("--instr-profile")
.arg(paths.profdata_file_path.as_path())
.arg(paths.object_file_path.as_path())
.arg("--show-instantiations=false")
.arg("--format=html")
.arg("--output-dir")
.arg(paths.report_dir_path.as_path())
.arg("-sources")
.arg(paths.package_directory_path.as_path())
.output()
.map_err(CoverageError::CommandFailedToRun)?;
if !report_generation_output.status.success() {
let error = String::from_utf8_lossy(&report_generation_output.stderr);
eprintln!("clang failed: {}", error);
return Err(CoverageError::LlvmCovFailed(error.to_string()));
}
Ok(())
}
}
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct Paths {
pub package_directory_path: PathBuf,
pub manifest_path: PathBuf,
pub package_name: String,
pub target_dir_path: PathBuf,
pub coverage_dir_path: PathBuf,
pub coverage_data_dir_path: PathBuf,
pub report_dir_path: PathBuf,
pub build_artifacts_dir_path: PathBuf,
pub file_name: String,
pub wasm_file_name: String,
pub wasm_file_path: PathBuf,
pub wasm_with_schema_file_name: String,
pub wasm_with_schema_file_path: PathBuf,
pub rpd_file_name: String,
pub rpd_file_path: PathBuf,
pub llvm_ir_file_name: String,
pub llvm_ir_pre_corrections_file_path: PathBuf,
pub llvm_ir_post_corrections_file_path: PathBuf,
pub object_file_name: String,
pub object_file_path: PathBuf,
pub profdata_file_name: String,
pub profdata_file_path: PathBuf,
}
impl Paths {
pub fn new(user_provided_path: Option<PathBuf>) -> Result<Self, CoverageError> {
let package_directory_path = user_provided_path
.or(current_dir().ok())
.ok_or(CoverageError::FailedToResolvePackagePath)?
.canonicalize()
.map_err(|_| CoverageError::FailedToResolvePackagePath)?;
let manifest_path = assert_path_exists(package_directory_path.join("Cargo.toml"))?;
let metadata = MetadataCommand::new()
.manifest_path(manifest_path.as_path())
.no_deps()
.exec()
.map_err(CoverageError::CargoMetadataError)?;
let package_name = match metadata.packages.as_slice() {
[Package { name, .. }] => Ok(name.as_str()),
[] => Err(CoverageError::NoPackagesFound),
[..] => Err(CoverageError::WorkspacesNotPermitted),
}?
.to_owned();
let file_name = package_name.replace('-', "_");
let target_dir_path = package_directory_path.join("target");
let coverage_dir_path = target_dir_path.join("coverage");
let report_dir_path = coverage_dir_path.join("report");
let coverage_data_dir_path = coverage_dir_path.join("data");
let build_artifacts_dir_path = coverage_dir_path
.join("wasm32-unknown-unknown")
.join("release");
let wasm_file_name = format!("{file_name}.wasm");
let wasm_file_path = build_artifacts_dir_path.join(wasm_file_name.clone());
let wasm_with_schema_file_name = format!("{file_name}_with_schema.wasm");
let wasm_with_schema_file_path =
build_artifacts_dir_path.join(wasm_with_schema_file_name.clone());
let rpd_file_name = format!("{file_name}.rpd");
let rpd_file_path = build_artifacts_dir_path.join(rpd_file_name.clone());
let llvm_ir_file_name = format!("{file_name}.ll");
let llvm_ir_pre_corrections_file_path = build_artifacts_dir_path
.join("deps")
.join(llvm_ir_file_name.clone());
let llvm_ir_post_corrections_file_path =
build_artifacts_dir_path.join(llvm_ir_file_name.clone());
let object_file_name = format!("{file_name}.o");
let object_file_path = build_artifacts_dir_path.join(object_file_name.clone());
let profdata_file_name = format!("{file_name}.profdata");
let profdata_file_path = coverage_data_dir_path.join(profdata_file_name.clone());
Ok(Self {
package_directory_path,
manifest_path,
package_name,
target_dir_path,
coverage_dir_path,
coverage_data_dir_path,
report_dir_path,
build_artifacts_dir_path,
file_name,
wasm_file_name,
wasm_file_path,
wasm_with_schema_file_name,
wasm_with_schema_file_path,
rpd_file_name,
rpd_file_path,
llvm_ir_file_name,
llvm_ir_pre_corrections_file_path,
llvm_ir_post_corrections_file_path,
object_file_name,
object_file_path,
profdata_file_name,
profdata_file_path,
})
}
pub fn reinitialize_required_directories(&self) -> Result<(), CoverageError> {
let directory_path = self.coverage_data_dir_path.as_path();
let _ = std::fs::remove_dir_all(directory_path);
std::fs::create_dir(directory_path)?;
Ok(())
}
}
struct LLVMToolchain {
clang_path: PathBuf,
llvm_profdata_path: PathBuf,
llvm_cov_path: PathBuf,
}
impl LLVMToolchain {
pub fn new() -> Result<Self, CoverageError> {
static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^LLVM version: (?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$").unwrap()
});
let output = new_nightly_command("rustc")
.arg("-vV")
.stdout(Stdio::piped())
.spawn()
.map_err(CoverageError::CommandFailedToRun)?
.wait_with_output()
.map_err(CoverageError::CommandFailedToRun)?;
let stdout_string = String::from_utf8(output.stdout)?;
let llvm_major_version = VERSION_REGEX
.captures(&stdout_string)
.expect("Can't fail")
.name("major")
.expect("Can't fail")
.as_str()
.parse::<usize>()
.expect("Can't fail");
Ok(Self {
clang_path: select_llvm_command(
["clang".to_string(), format!("clang-{llvm_major_version}")],
llvm_major_version,
)?
.into(),
llvm_profdata_path: select_llvm_command(
[
"llvm-profdata".to_string(),
format!("llvm-profdata-{llvm_major_version}"),
],
llvm_major_version,
)?
.into(),
llvm_cov_path: select_llvm_command(
[
"llvm-cov".to_string(),
format!("llvm-cov-{llvm_major_version}"),
],
llvm_major_version,
)?
.into(),
})
}
pub fn new_clang_command(&self) -> Command {
let mut cmd = Command::new(self.clang_path.as_path());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
cmd
}
pub fn new_llvm_profdata_command(&self) -> Command {
let mut cmd = Command::new(self.llvm_profdata_path.as_path());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
cmd
}
pub fn new_llvm_cov_command(&self) -> Command {
let mut cmd = Command::new(self.llvm_cov_path.as_path());
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
cmd
}
}
#[derive(Debug, thiserror::Error)]
pub enum CoverageError {
#[error("Resolution of the package path failed.")]
FailedToResolvePackagePath,
#[error("This path doesn't exist but it must exist for the coverage tool to work: {0:?}")]
PathDoesntExist(PathBuf),
#[error("Encountered an error when trying to get the cargo metadata for the package: {0}")]
CargoMetadataError(#[from] cargo_metadata::Error),
#[error("The provided package is a workspace which we don't currently support")]
WorkspacesNotPermitted,
#[error("The provided directory doesn't contain any packages")]
NoPackagesFound,
#[error("Command failed to run: {0:?}")]
CommandFailedToRun(std::io::Error),
#[error(
"The data the the command produced on stdout is not a valid utf-8, decoding failed: {0:?}"
)]
StdoutIsNotValidUtf8(#[from] FromUtf8Error),
#[error("A command with the following permitted aliases was not found in the system. Is it available in $PATH?")]
CommandNotFound(Vec<String>),
#[error("An error was encountered when trying to build the package: {0:?}")]
BuildError(BuildError),
#[error("An error was encountered when trying to test the package: {0:?}")]
TestError(TestError),
#[error("An IO error was encountered: {0:?}")]
IoError(#[from] std::io::Error),
#[error("An error was encountered when running the clang command: {0:?}")]
ClangFailed(String),
#[error("An error was encountered when running the llvm-profdata command: {0:?}")]
LlvmProfdataFailed(String),
#[error("An error was encountered when running the llvm-cov command: {0:?}")]
LlvmCovFailed(String),
}
fn assert_path_exists<P: AsRef<Path>>(path: P) -> Result<P, CoverageError> {
if path.as_ref().exists() {
Ok(path)
} else {
Err(CoverageError::PathDoesntExist(path.as_ref().to_path_buf()))
}
}
fn new_nightly_command(program: impl AsRef<OsStr>) -> Command {
let mut command = Command::new(program);
command.env("RUSTUP_TOOLCHAIN", "nightly");
command
}
fn select_llvm_command<P: AsRef<OsStr>>(
commands: impl IntoIterator<Item = P> + Clone,
llvm_major_version: usize,
) -> Result<P, CoverageError> {
let match_string = format!("version {llvm_major_version}");
for command in commands.clone() {
let Ok(output) = new_nightly_command(command.as_ref())
.arg("--version")
.stdout(Stdio::piped())
.spawn()
.map_err(CoverageError::CommandFailedToRun)
.and_then(|child| {
child
.wait_with_output()
.map_err(CoverageError::CommandFailedToRun)
})
else {
continue;
};
let Ok(stdout_string) = String::from_utf8(output.stdout) else {
continue;
};
if stdout_string.contains(match_string.as_str()) {
return Ok(command);
}
}
Err(CoverageError::CommandNotFound(
commands
.into_iter()
.map(|os_str| os_str.as_ref().to_string_lossy().to_string())
.collect(),
))
}
fn construct_build_environment_variables() -> IndexMap<String, String> {
let mut environment_variables = DEFAULT_ENVIRONMENT_VARIABLES
.clone()
.into_iter()
.flat_map(|(k, v)| v.into_set().map(|v| (k, v)))
.collect::<IndexMap<_, _>>();
let rust_flags = RustFlags::for_scrypto_compilation()
.with_flag("-Clto=off")
.with_flag("-Cinstrument-coverage")
.with_flag("-Zno-profiler-runtime")
.with_flag("--emit=llvm-ir")
.with_flag("-Zlocation-detail=none");
for (env_var, cargo_encoding) in [
("RUSTFLAGS", false),
("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS", false),
("CARGO_ENCODED_RUSTFLAGS", true),
] {
let encoded_rust_flags = if cargo_encoding {
rust_flags.encode_as_cargo_encoded_rust_flags()
} else {
rust_flags.encode_as_rust_flags()
};
environment_variables.insert(env_var.to_owned(), encoded_rust_flags);
}
environment_variables.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
environment_variables
}