testlint_sdk/runtime_coverage/
javascript.rs1use crate::platform::{process_exists, signal_process, ProcessSignal};
2use crate::runtime_coverage::{CoverageSummary, RuntimeCoverageResult};
3use std::fs;
4use std::path::Path;
5use std::process::Command;
6use std::thread;
7use std::time::Duration;
8
9pub struct JavaScriptRuntimeCoverage;
10
11impl Default for JavaScriptRuntimeCoverage {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl JavaScriptRuntimeCoverage {
18 pub fn new() -> Self {
19 JavaScriptRuntimeCoverage
20 }
21
22 pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
24 println!("š Attaching coverage to Node.js process PID: {}", pid);
25 println!("š Collecting coverage continuously (press Ctrl+C to stop)...");
26
27 self.check_nodejs_process(pid)?;
29
30 let inspector_port = self.find_or_enable_inspector(pid)?;
32
33 println!(
34 "š Connected to Node.js inspector on port {}",
35 inspector_port
36 );
37
38 println!("š Enabling coverage collection...");
40 self.enable_coverage(inspector_port)?;
41
42 use std::sync::atomic::{AtomicBool, Ordering};
44 use std::sync::Arc;
45
46 let running = Arc::new(AtomicBool::new(true));
47 let r = running.clone();
48
49 ctrlc::set_handler(move || {
50 r.store(false, Ordering::SeqCst);
51 })
52 .expect("Error setting Ctrl-C handler");
53
54 let start_time = std::time::Instant::now();
55
56 println!("ā³ Collecting coverage... Press Ctrl+C to stop");
57
58 while running.load(Ordering::SeqCst) {
59 thread::sleep(Duration::from_millis(100));
60 }
61
62 let duration_secs = start_time.elapsed().as_secs();
63
64 println!("\nš Collecting coverage data...");
66 let coverage_file = self.collect_coverage(pid, inspector_port)?;
67
68 let summary = self.parse_coverage_summary(&coverage_file)?;
70
71 Ok(RuntimeCoverageResult {
72 language: "JavaScript".to_string(),
73 pid,
74 duration_secs,
75 coverage_file,
76 summary,
77 })
78 }
79
80 fn check_nodejs_process(&self, pid: u32) -> Result<(), String> {
82 use crate::platform::get_process_name;
83
84 if !process_exists(pid)? {
85 return Err(format!("Process {} not found", pid));
86 }
87
88 let process_name = get_process_name(pid)?;
89 if !process_name.to_lowercase().contains("node") {
90 return Err(format!("Process {} is not a Node.js process", pid));
91 }
92
93 Ok(())
94 }
95
96 fn find_or_enable_inspector(&self, pid: u32) -> Result<u16, String> {
98 if let Ok(port) = self.find_existing_inspector(pid) {
100 return Ok(port);
101 }
102
103 println!("š§ Enabling Node.js inspector...");
105
106 #[cfg(unix)]
107 {
108 signal_process(pid, ProcessSignal::User1)?;
109
110 thread::sleep(Duration::from_secs(1));
112
113 self.find_existing_inspector(pid)
115 }
116
117 #[cfg(windows)]
118 {
119 let trigger_file = std::env::temp_dir().join(format!("inspector_trigger_{}.txt", pid));
121 fs::write(&trigger_file, "enable")?;
122 println!(
123 "ā¹ļø Created trigger file (Windows alternative to SIGUSR1): {}",
124 trigger_file.display()
125 );
126
127 thread::sleep(Duration::from_secs(1));
128 self.find_existing_inspector(pid)
129 }
130
131 #[cfg(not(any(unix, windows)))]
132 {
133 Err("Inspector detection not supported on this platform".to_string())
134 }
135 }
136
137 fn find_existing_inspector(&self, pid: u32) -> Result<u16, String> {
139 for port in [9229, 9230, 9231, 9232, 9233] {
141 if self.test_inspector_port(port) {
142 return Ok(port);
143 }
144 }
145
146 #[cfg(target_os = "linux")]
148 {
149 let cmdline_path = format!("/proc/{}/cmdline", pid);
151 if let Ok(cmdline) = fs::read_to_string(&cmdline_path) {
152 if let Some(port) = self.extract_inspector_port(&cmdline) {
153 return Ok(port);
154 }
155 }
156 }
157
158 Err(format!(
159 "Could not find Node.js inspector port for PID {}. \
160 Process may not have inspector enabled. \
161 Start your Node.js app with --inspect flag.",
162 pid
163 ))
164 }
165
166 fn test_inspector_port(&self, port: u16) -> bool {
168 use std::net::TcpStream;
170 TcpStream::connect(format!("127.0.0.1:{}", port))
171 .map(|_| true)
172 .unwrap_or(false)
173 }
174
175 #[allow(dead_code)]
177 fn extract_inspector_port(&self, cmdline: &str) -> Option<u16> {
178 for arg in cmdline.split('\0') {
180 if arg.starts_with("--inspect=") {
181 if let Some(port_str) = arg.strip_prefix("--inspect=") {
182 if let Ok(port) = port_str.parse() {
183 return Some(port);
184 }
185 }
186 }
187 }
188 None
189 }
190
191 fn enable_coverage(&self, inspector_port: u16) -> Result<(), String> {
193 let script = r#"
195const inspector = require('inspector');
196const session = new inspector.Session();
197
198session.connect();
199
200// Enable Profiler domain
201session.post('Profiler.enable');
202
203// Enable precise coverage
204session.post('Profiler.startPreciseCoverage', {
205 callCount: true,
206 detailed: true
207});
208
209console.log('[Coverage] Precise coverage enabled');
210
211// Keep the connection alive
212setTimeout(() => {}, 1000000);
213"#
214 .to_string();
215
216 let temp_dir = std::env::temp_dir();
217 let script_path = temp_dir.join(format!("enable_coverage_{}.js", inspector_port));
218 fs::write(&script_path, script).map_err(|e| format!("Failed to write script: {}", e))?;
219
220 let output = Command::new("node")
221 .arg(&script_path)
222 .output()
223 .map_err(|e| format!("Failed to enable coverage: {}", e))?;
224
225 let _ = fs::remove_file(&script_path);
226
227 if !output.status.success() {
228 return Err(format!(
229 "Failed to enable coverage: {}",
230 String::from_utf8_lossy(&output.stderr)
231 ));
232 }
233
234 Ok(())
235 }
236
237 fn collect_coverage(&self, pid: u32, _inspector_port: u16) -> Result<String, String> {
239 let coverage_file = format!("coverage-{}.json", pid);
241
242 let script = format!(
243 r#"
244const inspector = require('inspector');
245const fs = require('fs');
246const session = new inspector.Session();
247
248session.connect();
249
250// Take precise coverage
251session.post('Profiler.takePreciseCoverage', (err, {{ result }}) => {{
252 if (err) {{
253 console.error('Failed to take coverage:', err);
254 process.exit(1);
255 }}
256
257 // Save coverage to file
258 fs.writeFileSync('{}', JSON.stringify(result, null, 2));
259 console.log('[Coverage] Saved to {}');
260
261 // Stop coverage
262 session.post('Profiler.stopPreciseCoverage');
263 session.post('Profiler.disable');
264
265 process.exit(0);
266}});
267"#,
268 coverage_file, coverage_file
269 );
270
271 let temp_dir = std::env::temp_dir();
272 let script_path = temp_dir.join(format!("collect_coverage_{}.js", pid));
273 fs::write(&script_path, script)
274 .map_err(|e| format!("Failed to write collection script: {}", e))?;
275
276 let output = Command::new("node")
277 .arg(&script_path)
278 .output()
279 .map_err(|e| format!("Failed to collect coverage: {}", e))?;
280
281 let _ = fs::remove_file(&script_path);
282
283 if !output.status.success() {
284 return Err(format!(
285 "Failed to collect coverage: {}",
286 String::from_utf8_lossy(&output.stderr)
287 ));
288 }
289
290 if !Path::new(&coverage_file).exists() {
291 return Err(format!("Coverage file {} not found", coverage_file));
292 }
293
294 Ok(coverage_file)
295 }
296
297 fn parse_coverage_summary(&self, coverage_file: &str) -> Result<CoverageSummary, String> {
299 println!("š Parsing coverage report...");
300
301 let content = fs::read_to_string(coverage_file)
302 .map_err(|e| format!("Failed to read coverage file: {}", e))?;
303
304 let data: serde_json::Value =
305 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?;
306
307 let mut total_lines = 0;
308 let mut covered_lines = 0;
309
310 if let Some(scripts) = data.as_array() {
312 for script in scripts {
313 if let Some(functions) = script["functions"].as_array() {
314 for function in functions {
315 if let Some(ranges) = function["ranges"].as_array() {
316 for range in ranges {
317 if let (Some(count), Some(_start), Some(_end)) = (
318 range["count"].as_u64(),
319 range["startOffset"].as_u64(),
320 range["endOffset"].as_u64(),
321 ) {
322 total_lines += 1;
323 if count > 0 {
324 covered_lines += 1;
325 }
326 }
327 }
328 }
329 }
330 }
331 }
332 }
333
334 if total_lines == 0 {
335 return Err("No coverage data found".to_string());
336 }
337
338 let coverage_percentage = (covered_lines as f64 / total_lines as f64) * 100.0;
339
340 Ok(CoverageSummary {
341 total_lines,
342 covered_lines,
343 coverage_percentage,
344 total_branches: None,
345 covered_branches: None,
346 branch_percentage: None,
347 })
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_runtime_coverage_new() {
357 let coverage = JavaScriptRuntimeCoverage::new();
358 assert_eq!(std::mem::size_of_val(&coverage), 0);
359 }
360
361 #[test]
362 fn test_runtime_coverage_default() {
363 let coverage = JavaScriptRuntimeCoverage;
364 assert_eq!(std::mem::size_of_val(&coverage), 0);
365 }
366}