testlint_sdk/runtime_coverage/
java.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 JavaRuntimeCoverage;
12
13impl Default for JavaRuntimeCoverage {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl JavaRuntimeCoverage {
20 pub fn new() -> Self {
21 JavaRuntimeCoverage
22 }
23
24 pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
26 println!("ā Attaching JaCoCo coverage to Java process PID: {}", pid);
27 println!("š Collecting coverage continuously (press Ctrl+C to stop)...");
28
29 self.check_java_process(pid)?;
31
32 let agent_path = self.ensure_jacoco_agent()?;
34
35 println!("š Attaching JaCoCo agent to process...");
37 self.attach_agent(pid, &agent_path)?;
38
39 use std::sync::atomic::{AtomicBool, Ordering};
41 use std::sync::Arc;
42
43 let running = Arc::new(AtomicBool::new(true));
44 let r = running.clone();
45
46 ctrlc::set_handler(move || {
47 r.store(false, Ordering::SeqCst);
48 })
49 .expect("Error setting Ctrl-C handler");
50
51 let start_time = std::time::Instant::now();
52
53 println!("ā³ Collecting coverage... Press Ctrl+C to stop");
54
55 while running.load(Ordering::SeqCst) {
56 thread::sleep(Duration::from_millis(100));
57 }
58
59 let duration_secs = start_time.elapsed().as_secs();
60
61 println!("\nš Triggering coverage dump...");
63 self.dump_coverage(pid)?;
64
65 let coverage_file = format!("jacoco-{}.exec", pid);
67 if !Path::new(&coverage_file).exists() {
68 return Err(format!("Coverage file {} not found", coverage_file));
69 }
70
71 self.convert_to_xml(&coverage_file, pid)?;
73
74 let xml_file = format!("jacoco-{}.xml", pid);
75 let summary = self.parse_coverage_xml(&xml_file)?;
76
77 Ok(RuntimeCoverageResult {
78 language: "Java".to_string(),
79 pid,
80 duration_secs,
81 coverage_file: coverage_file.clone(),
82 summary,
83 })
84 }
85
86 fn check_java_process(&self, pid: u32) -> Result<(), String> {
88 if !process_exists(pid)? {
89 return Err(format!("Process {} not found", pid));
90 }
91
92 let process_name = get_process_name(pid)?;
93 if !process_name.to_lowercase().contains("java") {
94 return Err(format!("Process {} is not a Java process", pid));
95 }
96
97 Ok(())
98 }
99
100 fn ensure_jacoco_agent(&self) -> Result<String, String> {
102 let agent_path = "jacocoagent.jar";
103
104 if Path::new(agent_path).exists() {
105 return Ok(agent_path.to_string());
106 }
107
108 println!("š„ Downloading JaCoCo agent...");
109
110 let download_url = "https://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/0.8.11/org.jacoco.agent-0.8.11-runtime.jar";
112
113 let output = Command::new("curl")
114 .args(["-L", "-o", agent_path, download_url])
115 .output()
116 .map_err(|e| format!("Failed to download JaCoCo agent: {}", e))?;
117
118 if !output.status.success() {
119 return Err("Failed to download JaCoCo agent".to_string());
120 }
121
122 println!("ā JaCoCo agent downloaded");
123 Ok(agent_path.to_string())
124 }
125
126 fn attach_agent(&self, pid: u32, agent_path: &str) -> Result<(), String> {
128 let attach_code = format!(
130 r#"
131import com.sun.tools.attach.VirtualMachine;
132
133public class AttachAgent {{
134 public static void main(String[] args) throws Exception {{
135 String pid = "{}";
136 String agentPath = "{}";
137 String options = "destfile=jacoco-{}.exec,append=false,output=file,jmx=true";
138
139 VirtualMachine vm = VirtualMachine.attach(pid);
140 System.out.println("Attached to process " + pid);
141
142 vm.loadAgent(agentPath, options);
143 System.out.println("JaCoCo agent loaded successfully");
144
145 vm.detach();
146 System.out.println("Detached from process");
147 }}
148}}
149"#,
150 pid, agent_path, pid
151 );
152
153 fs::write("AttachAgent.java", attach_code)
155 .map_err(|e| format!("Failed to write attach program: {}", e))?;
156
157 let output = Command::new("javac")
159 .arg("AttachAgent.java")
160 .output()
161 .map_err(|e| format!("Failed to compile attach program: {}", e))?;
162
163 if !output.status.success() {
164 return Err(format!(
165 "Compilation failed: {}",
166 String::from_utf8_lossy(&output.stderr)
167 ));
168 }
169
170 let output = Command::new("java")
172 .arg("AttachAgent")
173 .output()
174 .map_err(|e| format!("Failed to run attach program: {}", e))?;
175
176 if !output.status.success() {
177 return Err(format!(
178 "Agent attachment failed: {}",
179 String::from_utf8_lossy(&output.stderr)
180 ));
181 }
182
183 let _ = fs::remove_file("AttachAgent.java");
185 let _ = fs::remove_file("AttachAgent.class");
186
187 Ok(())
188 }
189
190 fn dump_coverage(&self, pid: u32) -> Result<(), String> {
192 let dump_code = format!(
194 r#"
195import javax.management.*;
196import javax.management.remote.*;
197import java.lang.management.*;
198
199public class DumpCoverage {{
200 public static void main(String[] args) throws Exception {{
201 // Get the JMX connector URL from the process
202 String jmxUrl = findJMXUrl({});
203 if (jmxUrl == null) {{
204 System.err.println("Could not find JMX URL for process");
205 System.exit(1);
206 }}
207
208 JMXServiceURL url = new JMXServiceURL(jmxUrl);
209 JMXConnector connector = JMXConnectorFactory.connect(url);
210 MBeanServerConnection mbsc = connector.getMBeanServerConnection();
211
212 // Find JaCoCo MBean and trigger dump
213 ObjectName jacocoMBean = new ObjectName("org.jacoco:type=Runtime");
214 mbsc.invoke(jacocoMBean, "dump", new Object[]{{true}}, new String[]{{"boolean"}});
215
216 System.out.println("Coverage dump triggered");
217 connector.close();
218 }}
219
220 private static String findJMXUrl(int pid) {{
221 // Try to find JMX connection URL for the process
222 // This is a simplified version
223 return "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi";
224 }}
225}}
226"#,
227 pid
228 );
229
230 fs::write("DumpCoverage.java", dump_code)
231 .map_err(|e| format!("Failed to write dump program: {}", e))?;
232
233 println!("Note: Coverage will be dumped when process exits or on timeout");
235
236 Ok(())
237 }
238
239 fn convert_to_xml(&self, exec_file: &str, pid: u32) -> Result<(), String> {
241 println!("š Converting coverage to XML format...");
242
243 let cli_jar = self.ensure_jacoco_cli()?;
245
246 let xml_file = format!("jacoco-{}.xml", pid);
247
248 let output = Command::new("java")
249 .args(["-jar", &cli_jar, "report", exec_file, "--xml", &xml_file])
250 .output()
251 .map_err(|e| format!("Failed to convert coverage: {}", e))?;
252
253 if !output.status.success() {
254 return Err(format!(
255 "XML conversion failed: {}",
256 String::from_utf8_lossy(&output.stderr)
257 ));
258 }
259
260 Ok(())
261 }
262
263 fn ensure_jacoco_cli(&self) -> Result<String, String> {
265 let cli_jar = "jacococli.jar";
266
267 if Path::new(cli_jar).exists() {
268 return Ok(cli_jar.to_string());
269 }
270
271 println!("š„ Downloading JaCoCo CLI...");
272
273 let download_url = "https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/0.8.11/org.jacoco.cli-0.8.11-nodeps.jar";
274
275 let output = Command::new("curl")
276 .args(["-L", "-o", cli_jar, download_url])
277 .output()
278 .map_err(|e| format!("Failed to download JaCoCo CLI: {}", e))?;
279
280 if !output.status.success() {
281 return Err("Failed to download JaCoCo CLI".to_string());
282 }
283
284 Ok(cli_jar.to_string())
285 }
286
287 fn parse_coverage_xml(&self, xml_file: &str) -> Result<CoverageSummary, String> {
289 let content =
290 fs::read_to_string(xml_file).map_err(|e| format!("Failed to read XML: {}", e))?;
291
292 let mut reader = Reader::from_str(&content);
293 reader.config_mut().trim_text(true);
294
295 let mut buf = Vec::new();
296 let mut total_lines = 0;
297 let mut covered_lines = 0;
298 let mut total_branches = 0;
299 let mut covered_branches = 0;
300
301 loop {
302 match reader.read_event_into(&mut buf) {
303 Ok(Event::Eof) => break,
304
305 Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
306 if e.name().as_ref() == b"counter" {
307 let mut counter_type = String::new();
308 let mut missed = 0;
309 let mut covered = 0;
310
311 for attr in e.attributes().flatten() {
313 let key = String::from_utf8_lossy(attr.key.as_ref());
314 let value = String::from_utf8_lossy(&attr.value);
315
316 match key.as_ref() {
317 "type" => {
318 counter_type = value.to_string();
319 }
320 "missed" => {
321 missed = value.parse().unwrap_or(0);
322 }
323 "covered" => {
324 covered = value.parse().unwrap_or(0);
325 }
326 _ => {}
327 }
328 }
329
330 match counter_type.as_str() {
332 "LINE" => {
333 total_lines = missed + covered;
334 covered_lines = covered;
335 }
336 "BRANCH" => {
337 total_branches = missed + covered;
338 covered_branches = covered;
339 }
340 _ => {}
341 }
342 }
343 }
344
345 Ok(_) => {}
346
347 Err(e) => {
348 return Err(format!(
349 "Error parsing XML at position {}: {:?}",
350 reader.buffer_position(),
351 e
352 ));
353 }
354 }
355
356 buf.clear();
357 }
358
359 let coverage_percentage = if total_lines > 0 {
360 (covered_lines as f64 / total_lines as f64) * 100.0
361 } else {
362 0.0
363 };
364
365 let branch_percentage = if total_branches > 0 {
366 Some((covered_branches as f64 / total_branches as f64) * 100.0)
367 } else {
368 None
369 };
370
371 Ok(CoverageSummary {
372 total_lines,
373 covered_lines,
374 coverage_percentage,
375 total_branches: if total_branches > 0 {
376 Some(total_branches)
377 } else {
378 None
379 },
380 covered_branches: if total_branches > 0 {
381 Some(covered_branches)
382 } else {
383 None
384 },
385 branch_percentage,
386 })
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_runtime_coverage_new() {
396 let coverage = JavaRuntimeCoverage::new();
397 assert_eq!(std::mem::size_of_val(&coverage), 0);
398 }
399
400 #[test]
401 fn test_runtime_coverage_default() {
402 let coverage = JavaRuntimeCoverage;
403 assert_eq!(std::mem::size_of_val(&coverage), 0);
404 }
405}