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}