testlint_sdk/runtime_coverage/
rust.rs1use crate::platform::{process_exists, signal_process, ProcessSignal};
2use crate::runtime_coverage::{CoverageSummary, RuntimeCoverageResult};
3use std::fs;
4use std::path::Path;
5use std::process::Command;
6use std::thread;
7use std::time::Duration;
8
9pub struct RustRuntimeCoverage;
10
11impl Default for RustRuntimeCoverage {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl RustRuntimeCoverage {
18 pub fn new() -> Self {
19 RustRuntimeCoverage
20 }
21
22 pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
25 println!("š¦ Attaching coverage to Rust process PID: {}", pid);
26 println!("š Collecting coverage continuously (press Ctrl+C to stop)...");
27 println!();
28 println!("ā ļø IMPORTANT: This only works if the Rust binary was built with:");
29 println!(" RUSTFLAGS=\"-C instrument-coverage\" cargo build");
30 println!(" AND");
31 println!(" Process must write profraw data on exit");
32 println!();
33
34 self.check_process_exists(pid)?;
36
37 let binary_path = self.get_binary_path(pid)?;
39 println!("š¦ Binary: {}", binary_path);
40
41 let _profraw_file = format!("rust-coverage-{}.profraw", pid);
43
44 println!("š” Rust coverage via LLVM profiling:");
45 println!(" - Process must be built with -C instrument-coverage");
46 println!(" - Coverage written to .profraw on process exit");
47 println!(" - We'll trigger graceful shutdown to dump coverage");
48 println!();
49
50 use std::sync::atomic::{AtomicBool, Ordering};
52 use std::sync::Arc;
53
54 let running = Arc::new(AtomicBool::new(true));
55 let r = running.clone();
56
57 ctrlc::set_handler(move || {
58 r.store(false, Ordering::SeqCst);
59 })
60 .expect("Error setting Ctrl-C handler");
61
62 let start_time = std::time::Instant::now();
63
64 println!("ā³ Monitoring process... Press Ctrl+C to trigger coverage dump");
65
66 while running.load(Ordering::SeqCst) {
67 thread::sleep(Duration::from_millis(100));
68 }
69
70 let duration_secs = start_time.elapsed().as_secs();
71
72 println!("\nš Sending SIGTERM to process to trigger coverage dump...");
74
75 #[cfg(unix)]
76 {
77 signal_process(pid, ProcessSignal::Terminate)?;
78 }
79
80 #[cfg(windows)]
81 {
82 let trigger_file = std::env::temp_dir().join(format!("coverage_trigger_{}.txt", pid));
84 fs::write(&trigger_file, "terminate")?;
85 println!(
86 "ā¹ļø Created trigger file (Windows alternative to SIGTERM): {}",
87 trigger_file.display()
88 );
89 println!(" Note: Rust process must watch for this file to trigger graceful shutdown");
90 }
91
92 println!("ā³ Waiting for process to write profraw file...");
94 thread::sleep(Duration::from_secs(3));
95
96 let profraw_file = self.find_profraw_file(pid)?;
98 println!("ā Found profraw file: {}", profraw_file);
99
100 let coverage_file = self.convert_to_lcov(&profraw_file, &binary_path, pid)?;
102
103 let summary = self.parse_lcov_file(&coverage_file)?;
105
106 Ok(RuntimeCoverageResult {
107 language: "Rust".to_string(),
108 pid,
109 duration_secs,
110 coverage_file,
111 summary,
112 })
113 }
114
115 fn check_process_exists(&self, pid: u32) -> Result<(), String> {
117 if !process_exists(pid)? {
118 return Err(format!("Process {} not found", pid));
119 }
120 Ok(())
121 }
122
123 fn get_binary_path(&self, pid: u32) -> Result<String, String> {
125 #[cfg(target_os = "linux")]
126 {
127 let exe_path = format!("/proc/{}/exe", pid);
128 match fs::read_link(&exe_path) {
129 Ok(path) => Ok(path.to_string_lossy().to_string()),
130 Err(e) => Err(format!("Failed to read process binary: {}", e)),
131 }
132 }
133
134 #[cfg(target_os = "macos")]
135 {
136 let output = Command::new("ps")
137 .args(["-p", &pid.to_string(), "-o", "comm="])
138 .output()
139 .map_err(|e| format!("Failed to get process info: {}", e))?;
140
141 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
142 }
143
144 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
145 {
146 Err("Binary path detection not supported on this platform".to_string())
147 }
148 }
149
150 fn find_profraw_file(&self, pid: u32) -> Result<String, String> {
152 let possible_files = vec![
154 format!("rust-coverage-{}.profraw", pid),
155 format!("default-{}.profraw", pid),
156 "default.profraw".to_string(),
157 format!("{}.profraw", pid),
158 ];
159
160 for file in &possible_files {
161 if Path::new(file).exists() {
162 return Ok(file.clone());
163 }
164 }
165
166 if let Ok(entries) = fs::read_dir(".") {
168 for entry in entries.flatten() {
169 if let Ok(file_name) = entry.file_name().into_string() {
170 if file_name.ends_with(".profraw") {
171 if let Ok(metadata) = entry.metadata() {
172 if let Ok(modified) = metadata.modified() {
173 if let Ok(elapsed) = modified.elapsed() {
174 if elapsed.as_secs() < 10 {
175 return Ok(file_name);
176 }
177 }
178 }
179 }
180 }
181 }
182 }
183 }
184
185 Err(format!(
186 "Coverage profraw file not found.\n\
187 \n\
188 The Rust process may not have been built with coverage instrumentation.\n\
189 \n\
190 To enable coverage in your Rust application:\n\
191 1. Build with: RUSTFLAGS=\"-C instrument-coverage\" cargo build\n\
192 2. Set LLVM_PROFILE_FILE env var when running:\n\
193 LLVM_PROFILE_FILE=\"rust-coverage-%p.profraw\" ./myapp\n\
194 \n\
195 Or use cargo-llvm-cov: cargo llvm-cov run\n\
196 \n\
197 Checked files: {:?}",
198 possible_files
199 ))
200 }
201
202 fn convert_to_lcov(
204 &self,
205 profraw_file: &str,
206 binary_path: &str,
207 pid: u32,
208 ) -> Result<String, String> {
209 println!("š Converting profraw to lcov format...");
210
211 let profdata_file = format!("rust-coverage-{}.profdata", pid);
213
214 let output = Command::new("llvm-profdata")
215 .args(["merge", "-sparse", profraw_file, "-o", &profdata_file])
216 .output()
217 .map_err(|e| {
218 format!(
219 "Failed to run llvm-profdata (is it installed?): {}\n\
220 Install with: rustup component add llvm-tools-preview",
221 e
222 )
223 })?;
224
225 if !output.status.success() {
226 return Err(format!(
227 "llvm-profdata failed: {}",
228 String::from_utf8_lossy(&output.stderr)
229 ));
230 }
231
232 println!("ā Created profdata file");
233
234 let lcov_file = format!("rust-coverage-{}.lcov", pid);
236
237 let output = Command::new("llvm-cov")
238 .args([
239 "export",
240 binary_path,
241 "-instr-profile",
242 &profdata_file,
243 "-format=lcov",
244 ])
245 .output()
246 .map_err(|e| format!("Failed to run llvm-cov: {}", e))?;
247
248 if !output.status.success() {
249 return Err(format!(
250 "llvm-cov export failed: {}",
251 String::from_utf8_lossy(&output.stderr)
252 ));
253 }
254
255 fs::write(&lcov_file, &output.stdout)
257 .map_err(|e| format!("Failed to write lcov file: {}", e))?;
258
259 println!("ā Converted to lcov format");
260
261 let _ = fs::remove_file(profraw_file);
263 let _ = fs::remove_file(&profdata_file);
264
265 Ok(lcov_file)
266 }
267
268 fn parse_lcov_file(&self, lcov_file: &str) -> Result<CoverageSummary, String> {
270 println!("š Parsing coverage report...");
271
272 let content = fs::read_to_string(lcov_file)
273 .map_err(|e| format!("Failed to read lcov file: {}", e))?;
274
275 let mut total_lines = 0;
276 let mut covered_lines = 0;
277
278 for line in content.lines() {
281 if line.starts_with("DA:") {
282 let parts: Vec<&str> = line.strip_prefix("DA:").unwrap().split(',').collect();
283 if parts.len() >= 2 {
284 if let Ok(count) = parts[1].parse::<usize>() {
285 total_lines += 1;
286 if count > 0 {
287 covered_lines += 1;
288 }
289 }
290 }
291 }
292 }
293
294 if total_lines == 0 {
295 return Err("No coverage data found in lcov file".to_string());
296 }
297
298 let coverage_percentage = (covered_lines as f64 / total_lines as f64) * 100.0;
299
300 Ok(CoverageSummary {
301 total_lines,
302 covered_lines,
303 coverage_percentage,
304 total_branches: None,
305 covered_branches: None,
306 branch_percentage: None,
307 })
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_runtime_coverage_new() {
317 let coverage = RustRuntimeCoverage::new();
318 assert_eq!(std::mem::size_of_val(&coverage), 0);
319 }
320
321 #[test]
322 fn test_runtime_coverage_default() {
323 let coverage = RustRuntimeCoverage;
324 assert_eq!(std::mem::size_of_val(&coverage), 0);
325 }
326}