testlint_sdk/runtime_coverage/
csharp.rs1use 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 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 self.check_dotnet_process(pid)?;
31
32 self.ensure_dotnet_coverage()?;
34
35 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 let mut child = cmd
52 .spawn()
53 .map_err(|e| format!("Failed to start dotnet-coverage: {}", e))?;
54
55 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 println!("\nš Stopping coverage collection...");
79 child
80 .kill()
81 .map_err(|e| format!("Failed to stop coverage: {}", e))?;
82
83 thread::sleep(Duration::from_secs(2));
85
86 if !Path::new(&coverage_file).exists() {
88 return Err(format!("Coverage file {} not found", coverage_file));
89 }
90
91 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 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 fn ensure_dotnet_coverage(&self) -> Result<(), String> {
120 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 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 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 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}