rb_sys_test_helpers/
ruby_exception.rs

1use crate::{rb_funcall_typed, rstring_to_string};
2use rb_sys::{
3    rb_ary_join, rb_class2name, rb_obj_class, rb_str_new,
4    ruby_value_type::{RUBY_T_ARRAY, RUBY_T_STRING},
5    RB_TYPE_P, VALUE,
6};
7use std::ffi::CStr;
8
9/// A simple wrapper around a Ruby exception that provides some convenience
10/// methods for testing.
11#[derive(Clone, Eq, PartialEq)]
12pub struct RubyException {
13    value: VALUE,
14}
15
16impl RubyException {
17    /// Creates a new Ruby exception from a Ruby value.
18    pub fn new(value: VALUE) -> Self {
19        Self { value }
20    }
21
22    /// Get the message of the Ruby exception.
23    pub fn message(&self) -> Option<String> {
24        unsafe {
25            rb_funcall_typed!(self.value, "message", [], RUBY_T_STRING)
26                .map(|mut message| rstring_to_string!(message))
27        }
28    }
29
30    /// Get the full message of the Ruby exception.
31    pub fn full_message(&self) -> Option<String> {
32        unsafe {
33            if let Some(mut message) =
34                rb_funcall_typed!(self.value, "full_message", [], RUBY_T_STRING)
35            {
36                let message = rstring_to_string!(message);
37                Some(message.trim_start_matches("-e: ").to_string())
38            } else {
39                None
40            }
41        }
42    }
43
44    /// Get the backtrace string of the Ruby exception.
45    pub fn backtrace(&self) -> Option<String> {
46        unsafe {
47            if let Some(backtrace) = rb_funcall_typed!(self.value, "backtrace", [], RUBY_T_ARRAY) {
48                let mut backtrace = rb_ary_join(backtrace, rb_str_new("\n".as_ptr() as _, 1));
49                let backtrace = rstring_to_string!(backtrace);
50
51                if backtrace.is_empty() {
52                    return None;
53                }
54
55                Some(backtrace)
56            } else {
57                None
58            }
59        }
60    }
61
62    /// Get the inspect string of the Ruby exception.
63    pub fn inspect(&self) -> String {
64        unsafe {
65            if let Some(mut inspect) = rb_funcall_typed!(self.value, "inspect", [], RUBY_T_STRING) {
66                rstring_to_string!(inspect)
67            } else {
68                format!("<no inspect: {:?}>", self.value)
69            }
70        }
71    }
72
73    /// Get the class name of the Ruby exception.
74    pub fn classname(&self) -> String {
75        unsafe {
76            let classname = rb_class2name(rb_obj_class(self.value));
77            CStr::from_ptr(classname).to_string_lossy().into_owned()
78        }
79    }
80}
81
82// impl Drop for RubyException {
83//     fn drop(&mut self) {
84//         rb_sys::rb_gc_guard!(self.value);
85//     }
86// }
87
88impl std::fmt::Debug for RubyException {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        let message = self.message();
91        let klass = self.classname();
92        let bt = self.backtrace();
93
94        if let Some(full_message) = self.full_message() {
95            return f.write_str(&full_message);
96        }
97
98        if let Some(message) = message {
99            f.write_str(&message)?;
100        } else {
101            f.write_str("<no message>")?;
102        }
103
104        f.write_fmt(format_args!(" ({}):\n", klass))?;
105
106        if let Some(bt) = bt {
107            f.write_str(&bt)?;
108        } else {
109            f.write_str("<no backtrace>")?;
110        }
111
112        Ok(())
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use crate::{protect, with_ruby_vm};
119    use rb_sys::rb_eval_string;
120
121    #[test]
122    fn test_exception() -> Result<(), Box<dyn std::error::Error>> {
123        with_ruby_vm(|| {
124            let exception = protect(|| unsafe {
125                rb_eval_string("raise 'oh no'\0".as_ptr() as _);
126            })
127            .unwrap_err();
128
129            assert_eq!("RuntimeError", exception.classname());
130            assert_eq!("oh no", exception.message().unwrap());
131            #[cfg(ruby_gt_2_4)]
132            {
133                let message = exception.full_message().unwrap();
134                assert!(message.contains("eval:1:in "), "message: {}", message);
135            }
136        })
137    }
138}