testlint_sdk/runtime_coverage/
ruby.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 RubyRuntimeCoverage;
10
11impl Default for RubyRuntimeCoverage {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl RubyRuntimeCoverage {
18 pub fn new() -> Self {
19 RubyRuntimeCoverage
20 }
21
22 pub fn attach_and_collect(&self, pid: u32) -> Result<RuntimeCoverageResult, String> {
24 println!("š Attaching coverage to Ruby process PID: {}", pid);
25 println!("š Collecting coverage continuously (press Ctrl+C to stop)...");
26
27 self.check_process_exists(pid)?;
29
30 let inject_script = self.create_injection_script(pid)?;
32
33 println!("š Injecting SimpleCov into process...");
35 let output = Command::new("ruby")
36 .arg(&inject_script)
37 .output()
38 .map_err(|e| format!("Failed to run injection script: {}", e))?;
39
40 if !output.status.success() {
41 let stderr = String::from_utf8_lossy(&output.stderr);
42 return Err(format!(
43 "Coverage injection failed: {}\n{}",
44 stderr, "Install debase for Ruby injection: gem install debase"
45 ));
46 }
47
48 println!("ā Coverage injected successfully");
49 println!("ā³ Collecting coverage... Press Ctrl+C to stop");
50
51 use std::sync::atomic::{AtomicBool, Ordering};
53 use std::sync::Arc;
54
55 let running = Arc::new(AtomicBool::new(true));
56 let r = running.clone();
57
58 ctrlc::set_handler(move || {
59 r.store(false, Ordering::SeqCst);
60 })
61 .expect("Error setting Ctrl-C handler");
62
63 let start_time = std::time::Instant::now();
64
65 while running.load(Ordering::SeqCst) {
66 thread::sleep(Duration::from_millis(100));
67 }
68
69 let duration_secs = start_time.elapsed().as_secs();
70
71 println!("\nš Signaling process to save coverage data...");
73
74 #[cfg(unix)]
75 {
76 signal_process(pid, ProcessSignal::User1)?;
77 }
78
79 #[cfg(windows)]
80 {
81 let trigger_file = std::env::temp_dir().join(format!("coverage_trigger_{}.txt", pid));
83 fs::write(&trigger_file, "save")?;
84 println!(
85 "ā¹ļø Created trigger file (Windows alternative to SIGUSR1): {}",
86 trigger_file.display()
87 );
88 }
89
90 thread::sleep(Duration::from_secs(2));
92
93 let coverage_file = self.find_coverage_file(pid)?;
95 let summary = self.parse_coverage_summary(&coverage_file)?;
96
97 let _ = fs::remove_file(&inject_script);
99
100 Ok(RuntimeCoverageResult {
101 language: "Ruby".to_string(),
102 pid,
103 duration_secs,
104 coverage_file,
105 summary,
106 })
107 }
108
109 fn check_process_exists(&self, pid: u32) -> Result<(), String> {
111 if !process_exists(pid)? {
112 return Err(format!("Process {} not found", pid));
113 }
114 Ok(())
115 }
116
117 fn create_injection_script(&self, pid: u32) -> Result<String, String> {
119 let temp_dir = std::env::temp_dir();
120 let script_path = temp_dir.join(format!("coverage_inject_{}.rb", pid));
121 let script_path_str = script_path.to_string_lossy().to_string();
122
123 let script_content = format!(
124 r#"#!/usr/bin/env ruby
125# Runtime coverage injection script for Ruby
126# Injects SimpleCov into a running Ruby process
127
128require 'socket'
129
130PID = {}
131
132# Ruby code to inject into the target process
133COVERAGE_CODE = <<~RUBY
134 require 'simplecov'
135 require 'json'
136
137 SimpleCov.start do
138 coverage_dir 'coverage'
139 command_name "runtime-pid-#{{Process.pid}}"
140 end
141
142 # Enable Ruby's built-in Coverage if SimpleCov isn't starting it
143 Coverage.start unless Coverage.running?
144
145 puts "[Coverage] SimpleCov started for PID #{{Process.pid}}"
146
147 # Save coverage on SIGUSR1
148 Signal.trap('USR1') do
149 puts "[Coverage] Saving coverage data..."
150 result = Coverage.result
151
152 # Save to JSON format
153 coverage_file = ".coverage.#{{Process.pid}}.json"
154 File.write(coverage_file, JSON.pretty_generate(result))
155 puts "[Coverage] Coverage saved to #{{coverage_file}}"
156
157 # Try to use SimpleCov formatter too
158 begin
159 SimpleCov.result.format!
160 puts "[Coverage] SimpleCov HTML report generated"
161 rescue => e
162 puts "[Coverage] SimpleCov format error: #{{e.message}}"
163 end
164 end
165
166 # Also save on exit
167 at_exit do
168 result = Coverage.result
169 coverage_file = ".coverage.#{{Process.pid}}.json"
170 File.write(coverage_file, JSON.pretty_generate(result))
171 end
172RUBY
173
174def inject_via_debugger
175 begin
176 require 'debase'
177
178 puts "Injecting coverage into PID #{{PID}} using debase..."
179
180 # Create temp file with coverage code
181 require 'tmpdir'
182 File.write(File.join(Dir.tmpdir, "cov_inject_#{{PID}}.rb"), COVERAGE_CODE)
183
184 # This is a simplified approach - real implementation would use
185 # Ruby debugger protocol to inject code into running process
186 puts "Note: Ruby runtime injection requires manual setup."
187 puts "For production use, consider:"
188 puts " 1. Start app with SimpleCov from beginning"
189 puts " 2. Use pry-remote or debugging gems"
190 puts " 3. Use file-based coverage triggers"
191
192 # Write signal script
193 signal_script = <<~SCRIPT
194 Process.kill('USR1', #{{PID}})
195 SCRIPT
196
197 File.write(File.join(Dir.tmpdir, "signal_#{{PID}}.rb"), signal_script)
198
199 return true
200 rescue LoadError
201 puts "debase not installed"
202 return false
203 end
204end
205
206def inject_via_gdb
207 # For MRI Ruby, we can try using gdb similar to Python
208 puts "Attempting GDB injection for Ruby process " + PID.to_s + "..."
209
210 # Simplified: Just notify user that gdb injection is complex
211 puts "Note: GDB injection for Ruby requires manual steps."
212 puts "For production use, start your Ruby app with SimpleCov enabled."
213
214 return false
215end
216
217# Try debugger approach first, fall back to gdb
218unless inject_via_debugger
219 unless inject_via_gdb
220 puts "ERROR: Failed to inject coverage."
221 puts "Install debase: gem install debase "
222 puts "or start your Ruby app with SimpleCov from the beginning"
223 exit 1
224 end
225end
226
227puts "Coverage collection running continuously..."
228puts "Process will save coverage when receiving SIGUSR1 signal "
229"#,
230 pid
231 );
232
233 fs::write(&script_path, script_content)
234 .map_err(|e| format!("Failed to create injection script: {}", e))?;
235
236 #[cfg(unix)]
238 {
239 Command::new("chmod")
240 .args(["+x", &script_path_str])
241 .output()
242 .map_err(|e| format!("Failed to make script executable: {}", e))?;
243 }
244
245 Ok(script_path_str)
246 }
247
248 fn find_coverage_file(&self, pid: u32) -> Result<String, String> {
250 let coverage_file = format!(".coverage.{}.json", pid);
251
252 if Path::new(&coverage_file).exists() {
253 return Ok(coverage_file);
254 }
255
256 let simplecov_file = "coverage/.resultset.json".to_string();
258 if Path::new(&simplecov_file).exists() {
259 return Ok(simplecov_file);
260 }
261
262 Err(format!(
263 "Coverage file not found. Expected .coverage.{}.json or coverage/.resultset.json",
264 pid
265 ))
266 }
267
268 fn parse_coverage_summary(&self, coverage_file: &str) -> Result<CoverageSummary, String> {
270 println!("š Parsing coverage report...");
271
272 let content = fs::read_to_string(coverage_file)
273 .map_err(|e| format!("Failed to read coverage file: {}", e))?;
274
275 let data: serde_json::Value =
276 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?;
277
278 let mut total_lines = 0;
279 let mut covered_lines = 0;
280
281 if let Some(obj) = data.as_object() {
284 for (_filename, coverage) in obj {
285 if let Some(lines) = coverage.as_array() {
286 for line_hits in lines {
287 if let Some(hits) = line_hits.as_u64() {
288 total_lines += 1;
289 if hits > 0 {
290 covered_lines += 1;
291 }
292 } else if let Some(hits) = line_hits.as_i64() {
293 if hits >= 0 {
294 total_lines += 1;
295 if hits > 0 {
296 covered_lines += 1;
297 }
298 }
299 }
300 }
301 }
302 }
303 }
304
305 let coverage_percentage = if total_lines > 0 {
306 (covered_lines as f64 / total_lines as f64) * 100.0
307 } else {
308 0.0
309 };
310
311 Ok(CoverageSummary {
312 total_lines,
313 covered_lines,
314 coverage_percentage,
315 total_branches: None,
316 covered_branches: None,
317 branch_percentage: None,
318 })
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_runtime_coverage_new() {
328 let coverage = RubyRuntimeCoverage::new();
329 assert_eq!(std::mem::size_of_val(&coverage), 0);
330 }
331
332 #[test]
333 fn test_runtime_coverage_default() {
334 let coverage = RubyRuntimeCoverage;
335 assert_eq!(std::mem::size_of_val(&coverage), 0);
336 }
337}