#![cfg(all(target_pointer_width = "64", not(feature = "catch-all")))] #![allow(dead_code)] //! Test that our use of #[track_caller] is making the correct line number //! show up. use std::panic; use std::process::abort; use std::ptr; use std::sync::Mutex; use objc2::encode::Encode; use objc2::rc::{self, Allocated, Retained}; use objc2::runtime::{self, NSObject}; use objc2::{class, define_class, msg_send, AnyThread, ClassType}; #[path = "../src/rc/test_object.rs"] #[allow(dead_code)] mod test_object; use self::test_object::RcTestObject; static EXPECTED_MESSAGE: Mutex = Mutex::new(String::new()); static EXPECTED_LINE: Mutex = Mutex::new(0); struct PanicChecker(()); impl PanicChecker { fn new() -> Self { panic::set_hook(Box::new(|info| { let expected_message = EXPECTED_MESSAGE.lock().unwrap(); let expected_line = EXPECTED_LINE.lock().unwrap(); let payload = info.payload(); let message = if let Some(payload) = payload.downcast_ref::<&'static str>() { payload.to_string() } else if let Some(payload) = payload.downcast_ref::() { payload.clone() } else { format!("could not extract message: {payload:?}") }; let location = info.location().expect("location"); if !message.contains(&*expected_message) { eprintln!("expected {expected_message:?}, got: {message:?}"); abort(); } if location.file() != file!() { eprintln!("expected file {:?}, got: {:?}", file!(), location.file()); abort(); } if location.line() != *expected_line { eprintln!("expected line {expected_line}, got: {}", location.line()); abort(); } })); Self(()) } fn assert_panics(&self, message: &str, line: u32, f: impl FnOnce()) { *EXPECTED_MESSAGE.lock().unwrap() = message.to_string(); *EXPECTED_LINE.lock().unwrap() = line; let res = panic::catch_unwind(panic::AssertUnwindSafe(|| { f(); })); assert!(res.is_err()); *EXPECTED_MESSAGE.lock().unwrap() = "unknown".to_string(); *EXPECTED_LINE.lock().unwrap() = 0; } } impl Drop for PanicChecker { fn drop(&mut self) { let _ = panic::take_hook(); } } #[test] #[cfg_attr(panic = "abort", ignore = "requires `catch_unwind`")] fn test_track_caller() { let checker = PanicChecker::new(); if cfg!(debug_assertions) { test_nil(&checker); if cfg!(not(feature = "disable-encoding-assertions")) { test_verify(&checker); test_error_methods(&checker); } } test_retained_unwrap(&checker); if cfg!(feature = "catch-all") { test_catch_all(&checker); } test_unwind(&checker); #[cfg(not(feature = "unstable-static-class"))] test_unknown_class(&checker); } fn test_nil(checker: &PanicChecker) { let nil: *mut NSObject = ptr::null_mut(); let msg = "messsaging description to nil"; checker.assert_panics(msg, line!() + 1, || { let _: *mut NSObject = unsafe { msg_send![nil, description] }; }); checker.assert_panics(msg, line!() + 1, || { let _: *mut NSObject = unsafe { msg_send![super(nil, NSObject::class()), description] }; }); checker.assert_panics(msg, line!() + 1, || { let _: Option> = unsafe { msg_send![nil, description] }; }); } fn test_verify(checker: &PanicChecker) { let obj = NSObject::new(); let msg = "invalid message send to -[NSObject description]: expected return to have type code '@', but found 'v'"; checker.assert_panics(msg, line!() + 1, || { let _: () = unsafe { msg_send![&obj, description] }; }); let msg = format!("invalid message send to -[NSObject hash]: expected return to have type code '{}', but found '@'", usize::ENCODING); checker.assert_panics(&msg, line!() + 1, || { let _: Option> = unsafe { msg_send![&obj, hash] }; }); } fn test_error_methods(checker: &PanicChecker) { let nil: *mut NSObject = ptr::null_mut(); let msg = "messsaging someSelectorWithError: to nil"; checker.assert_panics(msg, line!() + 1, || { let _: Result<(), Retained> = unsafe { msg_send![nil, someSelectorWithError: _] }; }); checker.assert_panics(msg, line!() + 2, || { let _: Result<(), Retained> = unsafe { msg_send![super(nil, NSObject::class()), someSelectorWithError: _] }; }); checker.assert_panics(msg, line!() + 2, || { let _: Result, Retained> = unsafe { msg_send![nil, someSelectorWithError: _] }; }); let msg = "invalid message send to -[NSObject someSelectorWithError:]: method not found"; checker.assert_panics(msg, line!() + 3, || { let obj = RcTestObject::new(); let _: Result<(), Retained> = unsafe { msg_send![super(&obj), someSelectorWithError: _] }; }); } fn test_retained_unwrap(checker: &PanicChecker) { let cls = RcTestObject::class(); let obj = RcTestObject::new(); let msg = "failed creating new instance using +[__RcTestObject newReturningNull]"; checker.assert_panics(msg, line!() + 1, || { let _obj: Retained = unsafe { msg_send![cls, newReturningNull] }; }); let msg = if cfg!(debug_assertions) { "messsaging init to nil" } else { "failed allocating object" }; checker.assert_panics(msg, line!() + 2, || { let obj: Allocated = unsafe { msg_send![cls, allocReturningNull] }; let _obj: Retained = unsafe { msg_send![obj, init] }; }); let msg = "failed initializing object with -initReturningNull"; checker.assert_panics(msg, line!() + 2, || { let _obj: Retained = unsafe { msg_send![RcTestObject::alloc(), initReturningNull] }; }); let msg = "failed copying object"; checker.assert_panics(msg, line!() + 1, || { let _obj: Retained = unsafe { msg_send![&obj, copyReturningNull] }; }); let msg = "unexpected NULL returned from -[__RcTestObject methodReturningNull]"; checker.assert_panics(msg, line!() + 1, || { let _obj: Retained = unsafe { msg_send![&obj, methodReturningNull] }; }); } fn test_catch_all(checker: &PanicChecker) { let obj: Retained = unsafe { msg_send![class!(NSArray), new] }; let msg = "NSRangeException"; checker.assert_panics(msg, line!() + 1, || { let _: *mut NSObject = unsafe { msg_send![&obj, objectAtIndex: 0usize] }; }); let msg = "NSRangeException"; checker.assert_panics(msg, line!() + 1, || { let _: Retained = unsafe { msg_send![&obj, objectAtIndex: 0usize] }; }); } define_class!( #[unsafe(super(NSObject))] struct PanickingClass; impl PanickingClass { #[unsafe(method(panic))] fn _panic() -> *mut Self { panic!("panic in PanickingClass") } } ); fn test_unwind(checker: &PanicChecker) { let msg = "panic in PanickingClass"; let line = line!() - 7; checker.assert_panics(msg, line, || { let _: *mut NSObject = unsafe { msg_send![PanickingClass::class(), panic] }; }); checker.assert_panics(msg, line, || { let _: Retained = unsafe { msg_send![PanickingClass::class(), panic] }; }); } #[cfg(not(feature = "unstable-static-class"))] fn test_unknown_class(checker: &PanicChecker) { let msg = "class NonExistentClass could not be found"; checker.assert_panics(msg, line!() + 1, || { let _ = class!(NonExistentClass); }); }