testlint_sdk/runtime_coverage/
cpp.rs1use 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 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 self.check_process_exists(pid)?;
37
38 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 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 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 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 println!("ā³ Waiting for process to write coverage data...");
92 thread::sleep(Duration::from_secs(3));
93
94 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 let coverage_file = self.generate_lcov_report(&gcda_files, pid)?;
105
106 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 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 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 fn find_gcda_files(&self) -> Result<Vec<String>, String> {
155 let mut gcda_files = Vec::new();
156
157 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 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 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 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 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 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 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}