testlint_sdk/runtime_coverage/
csharp.rs

1use crate::platform::{get_process_name, process_exists};
2use crate::runtime_coverage::{CoverageSummary, RuntimeCoverageResult};
3use quick_xml::events::Event;
4use quick_xml::Reader;
5use std::fs;
6use std::path::Path;
7use std::process::Command;
8use std::thread;
9use std::time::Duration;
10
11pub struct CSharpRuntimeCoverage;
12
13impl Default for CSharpRuntimeCoverage {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl CSharpRuntimeCoverage {
20    pub fn new() -> Self {
21        CSharpRuntimeCoverage
22    }
23
24    /// Attach coverage to a running .NET process
25    pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
26        println!("šŸ”· Attaching coverage to .NET process PID: {}", pid);
27        println!("šŸ“Š Collecting coverage continuously (press Ctrl+C to stop)...");
28
29        // Check if process exists and is .NET
30        self.check_dotnet_process(pid)?;
31
32        // Ensure dotnet-coverage tool is installed
33        self.ensure_dotnet_coverage()?;
34
35        // Start collecting coverage
36        println!("šŸ’‰ Starting coverage collection...");
37        let coverage_file = format!("coverage-{}.cobertura.xml", pid);
38
39        let mut cmd = Command::new("dotnet-coverage");
40        cmd.args([
41            "collect",
42            "--process-id",
43            &pid.to_string(),
44            "--output",
45            &coverage_file,
46            "--output-format",
47            "cobertura",
48        ]);
49
50        // Start coverage collection in background
51        let mut child = cmd
52            .spawn()
53            .map_err(|e| format!("Failed to start dotnet-coverage: {}", e))?;
54
55        // Wait for Ctrl+C
56        use std::sync::atomic::{AtomicBool, Ordering};
57        use std::sync::Arc;
58
59        let running = Arc::new(AtomicBool::new(true));
60        let r = running.clone();
61
62        ctrlc::set_handler(move || {
63            r.store(false, Ordering::SeqCst);
64        })
65        .expect("Error setting Ctrl-C handler");
66
67        let start_time = std::time::Instant::now();
68
69        println!("ā³ Collecting coverage... Press Ctrl+C to stop");
70
71        while running.load(Ordering::SeqCst) {
72            thread::sleep(Duration::from_millis(100));
73        }
74
75        let duration_secs = start_time.elapsed().as_secs();
76
77        // Stop coverage collection
78        println!("\nšŸ“ Stopping coverage collection...");
79        child
80            .kill()
81            .map_err(|e| format!("Failed to stop coverage: {}", e))?;
82
83        // Wait for process to finish writing
84        thread::sleep(Duration::from_secs(2));
85
86        // Check if coverage file was created
87        if !Path::new(&coverage_file).exists() {
88            return Err(format!("Coverage file {} not found", coverage_file));
89        }
90
91        // Parse coverage summary
92        let summary = self.parse_cobertura_xml(&coverage_file)?;
93
94        Ok(RuntimeCoverageResult {
95            language: "C#".to_string(),
96            pid,
97            duration_secs,
98            coverage_file,
99            summary,
100        })
101    }
102
103    /// Check if process is a .NET process
104    fn check_dotnet_process(&self, pid: u32) -> Result<(), String> {
105        if !process_exists(pid)? {
106            return Err(format!("Process {} not found", pid));
107        }
108
109        let process_name = get_process_name(pid)?;
110        let process_name_lower = process_name.to_lowercase();
111        if !process_name_lower.contains("dotnet") && !process_name_lower.contains(".net") {
112            println!("Warning: Process {} may not be a .NET process", pid);
113        }
114
115        Ok(())
116    }
117
118    /// Ensure dotnet-coverage tool is installed
119    fn ensure_dotnet_coverage(&self) -> Result<(), String> {
120        // Check if dotnet-coverage is installed
121        let check = Command::new("dotnet-coverage").arg("--version").output();
122
123        if check.is_ok() && check.unwrap().status.success() {
124            return Ok(());
125        }
126
127        println!("šŸ“„ Installing dotnet-coverage...");
128
129        let output = Command::new("dotnet")
130            .args(["tool", "install", "-g", "dotnet-coverage"])
131            .output()
132            .map_err(|e| format!("Failed to install dotnet-coverage: {}", e))?;
133
134        if !output.status.success() {
135            // May already be installed
136            let stderr = String::from_utf8_lossy(&output.stderr);
137            if !stderr.contains("already installed") {
138                return Err(format!("Failed to install dotnet-coverage: {}", stderr));
139            }
140        }
141
142        println!("āœ“ dotnet-coverage installed");
143        Ok(())
144    }
145
146    /// Parse Cobertura XML coverage file
147    fn parse_cobertura_xml(&self, xml_file: &str) -> Result<CoverageSummary, String> {
148        let content =
149            fs::read_to_string(xml_file).map_err(|e| format!("Failed to read XML: {}", e))?;
150
151        let mut reader = Reader::from_str(&content);
152        reader.config_mut().trim_text(true);
153
154        let mut buf = Vec::new();
155        let mut line_rate = 0.0;
156        let mut branch_rate: Option<f64> = None;
157        let mut total_lines = 0;
158        let mut total_branches: Option<usize> = None;
159
160        loop {
161            match reader.read_event_into(&mut buf) {
162                Ok(Event::Eof) => break,
163
164                Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
165                    if e.name().as_ref() == b"coverage" {
166                        // Parse attributes from <coverage> element
167                        for attr in e.attributes().flatten() {
168                            let key = String::from_utf8_lossy(attr.key.as_ref());
169                            let value = String::from_utf8_lossy(&attr.value);
170
171                            match key.as_ref() {
172                                "line-rate" => {
173                                    line_rate = value.parse().unwrap_or(0.0);
174                                }
175                                "branch-rate" => {
176                                    branch_rate = value.parse().ok();
177                                }
178                                "lines-valid" => {
179                                    total_lines = value.parse().unwrap_or(0);
180                                }
181                                "branches-valid" => {
182                                    total_branches = value.parse().ok();
183                                }
184                                _ => {}
185                            }
186                        }
187                    }
188                }
189
190                Ok(_) => {}
191
192                Err(e) => {
193                    return Err(format!(
194                        "Error parsing XML at position {}: {:?}",
195                        reader.buffer_position(),
196                        e
197                    ));
198                }
199            }
200
201            buf.clear();
202        }
203
204        if total_lines == 0 {
205            return Err("No coverage data found in XML".to_string());
206        }
207
208        let covered_lines = (total_lines as f64 * line_rate) as usize;
209        let coverage_percentage = line_rate * 100.0;
210
211        let (covered_branches, branch_percentage) =
212            if let (Some(total_b), Some(rate)) = (total_branches, branch_rate) {
213                (Some((total_b as f64 * rate) as usize), Some(rate * 100.0))
214            } else {
215                (None, None)
216            };
217
218        Ok(CoverageSummary {
219            total_lines,
220            covered_lines,
221            coverage_percentage,
222            total_branches,
223            covered_branches,
224            branch_percentage,
225        })
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_runtime_coverage_new() {
235        let coverage = CSharpRuntimeCoverage::new();
236        assert_eq!(std::mem::size_of_val(&coverage), 0);
237    }
238
239    #[test]
240    fn test_runtime_coverage_default() {
241        let coverage = CSharpRuntimeCoverage;
242        assert_eq!(std::mem::size_of_val(&coverage), 0);
243    }
244}