testlint_sdk/runtime_coverage/
go.rs1use crate::platform::{process_exists, signal_process, ProcessSignal};
2use crate::runtime_coverage::{CoverageSummary, RuntimeCoverageResult};
3use std::fs;
4use std::path::Path;
5use std::thread;
6use std::time::Duration;
7
8pub struct GoRuntimeCoverage;
9
10impl Default for GoRuntimeCoverage {
11 fn default() -> Self {
12 Self::new()
13 }
14}
15
16impl GoRuntimeCoverage {
17 pub fn new() -> Self {
18 GoRuntimeCoverage
19 }
20
21 pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
24 println!("š¹ Attaching coverage to Go process PID: {}", pid);
25 println!("š Collecting coverage continuously (press Ctrl+C to stop)...");
26 println!();
27 println!("ā ļø IMPORTANT: This only works if the Go process was built with:");
28 println!(" go build -cover -o myapp");
29 println!(" or");
30 println!(" go run -cover myapp.go");
31 println!();
32
33 self.check_process_exists(pid)?;
35
36 self.detect_coverage_support(pid)?;
38
39 println!("š” Go coverage works by signaling the process to dump coverage.");
42 println!(" The process must be built with -cover and -coverprofile flags.");
43 println!();
44
45 use std::sync::atomic::{AtomicBool, Ordering};
47 use std::sync::Arc;
48
49 let running = Arc::new(AtomicBool::new(true));
50 let r = running.clone();
51
52 ctrlc::set_handler(move || {
53 r.store(false, Ordering::SeqCst);
54 })
55 .expect("Error setting Ctrl-C handler");
56
57 let start_time = std::time::Instant::now();
58
59 println!("ā³ Monitoring process... Press Ctrl+C to trigger coverage dump");
60
61 while running.load(Ordering::SeqCst) {
62 thread::sleep(Duration::from_millis(100));
63 }
64
65 let duration_secs = start_time.elapsed().as_secs();
66
67 println!("\nš Sending SIGINT to process to trigger coverage dump...");
69
70 #[cfg(unix)]
71 {
72 signal_process(pid, ProcessSignal::Interrupt)?;
73 }
74
75 #[cfg(windows)]
76 {
77 let trigger_file = std::env::temp_dir().join(format!("coverage_trigger_{}.txt", pid));
79 fs::write(&trigger_file, "dump")?;
80 println!(
81 "ā¹ļø Created trigger file (Windows alternative to SIGINT): {}",
82 trigger_file.display()
83 );
84 println!(" Note: Go process must watch for this file to trigger coverage dump");
85 }
86
87 println!("ā³ Waiting for process to write coverage file...");
89 thread::sleep(Duration::from_secs(3));
90
91 let coverage_file = self.find_coverage_file(pid)?;
93
94 println!("ā Found coverage file: {}", coverage_file);
95
96 let summary = self.parse_coverage_file(&coverage_file)?;
98
99 Ok(RuntimeCoverageResult {
100 language: "Go".to_string(),
101 pid,
102 duration_secs,
103 coverage_file,
104 summary,
105 })
106 }
107
108 fn check_process_exists(&self, pid: u32) -> Result<(), String> {
110 if !process_exists(pid)? {
111 return Err(format!("Process {} not found", pid));
112 }
113 Ok(())
114 }
115
116 #[allow(unused_variables)]
118 fn detect_coverage_support(&self, pid: u32) -> Result<(), String> {
119 #[cfg(target_os = "linux")]
121 {
122 let cmdline_path = format!("/proc/{}/cmdline", pid);
123 if let Ok(cmdline) = fs::read_to_string(&cmdline_path) {
124 if cmdline.contains("-cover") || cmdline.contains("coverprofile") {
125 println!("ā Process appears to be built with coverage support");
126 return Ok(());
127 }
128 }
129 }
130
131 println!("ā ļø Could not verify if process has coverage enabled.");
132 println!(" If the process was NOT built with -cover, this will not work.");
133 println!();
134
135 Ok(())
136 }
137
138 fn find_coverage_file(&self, pid: u32) -> Result<String, String> {
140 let possible_files = vec![
142 format!("coverage-{}.out", pid),
143 "coverage.out".to_string(),
144 format!("cover-{}.out", pid),
145 "cover.out".to_string(),
146 ];
148
149 for file in &possible_files {
150 if Path::new(file).exists() {
151 return Ok(file.clone());
152 }
153 }
154
155 if let Ok(entries) = fs::read_dir(".") {
157 for entry in entries.flatten() {
158 if let Ok(file_name) = entry.file_name().into_string() {
159 if file_name.ends_with(".out") && file_name.contains("cov") {
160 if let Ok(metadata) = entry.metadata() {
161 if let Ok(modified) = metadata.modified() {
162 if let Ok(elapsed) = modified.elapsed() {
164 if elapsed.as_secs() < 10 {
165 return Ok(file_name);
166 }
167 }
168 }
169 }
170 }
171 }
172 }
173 }
174
175 Err(format!(
176 "Coverage file not found. \n\
177 \n\
178 The Go process may not have been built with coverage support.\n\
179 \n\
180 To enable coverage in your Go application:\n\
181 1. Build with: go build -cover -coverprofile=coverage.out -o myapp\n\
182 2. Or run with: go run -cover -coverprofile=coverage.out myapp.go\n\
183 \n\
184 Common coverage file names checked: {:?}",
185 possible_files
186 ))
187 }
188
189 fn parse_coverage_file(&self, coverage_file: &str) -> Result<CoverageSummary, String> {
191 println!("š Parsing Go coverage file...");
192
193 let content = fs::read_to_string(coverage_file)
194 .map_err(|e| format!("Failed to read coverage file: {}", e))?;
195
196 let mut total_statements = 0;
202 let mut covered_statements = 0;
203
204 for line in content.lines().skip(1) {
205 if line.is_empty() || line.starts_with("mode:") {
207 continue;
208 }
209
210 let parts: Vec<&str> = line.split_whitespace().collect();
212 if parts.len() >= 3 {
213 if let Ok(num_stmts) = parts[1].parse::<usize>() {
214 if let Ok(count) = parts[2].parse::<usize>() {
215 total_statements += num_stmts;
216 if count > 0 {
217 covered_statements += num_stmts;
218 }
219 }
220 }
221 }
222 }
223
224 if total_statements == 0 {
225 return Err("No coverage data found in file".to_string());
226 }
227
228 let coverage_percentage = (covered_statements as f64 / total_statements as f64) * 100.0;
229
230 Ok(CoverageSummary {
233 total_lines: total_statements,
234 covered_lines: covered_statements,
235 coverage_percentage,
236 total_branches: None,
237 covered_branches: None,
238 branch_percentage: None,
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_runtime_coverage_new() {
249 let coverage = GoRuntimeCoverage::new();
250 assert_eq!(std::mem::size_of_val(&coverage), 0);
251 }
252
253 #[test]
254 fn test_runtime_coverage_default() {
255 let coverage = GoRuntimeCoverage;
256 assert_eq!(std::mem::size_of_val(&coverage), 0);
257 }
258}