testlint_sdk/runtime_coverage/
java.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 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    /// Attach JaCoCo agent to a running Java process
25    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        // Check if process exists and is Java
30        self.check_java_process(pid)?;
31
32        // Download JaCoCo agent if needed
33        let agent_path = self.ensure_jacoco_agent()?;
34
35        // Attach JaCoCo agent to the process
36        println!("šŸ’‰ Attaching JaCoCo agent to process...");
37        self.attach_agent(pid, &agent_path)?;
38
39        // Wait for Ctrl+C
40        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        // Trigger dump via JMX or TCP
62        println!("\nšŸ“ Triggering coverage dump...");
63        self.dump_coverage(pid)?;
64
65        // Find and parse coverage file
66        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        // Convert to XML for parsing
72        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    /// Check if process is a Java process
87    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    /// Ensure JaCoCo agent JAR is available
101    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        // Download JaCoCo agent
111        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    /// Attach JaCoCo agent to running process using Java attach API
127    fn attach_agent(&self, pid: u32, agent_path: &str) -> Result<(), String> {
128        // Create a small Java program to attach the agent
129        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        // Write the attach program
154        fs::write("AttachAgent.java", attach_code)
155            .map_err(|e| format!("Failed to write attach program: {}", e))?;
156
157        // Compile it
158        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        // Run it (requires tools.jar in classpath)
171        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        // Cleanup
184        let _ = fs::remove_file("AttachAgent.java");
185        let _ = fs::remove_file("AttachAgent.class");
186
187        Ok(())
188    }
189
190    /// Trigger coverage dump via JMX
191    fn dump_coverage(&self, pid: u32) -> Result<(), String> {
192        // Create JMX dump program
193        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        // This is simplified - in practice, we'd wait and let the file-based dump happen
234        println!("Note: Coverage will be dumped when process exits or on timeout");
235
236        Ok(())
237    }
238
239    /// Convert JaCoCo exec file to XML
240    fn convert_to_xml(&self, exec_file: &str, pid: u32) -> Result<(), String> {
241        println!("šŸ“„ Converting coverage to XML format...");
242
243        // This requires JaCoCo CLI
244        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    /// Ensure JaCoCo CLI JAR is available
264    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    /// Parse XML coverage file
288    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                        // Parse attributes from <counter> element
312                        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                        // Apply based on counter type
331                        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}