coverage.rs (4954B)
1 use std::{fs, path::Path, process::Command}; 2 3 use crate::{ 4 check, 5 coverage_policy::{CoverageContract, evaluate_report, validate_contract}, 6 fs::workspace_root, 7 generate, wasm, 8 }; 9 10 pub fn run(args: &[String]) -> Result<(), String> { 11 match args { 12 [command] if command == "run" => run_coverage(), 13 [] => Err(usage()), 14 _ => Err(usage()), 15 } 16 } 17 18 fn usage() -> String { 19 "usage: cargo xtask coverage run".to_owned() 20 } 21 22 fn run_coverage() -> Result<(), String> { 23 let root = workspace_root()?; 24 let contract = load_contract(&root)?; 25 validate_contract(&contract)?; 26 preflight(&contract)?; 27 generate::generate_ts()?; 28 wasm::generate(&[])?; 29 check::check()?; 30 clean_report_output(&root, &contract)?; 31 run_llvm_cov(&root, &contract)?; 32 evaluate_report(&root, &root.join(&contract.report.output), &contract) 33 } 34 35 fn load_contract(root: &Path) -> Result<CoverageContract, String> { 36 let path = root.join("contracts").join("coverage.toml"); 37 let raw = fs::read_to_string(&path) 38 .map_err(|error| format!("failed to read {}: {error}", path.display()))?; 39 toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) 40 } 41 42 fn preflight(contract: &CoverageContract) -> Result<(), String> { 43 require_command("rustup", &["--version"], "install rustup")?; 44 require_command( 45 "rustup", 46 &[ 47 "run", 48 &contract.toolchain.rust, 49 "cargo", 50 "llvm-cov", 51 "--version", 52 ], 53 "install cargo-llvm-cov for the SDK Rust toolchain", 54 )?; 55 let component_output = output( 56 "rustup", 57 &["component", "list", "--toolchain", &contract.toolchain.rust], 58 ) 59 .map_err(|error| format!("failed to inspect Rust components: {error}"))?; 60 if !component_output 61 .lines() 62 .any(|line| line.starts_with("llvm-tools") && line.contains("(installed)")) 63 { 64 return Err(format!( 65 "missing llvm-tools-preview for Rust toolchain {}; run `rustup component add llvm-tools-preview --toolchain {}`", 66 contract.toolchain.rust, contract.toolchain.rust 67 )); 68 } 69 let target_output = output( 70 "rustup", 71 &["target", "list", "--toolchain", &contract.toolchain.rust], 72 ) 73 .map_err(|error| format!("failed to inspect Rust targets: {error}"))?; 74 let expected_target = format!("{} (installed)", contract.toolchain.wasm_target); 75 if !target_output.lines().any(|line| line == expected_target) { 76 return Err(format!( 77 "missing Rust target {}; run `rustup target add {} --toolchain {}`", 78 contract.toolchain.wasm_target, contract.toolchain.wasm_target, contract.toolchain.rust 79 )); 80 } 81 require_command("wasm-pack", &["--version"], "install wasm-pack")?; 82 require_command("pnpm", &["--version"], "install pnpm")?; 83 Ok(()) 84 } 85 86 fn require_command(command: &str, args: &[&str], install_hint: &str) -> Result<(), String> { 87 output(command, args) 88 .map(|_| ()) 89 .map_err(|error| format!("missing {command}: {error}; {install_hint}")) 90 } 91 92 fn output(command: &str, args: &[&str]) -> Result<String, String> { 93 let output = Command::new(command) 94 .args(args) 95 .output() 96 .map_err(|error| error.to_string())?; 97 if !output.status.success() { 98 return Err(format!( 99 "{}", 100 String::from_utf8_lossy(&output.stderr).trim() 101 )); 102 } 103 Ok(String::from_utf8_lossy(&output.stdout).to_string()) 104 } 105 106 fn clean_report_output(root: &Path, contract: &CoverageContract) -> Result<(), String> { 107 let output_path = root.join(&contract.report.output); 108 if let Some(parent) = output_path.parent() 109 && parent.exists() 110 { 111 fs::remove_dir_all(parent) 112 .map_err(|error| format!("failed to remove {}: {error}", parent.display()))?; 113 } 114 Ok(()) 115 } 116 117 fn run_llvm_cov(root: &Path, contract: &CoverageContract) -> Result<(), String> { 118 let output_path = root.join(&contract.report.output); 119 if let Some(parent) = output_path.parent() { 120 fs::create_dir_all(parent) 121 .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; 122 } 123 let status = Command::new("rustup") 124 .current_dir(root) 125 .args([ 126 "run", 127 &contract.toolchain.rust, 128 "cargo", 129 "llvm-cov", 130 "--workspace", 131 "--all-features", 132 "--summary-only", 133 "--json", 134 "--output-path", 135 &contract.report.output, 136 "--ignore-filename-regex", 137 &contract.report.ignore_filename_regex, 138 "--no-fail-fast", 139 ]) 140 .status() 141 .map_err(|error| format!("failed to run cargo llvm-cov: {error}"))?; 142 if !status.success() { 143 return Err(format!("cargo llvm-cov failed with status {status}")); 144 } 145 Ok(()) 146 }