Skip to main content

rusteal_core/
error.rs

1use std::any::Any;
2use std::borrow::Cow;
3use std::error::Error;
4use std::fmt;
5use std::panic::Location;
6
7use std::sync::OnceLock;
8
9use rusteal_ffi::{
10    FWeakObjectHandle, RustealErrorCode, RustealErrorReport, UClassHandle, UObjectHandle,
11};
12
13use crate::traits::UeHandle;
14
15#[non_exhaustive]
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum ErrorKind {
18    ObjectDestroyed,
19    InvalidCast,
20    PropertyNotFound,
21    FunctionNotFound,
22    TypeMismatch,
23    NullArgument,
24    IndexOutOfRange,
25    InvalidOperation,
26    BufferTooSmall,
27    Internal,
28    Other,
29}
30
31impl ErrorKind {
32    pub fn description(self) -> &'static str {
33        match self {
34            ErrorKind::ObjectDestroyed => "object has been destroyed",
35            ErrorKind::InvalidCast => "invalid cast",
36            ErrorKind::PropertyNotFound => "property not found",
37            ErrorKind::FunctionNotFound => "function not found",
38            ErrorKind::TypeMismatch => "type mismatch",
39            ErrorKind::NullArgument => "null argument",
40            ErrorKind::IndexOutOfRange => "index out of range",
41            ErrorKind::InvalidOperation => "invalid operation",
42            ErrorKind::BufferTooSmall => "buffer too small",
43            ErrorKind::Internal => "internal error",
44            ErrorKind::Other => "error",
45        }
46    }
47}
48
49impl fmt::Display for ErrorKind {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(self.description())
52    }
53}
54
55impl From<RustealErrorCode> for ErrorKind {
56    fn from(code: RustealErrorCode) -> Self {
57        match code {
58            RustealErrorCode::Ok | RustealErrorCode::InternalError => ErrorKind::Internal,
59            RustealErrorCode::ObjectDestroyed => ErrorKind::ObjectDestroyed,
60            RustealErrorCode::InvalidCast => ErrorKind::InvalidCast,
61            RustealErrorCode::PropertyNotFound => ErrorKind::PropertyNotFound,
62            RustealErrorCode::FunctionNotFound => ErrorKind::FunctionNotFound,
63            RustealErrorCode::TypeMismatch => ErrorKind::TypeMismatch,
64            RustealErrorCode::NullArgument => ErrorKind::NullArgument,
65            RustealErrorCode::IndexOutOfRange => ErrorKind::IndexOutOfRange,
66            RustealErrorCode::InvalidOperation => ErrorKind::InvalidOperation,
67            RustealErrorCode::BufferTooSmall => ErrorKind::BufferTooSmall,
68        }
69    }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct UeSubject {
74    pub object: Option<String>,
75    pub class: Option<String>,
76}
77
78impl fmt::Display for UeSubject {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match (&self.object, &self.class) {
81            (Some(object), Some(class)) => write!(f, "object {object} of class {class}"),
82            (Some(object), None) => write!(f, "object {object}"),
83            (None, Some(class)) => write!(f, "class {class}"),
84            (None, None) => Ok(()),
85        }
86    }
87}
88
89struct Frame {
90    message: Cow<'static, str>,
91    location: &'static Location<'static>,
92}
93
94struct Inner {
95    kind: ErrorKind,
96    message: Option<Cow<'static, str>>,
97    engine: Option<String>,
98    subject: Option<UeSubject>,
99    object: Option<FWeakObjectHandle>,
100    location: &'static Location<'static>,
101    frames: Vec<Frame>,
102    source: Option<Box<dyn Error + Send + Sync + 'static>>,
103}
104
105pub struct RustealError {
106    inner: Box<Inner>,
107}
108
109pub type RustealResult<T> = Result<T, RustealError>;
110
111impl RustealError {
112    pub(crate) fn build(kind: ErrorKind, location: &'static Location<'static>) -> Self {
113        RustealError {
114            inner: Box::new(Inner {
115                kind,
116                message: None,
117                engine: None,
118                subject: None,
119                object: None,
120                location,
121                frames: Vec::new(),
122                source: None,
123            }),
124        }
125    }
126
127    #[track_caller]
128    pub fn new(kind: ErrorKind) -> Self {
129        Self::build(kind, Location::caller())
130    }
131
132    #[track_caller]
133    pub fn with_message(kind: ErrorKind, message: impl Into<Cow<'static, str>>) -> Self {
134        let mut error = Self::build(kind, Location::caller());
135        error.inner.message = Some(message.into());
136        error
137    }
138
139    #[track_caller]
140    pub fn msg(message: impl Into<Cow<'static, str>>) -> Self {
141        Self::with_message(ErrorKind::Other, message)
142    }
143
144    #[track_caller]
145    pub fn other(error: impl Error + Send + Sync + 'static) -> Self {
146        into_rusteal(error, Location::caller())
147    }
148
149    #[track_caller]
150    pub fn object_destroyed() -> Self {
151        Self::new(ErrorKind::ObjectDestroyed)
152    }
153
154    #[track_caller]
155    pub fn invalid_cast() -> Self {
156        Self::new(ErrorKind::InvalidCast)
157    }
158
159    #[track_caller]
160    pub fn invalid_cast_to(object: UObjectHandle, target: UClassHandle) -> Self {
161        cast_failed(object, target, Location::caller())
162    }
163
164    #[track_caller]
165    pub fn property_not_found(name: impl Into<Cow<'static, str>>) -> Self {
166        Self::with_message(ErrorKind::PropertyNotFound, name)
167    }
168
169    #[track_caller]
170    pub fn function_not_found(name: impl Into<Cow<'static, str>>) -> Self {
171        Self::with_message(ErrorKind::FunctionNotFound, name)
172    }
173
174    #[track_caller]
175    pub fn type_mismatch() -> Self {
176        Self::new(ErrorKind::TypeMismatch)
177    }
178
179    #[track_caller]
180    pub fn null_argument() -> Self {
181        Self::new(ErrorKind::NullArgument)
182    }
183
184    #[track_caller]
185    pub fn index_out_of_range() -> Self {
186        Self::new(ErrorKind::IndexOutOfRange)
187    }
188
189    #[track_caller]
190    pub fn invalid_operation(message: impl Into<Cow<'static, str>>) -> Self {
191        Self::with_message(ErrorKind::InvalidOperation, message)
192    }
193
194    #[track_caller]
195    pub fn internal(message: impl Into<Cow<'static, str>>) -> Self {
196        Self::with_message(ErrorKind::Internal, message)
197    }
198
199    #[track_caller]
200    pub fn buffer_too_small() -> Self {
201        Self::new(ErrorKind::BufferTooSmall)
202    }
203
204    #[track_caller]
205    pub fn context(self, context: impl Into<Cow<'static, str>>) -> Self {
206        self.push_frame(context.into(), Location::caller())
207    }
208
209    fn push_frame(
210        mut self,
211        message: Cow<'static, str>,
212        location: &'static Location<'static>,
213    ) -> Self {
214        self.inner.frames.push(Frame { message, location });
215        self
216    }
217
218    pub fn message_or(mut self, message: impl Into<Cow<'static, str>>) -> Self {
219        if self.inner.message.as_deref().is_none_or(str::is_empty) {
220            self.inner.message = Some(message.into());
221        }
222
223        self
224    }
225
226    pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
227        self.inner.source = Some(Box::new(source));
228        self
229    }
230
231    pub fn on(self, object: &impl UeHandle) -> Self {
232        self.on_handle(object.raw_handle())
233    }
234
235    pub fn on_handle(mut self, object: UObjectHandle) -> Self {
236        if self.inner.subject.is_none() {
237            let subject = UeSubject {
238                object: object_name(object),
239                class: class_name(class_of(object)),
240            };
241
242            if subject.object.is_some() {
243                self.inner.object = Some(unsafe { crate::ffi_dispatch::core_make_weak(object) });
244            }
245
246            if subject.object.is_some() || subject.class.is_some() {
247                self.inner.subject = Some(subject);
248            }
249        }
250
251        self
252    }
253
254    pub fn on_class(mut self, class: UClassHandle) -> Self {
255        if self.inner.subject.is_none() {
256            self.inner.subject = class_name(class).map(|class| UeSubject {
257                object: None,
258                class: Some(class),
259            });
260        }
261
262        self
263    }
264
265    pub fn kind(&self) -> ErrorKind {
266        self.inner.kind
267    }
268
269    pub fn is(&self, kind: ErrorKind) -> bool {
270        self.inner.kind == kind
271    }
272
273    pub fn message(&self) -> Option<&str> {
274        self.inner.message.as_deref().filter(|m| !m.is_empty())
275    }
276
277    pub fn subject(&self) -> Option<&UeSubject> {
278        self.inner.subject.as_ref()
279    }
280
281    pub fn location(&self) -> &'static Location<'static> {
282        self.inner.location
283    }
284
285    pub fn contexts(&self) -> impl Iterator<Item = (&str, &'static Location<'static>)> {
286        self.inner
287            .frames
288            .iter()
289            .rev()
290            .map(|frame| (frame.message.as_ref(), frame.location))
291    }
292
293    pub fn causes(&self) -> impl Iterator<Item = &(dyn Error + 'static)> {
294        let mut next = self.source();
295
296        std::iter::from_fn(move || {
297            let current = next?;
298            next = current.source();
299            Some(current)
300        })
301    }
302
303    pub fn report(&self) -> String {
304        format!("{self:?}")
305    }
306
307    pub fn engine_message(&self) -> Option<&str> {
308        self.inner.engine.as_deref()
309    }
310
311    pub fn source_file(&self) -> String {
312        absolute_path(self.inner.location.file())
313    }
314
315    pub fn log(&self, level: u8) {
316        self.send(level, false, None);
317    }
318
319    #[doc(hidden)]
320    pub fn __raise(&self, function: &str) {
321        self.send(crate::logging::LOG_ERROR, true, Some(function));
322    }
323
324    #[doc(hidden)]
325    pub fn __log_in(&self, level: u8, what: &str) {
326        self.send(level, false, Some(what));
327    }
328
329    fn send(&self, level: u8, script_error: bool, function: Option<&str>) {
330        if !crate::api::is_api_initialized() {
331            return;
332        }
333
334        let summary = match function {
335            Some(function) => format!("{function} failed: {self}"),
336            None => self.to_string(),
337        };
338
339        let details = match function {
340            Some(function) => format!("{function} failed: {self:?}"),
341            None => format!("{self:?}"),
342        };
343
344        let file = self.source_file();
345
346        let report = RustealErrorReport {
347            level,
348            script_error,
349            summary: summary.as_ptr(),
350            summary_len: summary.len() as u32,
351            details: details.as_ptr(),
352            details_len: details.len() as u32,
353            object: self.inner.object.unwrap_or(FWeakObjectHandle {
354                object_index: -1,
355                object_serial_number: 0,
356            }),
357            file: file.as_ptr(),
358            file_len: file.len() as u32,
359            line: self.inner.location.line(),
360            column: self.inner.location.column(),
361        };
362
363        unsafe {
364            crate::ffi_dispatch::logging_report(&report);
365        }
366    }
367
368    fn wraps_its_source(&self) -> bool {
369        self.inner.kind == ErrorKind::Other
370            && self.message().is_none()
371            && self.inner.source.is_some()
372    }
373
374    fn shown_causes(&self) -> impl Iterator<Item = &(dyn Error + 'static)> {
375        self.causes().skip(usize::from(self.wraps_its_source()))
376    }
377
378    fn fmt_base(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        match (
380            self.inner.kind,
381            self.message(),
382            self.inner.source.as_deref(),
383        ) {
384            (ErrorKind::Other, Some(message), _) => f.write_str(message)?,
385            (ErrorKind::Other, None, Some(source)) => write!(f, "{source}")?,
386            (kind, Some(message), _) => write!(f, "{kind}: {message}")?,
387            (kind, None, _) => write!(f, "{kind}")?,
388        }
389
390        if let Some(subject) = &self.inner.subject {
391            write!(f, " ({subject})")?;
392        }
393
394        if let Some(engine) = &self.inner.engine {
395            write!(f, ": Unreal: {engine}")?;
396        }
397
398        Ok(())
399    }
400}
401
402impl PartialEq<ErrorKind> for RustealError {
403    fn eq(&self, kind: &ErrorKind) -> bool {
404        self.inner.kind == *kind
405    }
406}
407
408impl fmt::Display for RustealError {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        for (context, _) in self.contexts() {
411            write!(f, "{context}: ")?;
412        }
413
414        self.fmt_base(f)?;
415
416        if f.alternate() {
417            for cause in self.shown_causes() {
418                write!(f, ": {cause}")?;
419            }
420
421            write!(f, " [{}]", short_location(self.location()))?;
422        }
423
424        Ok(())
425    }
426}
427
428impl fmt::Debug for RustealError {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        self.fmt_base(f)?;
431        write!(f, "\n    at {}", short_location(self.location()))?;
432
433        let mut last = self.location();
434
435        for frame in &self.inner.frames {
436            write!(f, "\nwhile {}", frame.message)?;
437
438            if frame.location != last {
439                write!(f, "\n    at {}", short_location(frame.location))?;
440            }
441
442            last = frame.location;
443        }
444
445        for cause in self.shown_causes() {
446            write!(f, "\ncaused by: {cause}")?;
447        }
448
449        Ok(())
450    }
451}
452
453impl Error for RustealError {
454    fn source(&self) -> Option<&(dyn Error + 'static)> {
455        self.inner
456            .source
457            .as_deref()
458            .map(|source| source as &(dyn Error + 'static))
459    }
460}
461
462impl From<ErrorKind> for RustealError {
463    #[track_caller]
464    fn from(kind: ErrorKind) -> Self {
465        Self::build(kind, Location::caller())
466    }
467}
468
469impl From<RustealErrorCode> for RustealError {
470    #[track_caller]
471    fn from(code: RustealErrorCode) -> Self {
472        let error = from_code(code, Location::caller());
473
474        match code {
475            RustealErrorCode::Ok => error.message_or("unexpected Ok error code"),
476            _ => error,
477        }
478    }
479}
480
481macro_rules! from_std_errors {
482    ($($error:ty),* $(,)?) => {
483        $(
484            impl From<$error> for RustealError {
485                #[track_caller]
486                fn from(error: $error) -> Self {
487                    into_rusteal(error, Location::caller())
488                }
489            }
490        )*
491    };
492}
493
494from_std_errors!(
495    std::io::Error,
496    std::fmt::Error,
497    std::str::Utf8Error,
498    std::string::FromUtf8Error,
499    std::num::ParseIntError,
500    std::num::ParseFloatError,
501    std::num::TryFromIntError,
502    std::str::ParseBoolError,
503    std::char::ParseCharError,
504);
505
506impl From<Box<dyn Error + Send + Sync + 'static>> for RustealError {
507    #[track_caller]
508    fn from(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
509        match error.downcast::<RustealError>() {
510            Ok(error) => *error,
511            Err(error) => {
512                let mut wrapped = Self::build(ErrorKind::Other, Location::caller());
513                wrapped.inner.source = Some(error);
514                wrapped
515            }
516        }
517    }
518}
519
520fn into_rusteal<E: Error + Send + Sync + 'static>(
521    error: E,
522    location: &'static Location<'static>,
523) -> RustealError {
524    let mut slot = Some(error);
525
526    if let Some(own) = (&mut slot as &mut dyn Any).downcast_mut::<Option<RustealError>>() {
527        return own.take().expect("the slot holds the error");
528    }
529
530    let error = slot.take().expect("the slot holds the error");
531    let mut wrapped = RustealError::build(ErrorKind::Other, location);
532    wrapped.inner.source = Some(Box::new(error));
533    wrapped
534}
535
536pub trait ErrorContext<T> {
537    fn context(self, context: impl Into<Cow<'static, str>>) -> RustealResult<T>;
538
539    fn with_context<C, F>(self, context: F) -> RustealResult<T>
540    where
541        C: Into<Cow<'static, str>>,
542        F: FnOnce() -> C;
543}
544
545impl<T, E: Error + Send + Sync + 'static> ErrorContext<T> for Result<T, E> {
546    #[track_caller]
547    fn context(self, context: impl Into<Cow<'static, str>>) -> RustealResult<T> {
548        let location = Location::caller();
549
550        self.map_err(|error| into_rusteal(error, location).push_frame(context.into(), location))
551    }
552
553    #[track_caller]
554    fn with_context<C, F>(self, context: F) -> RustealResult<T>
555    where
556        C: Into<Cow<'static, str>>,
557        F: FnOnce() -> C,
558    {
559        let location = Location::caller();
560
561        self.map_err(|error| into_rusteal(error, location).push_frame(context().into(), location))
562    }
563}
564
565impl<T> ErrorContext<T> for Option<T> {
566    #[track_caller]
567    fn context(self, context: impl Into<Cow<'static, str>>) -> RustealResult<T> {
568        let location = Location::caller();
569
570        self.ok_or_else(|| RustealError::build(ErrorKind::Other, location).message_or(context))
571    }
572
573    #[track_caller]
574    fn with_context<C, F>(self, context: F) -> RustealResult<T>
575    where
576        C: Into<Cow<'static, str>>,
577        F: FnOnce() -> C,
578    {
579        let location = Location::caller();
580
581        self.ok_or_else(|| RustealError::build(ErrorKind::Other, location).message_or(context()))
582    }
583}
584
585#[macro_export]
586macro_rules! bail {
587    ($kind:path, $($arg:tt)+) => {
588        return ::core::result::Result::Err($crate::RustealError::with_message($kind, format!($($arg)+)).into())
589    };
590    ($($arg:tt)+) => {
591        return ::core::result::Result::Err($crate::RustealError::msg(format!($($arg)+)).into())
592    };
593}
594
595#[macro_export]
596macro_rules! ensure {
597    ($condition:expr, $($arg:tt)+) => {
598        if !$condition {
599            $crate::bail!($($arg)+);
600        }
601    };
602}
603
604fn short_location(location: &Location<'_>) -> String {
605    let file = location.file();
606
607    let short = ["/src/", "\\src\\"]
608        .iter()
609        .filter_map(|marker| file.rfind(marker))
610        .max()
611        .and_then(|src| {
612            let crate_start = file[..src].rfind(['/', '\\']).map_or(0, |i| i + 1);
613            file.get(crate_start..)
614        })
615        .unwrap_or(file);
616
617    format!("{short}:{}:{}", location.line(), location.column())
618}
619
620static SOURCE_ROOT: OnceLock<String> = OnceLock::new();
621
622#[doc(hidden)]
623pub fn set_source_root(crate_dir: &str) {
624    let root = std::path::Path::new(crate_dir)
625        .parent()
626        .unwrap_or(std::path::Path::new(crate_dir));
627
628    let _ = SOURCE_ROOT.set(root.to_string_lossy().into_owned());
629}
630
631pub(crate) fn absolute_path(file: &str) -> String {
632    let path = std::path::Path::new(file);
633
634    match SOURCE_ROOT.get() {
635        Some(root) if path.is_relative() => std::path::Path::new(root)
636            .join(path)
637            .to_string_lossy()
638            .into_owned(),
639        _ => file.to_owned(),
640    }
641}
642
643#[cold]
644fn from_code(code: RustealErrorCode, location: &'static Location<'static>) -> RustealError {
645    let mut error = RustealError::build(code.into(), location);
646
647    if code != RustealErrorCode::ObjectDestroyed {
648        error.inner.engine = engine_message(code);
649    }
650
651    error
652}
653
654fn engine_message(code: RustealErrorCode) -> Option<String> {
655    if !crate::api::is_api_initialized() {
656        return None;
657    }
658
659    let mut buf = vec![0u8; 512];
660    let mut len: u32 = 0;
661
662    let found = unsafe {
663        crate::ffi_dispatch::core_take_error_message(
664            code,
665            buf.as_mut_ptr(),
666            buf.len() as u32,
667            &mut len,
668        )
669    };
670
671    if !found {
672        return None;
673    }
674
675    if len as usize > buf.len() {
676        buf.resize(len as usize, 0);
677
678        unsafe {
679            crate::ffi_dispatch::core_take_error_message(code, buf.as_mut_ptr(), len, &mut len);
680        }
681    }
682
683    buf.truncate(len as usize);
684    Some(String::from_utf8_lossy(&buf).into_owned())
685}
686
687#[cold]
688pub(crate) fn destroyed<T: ?Sized>(location: &'static Location<'static>) -> RustealError {
689    RustealError::build(ErrorKind::ObjectDestroyed, location)
690        .message_or(format!("UObjectRef<{}>", short_type_name::<T>()))
691}
692
693#[cold]
694pub(crate) fn cast_failed(
695    object: UObjectHandle,
696    target: UClassHandle,
697    location: &'static Location<'static>,
698) -> RustealError {
699    let error = RustealError::build(ErrorKind::InvalidCast, location).on_handle(object);
700
701    match class_name(target) {
702        Some(target) => error.message_or(format!("expected {target}")),
703        None => error,
704    }
705}
706
707pub(crate) fn short_type_name<T: ?Sized>() -> &'static str {
708    let full = std::any::type_name::<T>();
709    let base = full.split('<').next().unwrap_or(full);
710    let start = base.rfind("::").map_or(0, |i| i + 2);
711    &full[start..]
712}
713
714pub(crate) fn object_name(object: UObjectHandle) -> Option<String> {
715    if object.is_null()
716        || !crate::api::is_api_initialized()
717        || !unsafe { crate::ffi_dispatch::core_is_valid(object) }
718    {
719        return None;
720    }
721
722    let mut len: u32 = 0;
723
724    let code =
725        unsafe { crate::ffi_dispatch::core_get_name(object, std::ptr::null_mut(), 0, &mut len) };
726
727    if code != RustealErrorCode::Ok {
728        return None;
729    }
730
731    let mut buf = vec![0u8; len as usize];
732
733    let code =
734        unsafe { crate::ffi_dispatch::core_get_name(object, buf.as_mut_ptr(), len, &mut len) };
735
736    (code == RustealErrorCode::Ok).then(|| {
737        buf.truncate(len as usize);
738        String::from_utf8_lossy(&buf).into_owned()
739    })
740}
741
742fn class_of(object: UObjectHandle) -> UClassHandle {
743    if object.is_null()
744        || !crate::api::is_api_initialized()
745        || !unsafe { crate::ffi_dispatch::core_is_valid(object) }
746    {
747        return UClassHandle::null();
748    }
749
750    unsafe { crate::ffi_dispatch::core_get_class(object) }
751}
752
753pub(crate) fn class_name(class: UClassHandle) -> Option<String> {
754    object_name(UObjectHandle(class.0))
755}
756
757#[track_caller]
758pub fn check_ffi(code: RustealErrorCode) -> RustealResult<()> {
759    match code {
760        RustealErrorCode::Ok => Ok(()),
761        other => Err(from_code(other, Location::caller())),
762    }
763}
764
765#[track_caller]
766pub fn check_ffi_ctx(code: RustealErrorCode, context: &str) -> RustealResult<()> {
767    match code {
768        RustealErrorCode::Ok => Ok(()),
769        other => {
770            let error = from_code(other, Location::caller());
771
772            Err(match other {
773                RustealErrorCode::PropertyNotFound
774                | RustealErrorCode::FunctionNotFound
775                | RustealErrorCode::InvalidOperation => error.message_or(context.to_owned()),
776                _ => error.context(context.to_owned()),
777            })
778        }
779    }
780}
781
782#[inline(always)]
783pub fn ffi_infallible(code: RustealErrorCode) {
784    debug_assert_eq!(
785        code,
786        RustealErrorCode::Ok,
787        "FFI call returned {code:?} after pre-validation"
788    );
789}
790
791#[inline(always)]
792pub fn ffi_infallible_ctx(code: RustealErrorCode, ctx: &str) {
793    debug_assert_eq!(
794        code,
795        RustealErrorCode::Ok,
796        "FFI '{ctx}' returned {code:?} after pre-validation"
797    );
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn check_ffi_ok_returns_ok() {
806        assert!(check_ffi(RustealErrorCode::Ok).is_ok());
807    }
808
809    #[test]
810    fn check_ffi_errors_map_to_their_kind() {
811        let cases = [
812            (
813                RustealErrorCode::ObjectDestroyed,
814                ErrorKind::ObjectDestroyed,
815            ),
816            (RustealErrorCode::InvalidCast, ErrorKind::InvalidCast),
817            (
818                RustealErrorCode::PropertyNotFound,
819                ErrorKind::PropertyNotFound,
820            ),
821            (
822                RustealErrorCode::FunctionNotFound,
823                ErrorKind::FunctionNotFound,
824            ),
825            (RustealErrorCode::TypeMismatch, ErrorKind::TypeMismatch),
826            (RustealErrorCode::NullArgument, ErrorKind::NullArgument),
827            (
828                RustealErrorCode::IndexOutOfRange,
829                ErrorKind::IndexOutOfRange,
830            ),
831            (
832                RustealErrorCode::InvalidOperation,
833                ErrorKind::InvalidOperation,
834            ),
835            (RustealErrorCode::InternalError, ErrorKind::Internal),
836            (RustealErrorCode::BufferTooSmall, ErrorKind::BufferTooSmall),
837        ];
838
839        for (code, kind) in cases {
840            let error = check_ffi(code).unwrap_err();
841            assert_eq!(error.kind(), kind);
842            assert!(error == kind);
843        }
844    }
845
846    #[test]
847    fn the_name_given_to_check_ffi_ctx_is_the_message() {
848        let error = check_ffi_ctx(RustealErrorCode::PropertyNotFound, "Health").unwrap_err();
849        assert_eq!(error.to_string(), "property not found: Health");
850
851        let error = check_ffi_ctx(RustealErrorCode::TypeMismatch, "Health").unwrap_err();
852        assert_eq!(error.to_string(), "Health: type mismatch");
853    }
854
855    #[test]
856    fn display_is_one_human_readable_line() {
857        assert_eq!(
858            RustealError::property_not_found("Health").to_string(),
859            "property not found: Health"
860        );
861
862        assert_eq!(RustealError::invalid_cast().to_string(), "invalid cast");
863        assert_eq!(
864            RustealError::msg("no save slot").to_string(),
865            "no save slot"
866        );
867    }
868
869    #[test]
870    fn the_location_is_where_the_error_was_made() {
871        let line = line!() + 1;
872        let error = RustealError::null_argument();
873        assert_eq!(error.location().line(), line);
874        assert!(error.location().file().ends_with("error.rs"));
875    }
876
877    fn parse(text: &str) -> (u32, RustealResult<i32>) {
878        let line = line!() + 1;
879        let result = (|| -> RustealResult<i32> { Ok(text.parse::<i32>()?) })();
880        (line, result)
881    }
882
883    #[test]
884    fn question_mark_converts_std_errors_where_it_is() {
885        let (line, result) = parse("x");
886        let error = result.unwrap_err();
887
888        assert_eq!(error.kind(), ErrorKind::Other);
889        assert_eq!(error.location().line(), line);
890        assert_eq!(error.causes().count(), 1);
891        assert_eq!(error.to_string(), "invalid digit found in string");
892    }
893
894    #[test]
895    fn context_frames_read_outermost_first() {
896        let error = Err::<(), _>(RustealError::property_not_found("Health"))
897            .context("reading the enemy")
898            .context("applying damage")
899            .unwrap_err();
900
901        assert_eq!(error.kind(), ErrorKind::PropertyNotFound);
902
903        assert_eq!(
904            error.to_string(),
905            "applying damage: reading the enemy: property not found: Health"
906        );
907
908        let contexts: Vec<&str> = error.contexts().map(|(c, _)| c).collect();
909        assert_eq!(contexts, ["applying damage", "reading the enemy"]);
910    }
911
912    #[test]
913    fn context_wraps_foreign_errors_as_their_cause() {
914        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "SAVE.sav missing");
915
916        let error = Err::<(), _>(io).context("loading the save").unwrap_err();
917
918        assert_eq!(error.kind(), ErrorKind::Other);
919        assert_eq!(error.to_string(), "loading the save: SAVE.sav missing");
920        assert_eq!(
921            error.causes().next().unwrap().to_string(),
922            "SAVE.sav missing"
923        );
924
925        let alternate = format!("{error:#}");
926        assert!(alternate.starts_with("loading the save: SAVE.sav missing ["));
927        assert!(alternate.contains("rusteal-core/src/error.rs:"));
928    }
929
930    #[test]
931    fn option_context_makes_an_error() {
932        let error = None::<i32>.context("no player pawn").unwrap_err();
933        assert_eq!(error.kind(), ErrorKind::Other);
934        assert_eq!(error.to_string(), "no player pawn");
935    }
936
937    #[test]
938    fn debug_is_a_report_with_locations() {
939        let error = Err::<(), _>(RustealError::function_not_found("Interact"))
940            .context("using the door")
941            .unwrap_err();
942
943        let report = format!("{error:?}");
944        let lines: Vec<&str> = report.lines().collect();
945
946        assert_eq!(lines[0], "function not found: Interact");
947        assert!(lines[1].starts_with("    at rusteal-core/src/error.rs:"));
948        assert_eq!(lines[2], "while using the door");
949        assert!(lines[3].starts_with("    at rusteal-core/src/error.rs:"));
950    }
951
952    #[test]
953    fn a_context_on_the_line_of_the_error_gives_no_second_location() {
954        let error = "x".parse::<i32>().context("reading the ammo").unwrap_err();
955        let report = format!("{error:?}");
956
957        assert_eq!(report.matches("    at ").count(), 1, "{report}");
958        assert!(report.contains("\nwhile reading the ammo"));
959    }
960
961    #[test]
962    fn a_rusteal_error_given_to_other_or_boxed_stays_itself() {
963        let original = RustealError::index_out_of_range().context("slot 9");
964        let line = original.location().line();
965        let error = RustealError::other(original);
966        assert_eq!(error.kind(), ErrorKind::IndexOutOfRange);
967        assert_eq!(error.location().line(), line);
968
969        let boxed: Box<dyn Error + Send + Sync> = Box::new(RustealError::type_mismatch());
970        assert_eq!(RustealError::from(boxed).kind(), ErrorKind::TypeMismatch);
971    }
972
973    #[test]
974    fn subjects_name_the_object_and_its_class() {
975        let subject = UeSubject {
976            object: Some("BP_Enemy_C_0".into()),
977            class: Some("BP_Enemy_C".into()),
978        };
979
980        let mut error = RustealError::property_not_found("Health");
981        error.inner.subject = Some(subject);
982
983        assert_eq!(
984            error.to_string(),
985            "property not found: Health (object BP_Enemy_C_0 of class BP_Enemy_C)"
986        );
987    }
988
989    #[test]
990    fn the_engine_reason_follows_the_object() {
991        let mut error = RustealError::type_mismatch();
992        error.inner.subject = Some(UeSubject {
993            object: Some("BP_Enemy_C_0".into()),
994            class: Some("BP_Enemy_C".into()),
995        });
996        error.inner.engine =
997            Some("Health, a FloatProperty of BP_Enemy_C, is not a FBoolProperty".into());
998
999        assert_eq!(
1000            error.to_string(),
1001            "type mismatch (object BP_Enemy_C_0 of class BP_Enemy_C): Unreal: Health, a \
1002             FloatProperty of BP_Enemy_C, is not a FBoolProperty"
1003        );
1004        assert_eq!(error.engine_message().unwrap().len(), 61);
1005    }
1006
1007    #[test]
1008    fn source_files_are_made_absolute_from_the_crate_root() {
1009        set_source_root("/project/Rust/my-game");
1010        assert_eq!(
1011            absolute_path("my-game/src/turret.rs"),
1012            "/project/Rust/my-game/src/turret.rs"
1013        );
1014        assert_eq!(
1015            absolute_path("/abs/rusteal-core/src/x.rs"),
1016            "/abs/rusteal-core/src/x.rs"
1017        );
1018    }
1019
1020    #[test]
1021    fn without_the_engine_no_subject_is_added() {
1022        let error = RustealError::invalid_cast().on_handle(UObjectHandle::null());
1023        assert!(error.subject().is_none());
1024    }
1025
1026    #[test]
1027    fn bail_and_ensure_return_errors() {
1028        fn check(health: i32) -> RustealResult<i32> {
1029            ensure!(health >= 0, "health is {health}");
1030
1031            if health > 100 {
1032                bail!(
1033                    ErrorKind::InvalidOperation,
1034                    "health {health} over the maximum"
1035                );
1036            }
1037
1038            Ok(health)
1039        }
1040
1041        assert_eq!(check(5).unwrap(), 5);
1042        assert_eq!(check(-1).unwrap_err().to_string(), "health is -1");
1043
1044        assert_eq!(
1045            check(101).unwrap_err().to_string(),
1046            "invalid operation: health 101 over the maximum"
1047        );
1048    }
1049
1050    #[test]
1051    fn locations_are_shortened_to_the_crate() {
1052        let location = Location::caller();
1053        assert!(short_location(location).starts_with("rusteal-core/src/error.rs:"));
1054    }
1055
1056    #[test]
1057    fn the_error_is_one_pointer_and_thread_safe() {
1058        fn assert_send_sync<T: Send + Sync + 'static>() {}
1059        assert_send_sync::<RustealError>();
1060        assert_eq!(size_of::<RustealResult<()>>(), size_of::<usize>());
1061    }
1062}