testlint_sdk/runtime_coverage/
rust.rs

1use 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    /// Attach to a running Rust process and collect coverage continuously
23    /// REQUIRES: Process must be built with LLVM coverage instrumentation
24    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        // Check if process exists
35        self.check_process_exists(pid)?;
36
37        // Detect binary path
38        let binary_path = self.get_binary_path(pid)?;
39        println!("šŸ“¦ Binary: {}", binary_path);
40
41        // Set coverage output path
42        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        // Wait for Ctrl+C
51        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        // Signal the process to exit gracefully
73        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            // On Windows, create a trigger file for graceful shutdown
83            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        // Wait for process to write profraw and exit
93        println!("ā³ Waiting for process to write profraw file...");
94        thread::sleep(Duration::from_secs(3));
95
96        // Find the profraw file
97        let profraw_file = self.find_profraw_file(pid)?;
98        println!("āœ“ Found profraw file: {}", profraw_file);
99
100        // Convert profraw to lcov using llvm-profdata and llvm-cov
101        let coverage_file = self.convert_to_lcov(&profraw_file, &binary_path, pid)?;
102
103        // Parse the lcov file
104        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    /// Check if the process exists
116    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    /// Get the binary path of the running process
124    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    /// Find the profraw file written by the Rust process
151    fn find_profraw_file(&self, pid: u32) -> Result<String, String> {
152        // Common locations for profraw files
153        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        // Search for recent .profraw files
167        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    /// Convert profraw to lcov format using llvm tools
203    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        // First, create profdata file
212        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        // Convert to lcov
235        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        // Write lcov output
256        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        // Clean up intermediate files
262        let _ = fs::remove_file(profraw_file);
263        let _ = fs::remove_file(&profdata_file);
264
265        Ok(lcov_file)
266    }
267
268    /// Parse lcov format coverage file
269    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        // LCOV format:
279        // DA:line,count
280        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}