testlint_sdk/runtime_coverage/
cpp.rs

1use crate::platform::{process_exists, signal_process, ProcessSignal};
2use crate::runtime_coverage::{CoverageSummary, RuntimeCoverageResult};
3use std::fs;
4use std::process::Command;
5use std::thread;
6use std::time::Duration;
7
8pub struct CppRuntimeCoverage;
9
10impl Default for CppRuntimeCoverage {
11    fn default() -> Self {
12        Self::new()
13    }
14}
15
16impl CppRuntimeCoverage {
17    pub fn new() -> Self {
18        CppRuntimeCoverage
19    }
20
21    /// Attach to a running C++ process and collect coverage
22    /// REQUIRES: Process must be built with -fprofile-arcs -ftest-coverage
23    pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
24        println!(
25            "āš™ļø  Attempting to collect coverage from C++ process PID: {}",
26            pid
27        );
28        println!();
29        println!("āš ļø  IMPORTANT: This only works if the C++ binary was compiled with:");
30        println!("   g++ -fprofile-arcs -ftest-coverage myapp.cpp -o myapp");
31        println!("   or");
32        println!("   clang++ -fprofile-instr-generate -fcoverage-mapping myapp.cpp -o myapp");
33        println!();
34
35        // Check if process exists
36        self.check_process_exists(pid)?;
37
38        // Detect binary path
39        let binary_path = self.get_binary_path(pid)?;
40        println!("šŸ“¦ Binary: {}", binary_path);
41
42        println!("šŸ’” C++ coverage via gcov/llvm-cov:");
43        println!("   - Process must be built with coverage flags");
44        println!("   - .gcda files written on process exit");
45        println!("   - We'll monitor for coverage files after signal");
46        println!();
47
48        // Wait for Ctrl+C
49        use std::sync::atomic::{AtomicBool, Ordering};
50        use std::sync::Arc;
51
52        let running = Arc::new(AtomicBool::new(true));
53        let r = running.clone();
54
55        ctrlc::set_handler(move || {
56            r.store(false, Ordering::SeqCst);
57        })
58        .expect("Error setting Ctrl-C handler");
59
60        let start_time = std::time::Instant::now();
61
62        println!("ā³ Monitoring process... Press Ctrl+C to trigger coverage dump");
63
64        while running.load(Ordering::SeqCst) {
65            thread::sleep(Duration::from_millis(100));
66        }
67
68        let duration_secs = start_time.elapsed().as_secs();
69
70        // Signal the process to exit gracefully (this writes .gcda files)
71        println!("\nšŸ“ Sending SIGTERM to process to trigger gcov data write...");
72
73        #[cfg(unix)]
74        {
75            signal_process(pid, ProcessSignal::Terminate)?;
76        }
77
78        #[cfg(windows)]
79        {
80            // On Windows, create a trigger file for graceful shutdown
81            let trigger_file = std::env::temp_dir().join(format!("coverage_trigger_{}.txt", pid));
82            fs::write(&trigger_file, "terminate")?;
83            println!(
84                "ā„¹ļø  Created trigger file (Windows alternative to SIGTERM): {}",
85                trigger_file.display()
86            );
87            println!("   Note: C++ process must watch for this file to trigger graceful shutdown");
88        }
89
90        // Wait for process to write .gcda files and exit
91        println!("ā³ Waiting for process to write coverage data...");
92        thread::sleep(Duration::from_secs(3));
93
94        // Find .gcda files
95        let gcda_files = self.find_gcda_files()?;
96
97        if gcda_files.is_empty() {
98            return Err(self.get_coverage_instructions());
99        }
100
101        println!("āœ“ Found {} .gcda file(s)", gcda_files.len());
102
103        // Generate lcov report
104        let coverage_file = self.generate_lcov_report(&gcda_files, pid)?;
105
106        // Parse the lcov file
107        let summary = self.parse_lcov_file(&coverage_file)?;
108
109        Ok(RuntimeCoverageResult {
110            language: "C++".to_string(),
111            pid,
112            duration_secs,
113            coverage_file,
114            summary,
115        })
116    }
117
118    /// Check if the process exists
119    fn check_process_exists(&self, pid: u32) -> Result<(), String> {
120        if !process_exists(pid)? {
121            return Err(format!("Process {} not found", pid));
122        }
123        Ok(())
124    }
125
126    /// Get the binary path of the running process
127    fn get_binary_path(&self, pid: u32) -> Result<String, String> {
128        #[cfg(target_os = "linux")]
129        {
130            let exe_path = format!("/proc/{}/exe", pid);
131            match fs::read_link(&exe_path) {
132                Ok(path) => Ok(path.to_string_lossy().to_string()),
133                Err(e) => Err(format!("Failed to read process binary: {}", e)),
134            }
135        }
136
137        #[cfg(target_os = "macos")]
138        {
139            let output = Command::new("ps")
140                .args(["-p", &pid.to_string(), "-o", "comm="])
141                .output()
142                .map_err(|e| format!("Failed to get process info: {}", e))?;
143
144            Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
145        }
146
147        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
148        {
149            Err("Binary path detection not supported on this platform".to_string())
150        }
151    }
152
153    /// Find .gcda files generated by the process
154    fn find_gcda_files(&self) -> Result<Vec<String>, String> {
155        let mut gcda_files = Vec::new();
156
157        // Search current directory and subdirectories
158        if let Ok(entries) = fs::read_dir(".") {
159            for entry in entries.flatten() {
160                if let Ok(file_name) = entry.file_name().into_string() {
161                    if file_name.ends_with(".gcda") {
162                        // Check if file was recently modified
163                        if let Ok(metadata) = entry.metadata() {
164                            if let Ok(modified) = metadata.modified() {
165                                if let Ok(elapsed) = modified.elapsed() {
166                                    if elapsed.as_secs() < 10 {
167                                        gcda_files.push(file_name);
168                                    }
169                                }
170                            }
171                        }
172                    }
173                }
174            }
175        }
176
177        Ok(gcda_files)
178    }
179
180    /// Get instructions for building with coverage
181    fn get_coverage_instructions(&self) -> String {
182        "No .gcda coverage files found.\n\
183        \n\
184        The C++ binary was likely NOT compiled with coverage flags.\n\
185        \n\
186        To enable coverage in C++ applications:\n\
187        \n\
188        Using GCC/gcov:\n\
189        1. Compile with coverage flags:\n\
190           g++ -fprofile-arcs -ftest-coverage -O0 myapp.cpp -o myapp\n\
191        \n\
192        2. Run your application:\n\
193           ./myapp\n\
194        \n\
195        3. .gcda files are written on exit\n\
196        \n\
197        Using Clang/llvm-cov:\n\
198        1. Compile with:\n\
199           clang++ -fprofile-instr-generate -fcoverage-mapping myapp.cpp -o myapp\n\
200        \n\
201        2. Set profile output:\n\
202           export LLVM_PROFILE_FILE=\"cpp-%p.profraw\"\n\
203        \n\
204        3. Run application\n\
205        \n\
206        Or use Coverage Orchestrator to wrap execution automatically."
207            .to_string()
208    }
209
210    /// Generate lcov report from .gcda files
211    fn generate_lcov_report(&self, _gcda_files: &[String], pid: u32) -> Result<String, String> {
212        println!("šŸ“„ Generating lcov report from gcda files...");
213
214        let lcov_file = format!("cpp-coverage-{}.lcov", pid);
215
216        // Run lcov to capture coverage
217        let output = Command::new("lcov")
218            .args([
219                "--capture",
220                "--directory",
221                ".",
222                "--output-file",
223                &lcov_file,
224                "--rc",
225                "lcov_branch_coverage=1",
226            ])
227            .output()
228            .map_err(|e| {
229                format!(
230                    "Failed to run lcov (is it installed?): {}\n\
231                Install with: apt-get install lcov  or  brew install lcov",
232                    e
233                )
234            })?;
235
236        if !output.status.success() {
237            return Err(format!(
238                "lcov failed: {}",
239                String::from_utf8_lossy(&output.stderr)
240            ));
241        }
242
243        println!("āœ“ Generated lcov report");
244
245        Ok(lcov_file)
246    }
247
248    /// Parse lcov format coverage file
249    fn parse_lcov_file(&self, lcov_file: &str) -> Result<CoverageSummary, String> {
250        println!("šŸ“Š Parsing coverage report...");
251
252        let content = fs::read_to_string(lcov_file)
253            .map_err(|e| format!("Failed to read lcov file: {}", e))?;
254
255        let mut total_lines = 0;
256        let mut covered_lines = 0;
257        let mut total_branches = 0;
258        let mut covered_branches = 0;
259
260        // LCOV format:
261        // DA:line,count (line coverage)
262        // BRDA:line,block,branch,taken (branch coverage)
263        for line in content.lines() {
264            if line.starts_with("DA:") {
265                let parts: Vec<&str> = line.strip_prefix("DA:").unwrap().split(',').collect();
266                if parts.len() >= 2 {
267                    if let Ok(count) = parts[1].parse::<usize>() {
268                        total_lines += 1;
269                        if count > 0 {
270                            covered_lines += 1;
271                        }
272                    }
273                }
274            } else if line.starts_with("BRDA:") {
275                let parts: Vec<&str> = line.strip_prefix("BRDA:").unwrap().split(',').collect();
276                if parts.len() >= 4 {
277                    total_branches += 1;
278                    if parts[3] != "-" && parts[3] != "0" {
279                        covered_branches += 1;
280                    }
281                }
282            }
283        }
284
285        if total_lines == 0 {
286            return Err("No coverage data found in lcov file".to_string());
287        }
288
289        let coverage_percentage = (covered_lines as f64 / total_lines as f64) * 100.0;
290
291        let branch_percentage = if total_branches > 0 {
292            Some((covered_branches as f64 / total_branches as f64) * 100.0)
293        } else {
294            None
295        };
296
297        Ok(CoverageSummary {
298            total_lines,
299            covered_lines,
300            coverage_percentage,
301            total_branches: if total_branches > 0 {
302                Some(total_branches)
303            } else {
304                None
305            },
306            covered_branches: if total_branches > 0 {
307                Some(covered_branches)
308            } else {
309                None
310            },
311            branch_percentage,
312        })
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_runtime_coverage_new() {
322        let coverage = CppRuntimeCoverage::new();
323        assert_eq!(std::mem::size_of_val(&coverage), 0);
324    }
325
326    #[test]
327    fn test_runtime_coverage_default() {
328        let coverage = CppRuntimeCoverage;
329        assert_eq!(std::mem::size_of_val(&coverage), 0);
330    }
331}