1use cargo_metadata::MetadataCommand;
12use cargo_metadata::Package;
13use clap::Parser;
14use radix_engine_interface::types::Level;
15use regex::Regex;
16use sbor::prelude::*;
17use scrypto_compiler::is_scrypto_cargo_locked_env_var_active;
18use scrypto_compiler::RustFlags;
19use scrypto_compiler::ScryptoCompiler;
20use scrypto_compiler::DEFAULT_ENVIRONMENT_VARIABLES;
21use std::env::current_dir;
22use std::ffi::OsStr;
23use std::path::Path;
24use std::path::PathBuf;
25use std::process::Command;
26use std::process::Stdio;
27use std::string::FromUtf8Error;
28use std::sync::LazyLock;
29use walkdir::WalkDir;
30
31use crate::utils::*;
32
33#[derive(Parser, Debug)]
35pub struct Coverage {
36 arguments: Vec<String>,
41
42 #[clap(long)]
46 locked: bool,
47
48 #[clap(long)]
50 path: Option<PathBuf>,
51}
52
53impl Coverage {
54 pub fn run(self) -> Result<(), CoverageError> {
55 static LLVM_IR_CORRECTIONS_REGEX: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new(r"(?ms)^(define[^\n]*\n).*?^}\s*$").unwrap());
57
58 let paths = Paths::new(self.path)?;
60
61 let llvm_toolchain = LLVMToolchain::new()?;
64
65 let build_environment_variables = construct_build_environment_variables();
67 ScryptoCompiler::builder()
68 .manifest_path(paths.manifest_path.as_path())
69 .log_level(Level::Trace)
70 .optimize_with_wasm_opt(None)
71 .target_directory(paths.coverage_dir_path.as_path())
72 .envs(build_environment_variables)
73 .coverage()
74 .compile()
75 .map_err(BuildError::ScryptoCompilerError)
76 .map_err(CoverageError::BuildError)?;
77
78 paths.reinitialize_required_directories()?;
80
81 test_package(
83 paths.package_directory_path.as_path(),
84 self.arguments.clone(),
85 true,
86 is_scrypto_cargo_locked_env_var_active() || self.locked,
87 indexmap! {
88 "COVERAGE_DIRECTORY" => paths.coverage_data_dir_path.as_path()
89 },
90 )
91 .map_err(CoverageError::TestError)?;
92
93 let llvm_ir_file_pre_correction_contents =
96 std::fs::read_to_string(paths.llvm_ir_pre_corrections_file_path.as_path())?;
97 let llvm_ir_file_post_correction_contents = LLVM_IR_CORRECTIONS_REGEX
98 .replace_all(
99 llvm_ir_file_pre_correction_contents.as_str(),
100 "${1}start:\n unreachable\n}\n",
101 )
102 .to_string();
103 std::fs::write(
104 paths.llvm_ir_post_corrections_file_path.as_path(),
105 llvm_ir_file_post_correction_contents,
106 )?;
107
108 let object_file_conversion_output = llvm_toolchain
110 .new_clang_command()
111 .arg(paths.llvm_ir_post_corrections_file_path.as_path())
112 .arg("-Wno-override-module")
113 .arg("-c")
114 .arg("-o")
115 .arg(paths.object_file_path.as_path())
116 .arg("--target=aarch64-unknown-linux-gnu")
117 .output()
118 .map_err(CoverageError::CommandFailedToRun)?;
119 if !object_file_conversion_output.status.success() {
120 let error = String::from_utf8_lossy(&object_file_conversion_output.stderr);
121 eprintln!("clang failed: {}", error);
122 return Err(CoverageError::ClangFailed(error.to_string()));
123 }
124
125 let profraw_files_iterator = WalkDir::new(paths.coverage_data_dir_path.as_path())
127 .into_iter()
128 .filter_map(|entry| entry.ok())
129 .map(|entry| entry.into_path())
130 .filter(|path| {
131 path.extension()
132 .is_some_and(|extension| extension.eq_ignore_ascii_case("profraw"))
133 });
134 let profraw_merge_output = llvm_toolchain
135 .new_llvm_profdata_command()
136 .arg("merge")
137 .arg("-sparse")
138 .args(profraw_files_iterator)
139 .arg("-o")
140 .arg(paths.profdata_file_path.as_path())
141 .output()
142 .map_err(CoverageError::CommandFailedToRun)?;
143 if !profraw_merge_output.status.success() {
144 let error = String::from_utf8_lossy(&profraw_merge_output.stderr);
145 eprintln!("clang failed: {}", error);
146 return Err(CoverageError::LlvmProfdataFailed(error.to_string()));
147 }
148
149 let report_generation_output = llvm_toolchain
151 .new_llvm_cov_command()
152 .arg("show")
153 .arg("--instr-profile")
154 .arg(paths.profdata_file_path.as_path())
155 .arg(paths.object_file_path.as_path())
156 .arg("--show-instantiations=false")
157 .arg("--format=html")
158 .arg("--output-dir")
159 .arg(paths.report_dir_path.as_path())
160 .arg("-sources")
161 .arg(paths.package_directory_path.as_path())
162 .output()
163 .map_err(CoverageError::CommandFailedToRun)?;
164 if !report_generation_output.status.success() {
165 let error = String::from_utf8_lossy(&report_generation_output.stderr);
166 eprintln!("clang failed: {}", error);
167 return Err(CoverageError::LlvmCovFailed(error.to_string()));
168 }
169
170 Ok(())
171 }
172}
173
174#[allow(dead_code)]
188#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
189struct Paths {
190 pub package_directory_path: PathBuf,
192 pub manifest_path: PathBuf,
194 pub package_name: String,
197
198 pub target_dir_path: PathBuf,
200 pub coverage_dir_path: PathBuf,
203 pub coverage_data_dir_path: PathBuf,
206 pub report_dir_path: PathBuf,
208 pub build_artifacts_dir_path: PathBuf,
210
211 pub file_name: String,
214
215 pub wasm_file_name: String,
217 pub wasm_file_path: PathBuf,
219
220 pub wasm_with_schema_file_name: String,
222 pub wasm_with_schema_file_path: PathBuf,
224
225 pub rpd_file_name: String,
227 pub rpd_file_path: PathBuf,
229
230 pub llvm_ir_file_name: String,
232 pub llvm_ir_pre_corrections_file_path: PathBuf,
234 pub llvm_ir_post_corrections_file_path: PathBuf,
236
237 pub object_file_name: String,
239 pub object_file_path: PathBuf,
241
242 pub profdata_file_name: String,
244 pub profdata_file_path: PathBuf,
246}
247
248impl Paths {
249 pub fn new(user_provided_path: Option<PathBuf>) -> Result<Self, CoverageError> {
250 let package_directory_path = user_provided_path
253 .or(current_dir().ok())
254 .ok_or(CoverageError::FailedToResolvePackagePath)?
255 .canonicalize()
256 .map_err(|_| CoverageError::FailedToResolvePackagePath)?;
257 let manifest_path = assert_path_exists(package_directory_path.join("Cargo.toml"))?;
258
259 let metadata = MetadataCommand::new()
261 .manifest_path(manifest_path.as_path())
262 .no_deps()
263 .exec()
264 .map_err(CoverageError::CargoMetadataError)?;
265 let package_name = match metadata.packages.as_slice() {
266 [Package { name, .. }] => Ok(name.as_str()),
267 [] => Err(CoverageError::NoPackagesFound),
268 [..] => Err(CoverageError::WorkspacesNotPermitted),
269 }?
270 .to_owned();
271 let file_name = package_name.replace('-', "_");
272
273 let target_dir_path = package_directory_path.join("target");
275 let coverage_dir_path = target_dir_path.join("coverage");
276 let report_dir_path = coverage_dir_path.join("report");
277 let coverage_data_dir_path = coverage_dir_path.join("data");
278 let build_artifacts_dir_path = coverage_dir_path
279 .join("wasm32-unknown-unknown")
280 .join("release");
281
282 let wasm_file_name = format!("{file_name}.wasm");
283 let wasm_file_path = build_artifacts_dir_path.join(wasm_file_name.clone());
284
285 let wasm_with_schema_file_name = format!("{file_name}_with_schema.wasm");
286 let wasm_with_schema_file_path =
287 build_artifacts_dir_path.join(wasm_with_schema_file_name.clone());
288
289 let rpd_file_name = format!("{file_name}.rpd");
290 let rpd_file_path = build_artifacts_dir_path.join(rpd_file_name.clone());
291
292 let llvm_ir_file_name = format!("{file_name}.ll");
293 let llvm_ir_pre_corrections_file_path = build_artifacts_dir_path
294 .join("deps")
295 .join(llvm_ir_file_name.clone());
296 let llvm_ir_post_corrections_file_path =
297 build_artifacts_dir_path.join(llvm_ir_file_name.clone());
298
299 let object_file_name = format!("{file_name}.o");
300 let object_file_path = build_artifacts_dir_path.join(object_file_name.clone());
301
302 let profdata_file_name = format!("{file_name}.profdata");
303 let profdata_file_path = coverage_data_dir_path.join(profdata_file_name.clone());
304
305 Ok(Self {
306 package_directory_path,
307 manifest_path,
308 package_name,
309 target_dir_path,
310 coverage_dir_path,
311 coverage_data_dir_path,
312 report_dir_path,
313 build_artifacts_dir_path,
314 file_name,
315 wasm_file_name,
316 wasm_file_path,
317 wasm_with_schema_file_name,
318 wasm_with_schema_file_path,
319 rpd_file_name,
320 rpd_file_path,
321 llvm_ir_file_name,
322 llvm_ir_pre_corrections_file_path,
323 llvm_ir_post_corrections_file_path,
324 object_file_name,
325 object_file_path,
326 profdata_file_name,
327 profdata_file_path,
328 })
329 }
330
331 pub fn reinitialize_required_directories(&self) -> Result<(), CoverageError> {
333 let directory_path = self.coverage_data_dir_path.as_path();
334 let _ = std::fs::remove_dir_all(directory_path);
335 std::fs::create_dir(directory_path)?;
336 Ok(())
337 }
338}
339
340struct LLVMToolchain {
352 clang_path: PathBuf,
354 llvm_profdata_path: PathBuf,
356 llvm_cov_path: PathBuf,
358}
359
360impl LLVMToolchain {
361 pub fn new() -> Result<Self, CoverageError> {
362 static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
363 Regex::new(r"(?m)^LLVM version: (?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$").unwrap()
364 });
365
366 let output = new_nightly_command("rustc")
368 .arg("-vV")
369 .stdout(Stdio::piped())
370 .spawn()
371 .map_err(CoverageError::CommandFailedToRun)?
372 .wait_with_output()
373 .map_err(CoverageError::CommandFailedToRun)?;
374 let stdout_string = String::from_utf8(output.stdout)?;
375
376 let llvm_major_version = VERSION_REGEX
380 .captures(&stdout_string)
381 .expect("Can't fail")
382 .name("major")
383 .expect("Can't fail")
384 .as_str()
385 .parse::<usize>()
386 .expect("Can't fail");
387
388 Ok(Self {
389 clang_path: select_llvm_command(
390 ["clang".to_string(), format!("clang-{llvm_major_version}")],
391 llvm_major_version,
392 )?
393 .into(),
394 llvm_profdata_path: select_llvm_command(
395 [
396 "llvm-profdata".to_string(),
397 format!("llvm-profdata-{llvm_major_version}"),
398 ],
399 llvm_major_version,
400 )?
401 .into(),
402 llvm_cov_path: select_llvm_command(
403 [
404 "llvm-cov".to_string(),
405 format!("llvm-cov-{llvm_major_version}"),
406 ],
407 llvm_major_version,
408 )?
409 .into(),
410 })
411 }
412
413 pub fn new_clang_command(&self) -> Command {
415 let mut cmd = Command::new(self.clang_path.as_path());
416 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
417 cmd
418 }
419
420 pub fn new_llvm_profdata_command(&self) -> Command {
422 let mut cmd = Command::new(self.llvm_profdata_path.as_path());
423 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
424 cmd
425 }
426
427 pub fn new_llvm_cov_command(&self) -> Command {
429 let mut cmd = Command::new(self.llvm_cov_path.as_path());
430 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
431 cmd
432 }
433}
434
435#[derive(Debug, thiserror::Error)]
437pub enum CoverageError {
438 #[error("Resolution of the package path failed.")]
442 FailedToResolvePackagePath,
443
444 #[error("This path doesn't exist but it must exist for the coverage tool to work: {0:?}")]
446 PathDoesntExist(PathBuf),
447
448 #[error("Encountered an error when trying to get the cargo metadata for the package: {0}")]
450 CargoMetadataError(#[from] cargo_metadata::Error),
451
452 #[error("The provided package is a workspace which we don't currently support")]
455 WorkspacesNotPermitted,
456
457 #[error("The provided directory doesn't contain any packages")]
460 NoPackagesFound,
461
462 #[error("Command failed to run: {0:?}")]
464 CommandFailedToRun(std::io::Error),
465
466 #[error(
468 "The data the the command produced on stdout is not a valid utf-8, decoding failed: {0:?}"
469 )]
470 StdoutIsNotValidUtf8(#[from] FromUtf8Error),
471
472 #[error("A command with the following permitted aliases was not found in the system. Is it available in $PATH?")]
474 CommandNotFound(Vec<String>),
475
476 #[error("An error was encountered when trying to build the package: {0:?}")]
478 BuildError(BuildError),
479
480 #[error("An error was encountered when trying to test the package: {0:?}")]
482 TestError(TestError),
483
484 #[error("An IO error was encountered: {0:?}")]
486 IoError(#[from] std::io::Error),
487
488 #[error("An error was encountered when running the clang command: {0:?}")]
490 ClangFailed(String),
491
492 #[error("An error was encountered when running the llvm-profdata command: {0:?}")]
494 LlvmProfdataFailed(String),
495
496 #[error("An error was encountered when running the llvm-cov command: {0:?}")]
498 LlvmCovFailed(String),
499}
500
501fn assert_path_exists<P: AsRef<Path>>(path: P) -> Result<P, CoverageError> {
503 if path.as_ref().exists() {
504 Ok(path)
505 } else {
506 Err(CoverageError::PathDoesntExist(path.as_ref().to_path_buf()))
507 }
508}
509
510fn new_nightly_command(program: impl AsRef<OsStr>) -> Command {
514 let mut command = Command::new(program);
515 command.env("RUSTUP_TOOLCHAIN", "nightly");
516 command
517}
518
519fn select_llvm_command<P: AsRef<OsStr>>(
523 commands: impl IntoIterator<Item = P> + Clone,
524 llvm_major_version: usize,
525) -> Result<P, CoverageError> {
526 let match_string = format!("version {llvm_major_version}");
527 for command in commands.clone() {
528 let Ok(output) = new_nightly_command(command.as_ref())
529 .arg("--version")
530 .stdout(Stdio::piped())
531 .spawn()
532 .map_err(CoverageError::CommandFailedToRun)
533 .and_then(|child| {
534 child
535 .wait_with_output()
536 .map_err(CoverageError::CommandFailedToRun)
537 })
538 else {
539 continue;
540 };
541 let Ok(stdout_string) = String::from_utf8(output.stdout) else {
542 continue;
543 };
544 if stdout_string.contains(match_string.as_str()) {
545 return Ok(command);
546 }
547 }
548 Err(CoverageError::CommandNotFound(
549 commands
550 .into_iter()
551 .map(|os_str| os_str.as_ref().to_string_lossy().to_string())
552 .collect(),
553 ))
554}
555
556fn construct_build_environment_variables() -> IndexMap<String, String> {
559 let mut environment_variables = DEFAULT_ENVIRONMENT_VARIABLES
560 .clone()
561 .into_iter()
562 .flat_map(|(k, v)| v.into_set().map(|v| (k, v)))
563 .collect::<IndexMap<_, _>>();
564 let rust_flags = RustFlags::for_scrypto_compilation()
565 .with_flag("-Clto=off")
566 .with_flag("-Cinstrument-coverage")
567 .with_flag("-Zno-profiler-runtime")
568 .with_flag("--emit=llvm-ir")
569 .with_flag("-Zlocation-detail=none");
570 for (env_var, cargo_encoding) in [
571 ("RUSTFLAGS", false),
572 ("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS", false),
573 ("CARGO_ENCODED_RUSTFLAGS", true),
574 ] {
575 let encoded_rust_flags = if cargo_encoding {
576 rust_flags.encode_as_cargo_encoded_rust_flags()
577 } else {
578 rust_flags.encode_as_rust_flags()
579 };
580 environment_variables.insert(env_var.to_owned(), encoded_rust_flags);
581 }
582 environment_variables.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
583 environment_variables
584}