use std::convert::From;
use crate::{
binding::{debug, vm},
rubysys::debug as rubysys_debug,
types::{c_int, Value},
AnyException, AnyObject, Array, Integer, NilClass, Object, RString,
};
fn optional(value: Value) -> Option<Value> {
if value.is_nil() {
None
} else {
Some(value)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProfileFrame {
frame: Value,
line: c_int,
}
impl ProfileFrame {
pub(crate) fn from_frames(frames: Vec<(Value, c_int)>) -> Vec<ProfileFrame> {
frames
.into_iter()
.map(|(frame, line)| ProfileFrame { frame, line })
.collect()
}
fn string(value: Value) -> Option<RString> {
optional(value).map(RString::from)
}
pub fn line(&self) -> i32 {
self.line
}
pub fn path(&self) -> Option<RString> {
Self::string(debug::profile_frame_path(self.frame))
}
pub fn absolute_path(&self) -> Option<RString> {
Self::string(debug::profile_frame_absolute_path(self.frame))
}
pub fn label(&self) -> Option<RString> {
Self::string(debug::profile_frame_label(self.frame))
}
pub fn base_label(&self) -> Option<RString> {
Self::string(debug::profile_frame_base_label(self.frame))
}
pub fn full_label(&self) -> Option<RString> {
Self::string(debug::profile_frame_full_label(self.frame))
}
pub fn first_lineno(&self) -> Option<i64> {
optional(debug::profile_frame_first_lineno(self.frame))
.map(|line| Integer::from(line).to_i64())
}
pub fn classpath(&self) -> Option<RString> {
Self::string(debug::profile_frame_classpath(self.frame))
}
pub fn is_singleton_method(&self) -> bool {
debug::profile_frame_is_singleton_method(self.frame)
}
pub fn method_name(&self) -> Option<RString> {
Self::string(debug::profile_frame_method_name(self.frame))
}
pub fn qualified_method_name(&self) -> Option<RString> {
Self::string(debug::profile_frame_qualified_method_name(self.frame))
}
}
pub struct DebugInspector {
context: *const rubysys_debug::DebugInspector,
frames: usize,
}
impl DebugInspector {
pub fn open<F, R>(func: F) -> Result<R, AnyException>
where
F: FnOnce(&DebugInspector) -> R,
{
let mut result = None;
vm::protect_value(|| {
debug::debug_inspector_open(|context| {
let locations = unsafe { debug::debug_inspector_backtrace_locations(context) };
let inspector = DebugInspector {
context,
frames: Array::from(locations).length(),
};
result = Some(func(&inspector));
NilClass::new().value()
})
})
.map_err(AnyException::from)?;
Ok(result.expect("the debug inspector did not run"))
}
pub fn backtrace_locations(&self) -> Array {
Array::from(unsafe { debug::debug_inspector_backtrace_locations(self.context) })
}
pub fn frame_count(&self) -> usize {
self.frames
}
fn frame<F>(&self, index: usize, get: F) -> Option<AnyObject>
where
F: FnOnce(*const rubysys_debug::DebugInspector, usize) -> Value,
{
if index < self.frames {
Some(AnyObject::from(get(self.context, index)))
} else {
None
}
}
pub fn frame_self(&self, index: usize) -> Option<AnyObject> {
self.frame(index, |context, index| unsafe {
debug::debug_inspector_frame_self(context, index)
})
}
pub fn frame_class(&self, index: usize) -> Option<AnyObject> {
self.frame(index, |context, index| unsafe {
debug::debug_inspector_frame_class(context, index)
})
}
pub fn frame_binding(&self, index: usize) -> Option<AnyObject> {
self.frame(index, |context, index| unsafe {
debug::debug_inspector_frame_binding(context, index)
})
}
pub fn frame_iseq(&self, index: usize) -> Option<AnyObject> {
self.frame(index, |context, index| unsafe {
debug::debug_inspector_frame_iseq(context, index)
})
}
#[cfg(ruby_gte_3_2)]
pub fn frame_depth(&self, index: usize) -> Option<i64> {
self.frame(index, |context, index| unsafe {
debug::debug_inspector_frame_depth(context, index)
})
.map(|depth| Integer::from(depth.value()).to_i64())
}
#[cfg(ruby_gte_3_2)]
pub fn current_depth() -> i64 {
Integer::from(debug::debug_inspector_current_depth()).to_i64()
}
}
#[cfg(ruby_gte_3_3)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PostponedJob(rubysys_debug::PostponedJobHandle);
#[cfg(ruby_gte_3_3)]
impl PostponedJob {
pub fn preregister<F>(func: F) -> Option<Self>
where
F: Fn() + Send + Sync + 'static,
{
debug::postponed_job_preregister(func).map(PostponedJob)
}
pub fn trigger(&self) {
unsafe { debug::postponed_job_trigger(self.0) }
}
}
#[cfg(test)]
mod tests {
use crate::{
AnyException, AnyObject, Array, Class, DebugInspector, Exception, Fixnum, Object, RString,
VM,
};
crate::methods!(
AnyObject,
_rtself,
fn rutie_test_profile() -> Array {
let mut rows = Array::new();
for frame in VM::profile_frames(0, 16) {
let mut row = Array::new();
let text = |value: Option<RString>| match value {
Some(text) => text.to_any_object(),
None => crate::NilClass::new().to_any_object(),
};
row.push(Fixnum::new(frame.line() as i64));
row.push(text(frame.path()));
row.push(text(frame.absolute_path()));
row.push(text(frame.label()));
row.push(text(frame.base_label()));
row.push(text(frame.full_label()));
row.push(match frame.first_lineno() {
Some(line) => Fixnum::new(line).to_any_object(),
None => crate::NilClass::new().to_any_object(),
});
row.push(text(frame.classpath()));
row.push(crate::Boolean::new(frame.is_singleton_method()));
row.push(text(frame.method_name()));
row.push(text(frame.qualified_method_name()));
rows.push(row);
}
rows
},
fn rutie_test_inspect() -> Array {
DebugInspector::open(|inspector| {
let mut rows = Array::new();
let count = inspector.frame_count();
assert_eq!(count, inspector.backtrace_locations().length());
assert!(inspector.frame_self(count).is_none());
assert!(inspector.frame_class(count).is_none());
assert!(inspector.frame_binding(count).is_none());
assert!(inspector.frame_iseq(count).is_none());
for i in 0..count {
let mut row = Array::new();
row.push(inspector.frame_self(i).unwrap());
row.push(inspector.frame_class(i).unwrap());
row.push(inspector.frame_binding(i).unwrap());
row.push(inspector.frame_iseq(i).unwrap());
rows.push(row);
}
#[cfg(ruby_gte_3_2)]
{
assert!(inspector.frame_depth(count).is_none());
assert_eq!(
inspector.frame_depth(0),
Some(DebugInspector::current_depth())
);
let depths: Vec<i64> = (0..count)
.map(|i| inspector.frame_depth(i).unwrap())
.collect();
assert!(depths.windows(2).all(|pair| pair[0] > pair[1]));
}
rows
})
.unwrap()
}
);
fn string(object: AnyObject) -> Option<String> {
object
.try_convert_to::<RString>()
.ok()
.map(|text| text.to_string())
}
#[test]
fn test_profile_frames() {
crate::on_ruby_thread(|| {
Class::from_existing("Object").define(|klass| {
klass.def_private("rutie_test_profile", rutie_test_profile);
});
let rows = VM::eval(
"module RutieProfile
class Sample
def self.collect
rutie_test_profile
end
end
end
RutieProfile::Sample.collect",
)
.unwrap()
.try_convert_to::<Array>()
.unwrap();
let row = |i: usize| rows.at(i as i64).try_convert_to::<Array>().unwrap();
let native = row(0);
assert_eq!(native.at(0), Fixnum::new(0).into());
assert!(native.at(1).is_nil());
assert_eq!(string(native.at(2)).unwrap(), "<cfunc>");
assert!(native.at(3).is_nil());
assert_eq!(string(native.at(5)).unwrap(), "Object#rutie_test_profile");
assert!(native.at(6).is_nil());
assert_eq!(string(native.at(7)).unwrap(), "Object");
assert!(native.at(8).is_false());
assert_eq!(string(native.at(9)).unwrap(), "rutie_test_profile");
let ruby = row(1);
assert_eq!(ruby.at(0), Fixnum::new(4).into());
assert_eq!(string(ruby.at(1)).unwrap(), "eval");
assert_eq!(string(ruby.at(3)).unwrap(), "collect");
assert_eq!(string(ruby.at(4)).unwrap(), "collect");
assert_eq!(string(ruby.at(5)).unwrap(), "RutieProfile::Sample.collect");
assert_eq!(ruby.at(6), Fixnum::new(3).into());
assert_eq!(string(ruby.at(7)).unwrap(), "RutieProfile::Sample");
assert!(ruby.at(8).is_true());
assert_eq!(string(ruby.at(9)).unwrap(), "collect");
assert_eq!(string(ruby.at(10)).unwrap(), "RutieProfile::Sample.collect");
let top = row(2);
assert_eq!(string(top.at(3)).unwrap(), "<main>");
assert!(top.at(9).is_nil());
assert!(VM::profile_frames(0, 8).len() <= 1);
assert!(VM::profile_frames(0, 0).is_empty());
});
}
#[test]
fn test_debug_inspector() {
crate::on_ruby_thread(|| {
Class::from_existing("Object").define(|klass| {
klass.def_private("rutie_test_inspect", rutie_test_inspect);
});
let rows = VM::eval(
"class RutieInspected
def run
answer = 42
[1].map { rutie_test_inspect }.first
end
end
RutieInspected.new.run",
)
.unwrap()
.try_convert_to::<Array>()
.unwrap();
let row = |i: i64| rows.at(i).try_convert_to::<Array>().unwrap();
assert!(row(0).at(2).is_nil());
assert!(row(0).at(3).is_nil());
assert_eq!(row(1).at(1), Class::from_existing("RutieInspected").into());
assert_eq!(row(2).at(1), Class::from_existing("Array").into());
assert!(row(2).at(2).is_nil());
let binding = row(3).at(2);
let answer = binding
.protect_send("local_variable_get", &[crate::Symbol::new("answer").into()])
.unwrap();
assert_eq!(answer, Fixnum::new(42).into());
let label = row(3).at(3).protect_send("label", &[]).unwrap();
assert_eq!(string(label).unwrap(), "run");
assert_eq!(DebugInspector::open(|_| 7).unwrap(), 7);
let error = DebugInspector::open(|_| {
VM::raise_ex(AnyException::new("TypeError", Some("in inspector")));
})
.unwrap_err();
assert_eq!(error.message(), "in inspector");
let panic = DebugInspector::open(|_| panic!("inspector panic")).unwrap_err();
assert_eq!(panic.message(), "Rust panic: inspector panic");
});
}
#[cfg(ruby_gte_3_3)]
#[test]
fn test_postponed_job() {
use crate::{PostponedJob, Thread};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
crate::on_ruby_thread(|| {
let runs = Arc::new(AtomicUsize::new(0));
let job = {
let runs = runs.clone();
PostponedJob::preregister(move || {
if runs.fetch_add(1, Ordering::SeqCst) == 1 {
VM::raise_ex(AnyException::new("RuntimeError", Some("discarded")));
}
})
.unwrap()
};
Thread::check_interrupts();
assert_eq!(runs.load(Ordering::SeqCst), 0);
job.trigger();
job.trigger();
Thread::check_interrupts();
assert_eq!(runs.load(Ordering::SeqCst), 1);
job.trigger();
Thread::check_interrupts();
assert_eq!(runs.load(Ordering::SeqCst), 2);
assert!(VM::error_info().is_err());
let handles: Vec<_> = (0..4)
.map(|_| std::thread::spawn(move || job.trigger()))
.collect();
for handle in handles {
handle.join().unwrap();
}
Thread::check_interrupts();
assert_eq!(runs.load(Ordering::SeqCst), 3);
let panicking = PostponedJob::preregister(|| panic!("job panic")).unwrap();
assert_ne!(panicking, job);
panicking.trigger();
Thread::check_interrupts();
assert_eq!(VM::eval("1 + 1").unwrap(), Fixnum::new(2).into());
});
}
}