testlint_sdk/runtime_coverage/
go.rs

1use 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    /// Attach to a running Go process and collect coverage continuously
22    /// REQUIRES: Process must be built with -cover flag
23    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        // Check if process exists
34        self.check_process_exists(pid)?;
35
36        // Try to detect if coverage is enabled
37        self.detect_coverage_support(pid)?;
38
39        // Set environment variable to tell Go to write coverage on signal
40        // Go processes built with -cover respond to SIGINT/SIGTERM by writing coverage
41        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        // Wait for Ctrl+C
46        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        // Signal the process to exit gracefully (triggers coverage write)
68        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            // On Windows, create a trigger file for graceful shutdown
78            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        // Wait for process to write coverage and exit
88        println!("ā³ Waiting for process to write coverage file...");
89        thread::sleep(Duration::from_secs(3));
90
91        // Look for coverage files
92        let coverage_file = self.find_coverage_file(pid)?;
93
94        println!("āœ“ Found coverage file: {}", coverage_file);
95
96        // Parse the coverage file
97        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    /// Check if the process exists
109    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    /// Try to detect if the Go process was built with coverage support
117    #[allow(unused_variables)]
118    fn detect_coverage_support(&self, pid: u32) -> Result<(), String> {
119        // Check command line for -cover flag
120        #[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    /// Find the coverage file written by the Go process
139    fn find_coverage_file(&self, pid: u32) -> Result<String, String> {
140        // Common coverage file names
141        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            // Check current directory for any recent .out files
147        ];
148
149        for file in &possible_files {
150            if Path::new(file).exists() {
151                return Ok(file.clone());
152            }
153        }
154
155        // Search for recent .out files in current directory
156        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                                // Check if file was modified in last 10 seconds
163                                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    /// Parse Go coverage file format
190    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        // Go coverage format:
197        // mode: set (or count, atomic)
198        // file.go:line.col,line.col statements count
199        // example.go:10.2,12.3 2 1
200
201        let mut total_statements = 0;
202        let mut covered_statements = 0;
203
204        for line in content.lines().skip(1) {
205            // Skip the "mode:" line
206            if line.is_empty() || line.starts_with("mode:") {
207                continue;
208            }
209
210            // Parse: file.go:start,end num_statements covered_count
211            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        // Note: Go's coverage is statement-based, not line-based
231        // We report statements as "lines" for consistency
232        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}