Skip to content

Commit f7cf30a

Browse files
committed
Handle panics occurring in GDScript tests; trace logging
1 parent 3bd4266 commit f7cf30a

File tree

8 files changed

+70
-22
lines changed

8 files changed

+70
-22
lines changed

godot-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ pub mod private {
8282
&& *global_config.is_editor.get_or_init(is_editor)
8383
}
8484

85-
fn print_panic(err: Box<dyn std::any::Any + Send>) {
85+
pub fn print_panic(err: Box<dyn std::any::Any + Send>) {
8686
if let Some(s) = err.downcast_ref::<&'static str>() {
8787
print_panic_message(s);
8888
} else if let Some(s) = err.downcast_ref::<String>() {

godot-core/src/obj/gd.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,8 @@ where
406406
let ref_counted = T::Mem::is_ref_counted(&self.raw);
407407
assert_ne!(
408408
ref_counted, Some(true),
409-
"called free() on Gd<Object> which points to a RefCounted dynamic type; free() only supported for manually managed types."
409+
"called free() on Gd<Object> which points to a RefCounted dynamic type; free() only supported for manually managed types\n\
410+
object: {self:?}"
410411
);
411412

412413
// If ref_counted returned None, that means the instance was destroyed

godot-core/src/obj/guards.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7+
use godot_ffi::out;
8+
79
#[cfg(not(feature = "experimental-threads"))]
810
use std::cell;
911
use std::fmt::Debug;
@@ -31,6 +33,7 @@ impl<'a, T> GdRef<'a, T> {
3133

3234
#[cfg(feature = "experimental-threads")]
3335
pub(crate) fn from_cell(cell_ref: sync::RwLockReadGuard<'a, T>) -> Self {
36+
out!("GdRef init: {:?}", std::any::type_name::<T>());
3437
Self { cell_ref }
3538
}
3639
}
@@ -43,6 +46,12 @@ impl<T> Deref for GdRef<'_, T> {
4346
}
4447
}
4548

49+
impl<T> Drop for GdRef<'_, T> {
50+
fn drop(&mut self) {
51+
out!("GdRef drop: {:?}", std::any::type_name::<T>());
52+
}
53+
}
54+
4655
// TODO Clone or Share
4756

4857
// ----------------------------------------------------------------------------------------------------------------------------------------------
@@ -67,6 +76,7 @@ impl<'a, T> GdMut<'a, T> {
6776

6877
#[cfg(feature = "experimental-threads")]
6978
pub(crate) fn from_cell(cell_ref: sync::RwLockWriteGuard<'a, T>) -> Self {
79+
out!("GdMut init: {:?}", std::any::type_name::<T>());
7080
Self { cell_ref }
7181
}
7282
}
@@ -84,3 +94,9 @@ impl<T> DerefMut for GdMut<'_, T> {
8494
self.cell_ref.deref_mut()
8595
}
8696
}
97+
98+
impl<T> Drop for GdMut<'_, T> {
99+
fn drop(&mut self) {
100+
out!("GdMut drop: {:?}", std::any::type_name::<T>());
101+
}
102+
}

godot-core/src/obj/raw.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,8 @@ impl<T: GodotClass> Drop for RawGd<T> {
461461
fn drop(&mut self) {
462462
// No-op for manually managed objects
463463

464-
out!("RawGd::drop <{}>", std::any::type_name::<T>());
464+
// out!("RawGd::drop <{}>", std::any::type_name::<T>());
465+
465466
// SAFETY: This `Gd` wont be dropped again after this.
466467
let is_last = unsafe { T::Mem::maybe_dec_ref(self) }; // may drop
467468
if is_last {

godot-core/src/storage.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mod single_threaded {
3737
/// Manages storage and lifecycle of user's extension class instances.
3838
pub struct InstanceStorage<T: GodotClass> {
3939
user_instance: cell::RefCell<T>,
40-
base: Base<T::Base>,
40+
pub(super) base: Base<T::Base>,
4141

4242
// Declared after `user_instance`, is dropped last
4343
pub(super) lifecycle: cell::Cell<Lifecycle>,
@@ -160,7 +160,7 @@ mod multi_threaded {
160160
/// Manages storage and lifecycle of user's extension class instances.
161161
pub struct InstanceStorage<T: GodotClass> {
162162
user_instance: sync::RwLock<T>,
163-
base: Base<T::Base>,
163+
pub(super) base: Base<T::Base>,
164164

165165
// Declared after `user_instance`, is dropped last
166166
pub(super) lifecycle: AtomicLifecycle,
@@ -256,6 +256,12 @@ mod multi_threaded {
256256
}
257257

258258
impl<T: GodotClass> InstanceStorage<T> {
259+
pub fn debug_info(&self) -> String {
260+
// Unlike get_gd(), this doesn't require special trait bounds.
261+
262+
format!("{:?}", self.base)
263+
}
264+
259265
#[must_use]
260266
pub fn into_raw(self) -> *mut Self {
261267
Box::into_raw(Box::new(self))
@@ -268,18 +274,20 @@ impl<T: GodotClass> InstanceStorage<T> {
268274
);
269275
self.lifecycle.set(Lifecycle::Destroying);
270276
out!(
271-
" mark; self={:?}, val={:?}",
277+
" mark; self={:?}, val={:?}, obj={:?}",
272278
self as *const _,
273-
self.lifecycle.get()
279+
self.lifecycle.get(),
280+
self.base,
274281
);
275282
}
276283

277284
#[inline(always)]
278285
pub fn destroyed_by_godot(&self) -> bool {
279286
out!(
280-
" is_d; self={:?}, val={:?}",
287+
" is_d; self={:?}, val={:?}, obj={:?}",
281288
self as *const _,
282-
self.lifecycle.get()
289+
self.lifecycle.get(),
290+
self.base,
283291
);
284292
matches!(
285293
self.lifecycle.get(),
@@ -325,7 +333,9 @@ pub unsafe fn destroy_storage<T: GodotClass>(instance_ptr: sys::GDExtensionClass
325333

326334
assert!(
327335
!(*raw).is_bound(),
328-
"tried to destroy object while a bind() or bind_mut() call is active"
336+
"tried to destroy object while a bind() or bind_mut() call is active\n \
337+
object: {}",
338+
(*raw).debug_info()
329339
);
330340

331341
let _drop = Box::from_raw(raw);

itest/godot/ManualFfiTests.gd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,14 @@ func test_custom_property():
254254
func test_custom_property_wrong_values_1():
255255
var has_property: HasCustomProperty = HasCustomProperty.new()
256256
disable_error_messages()
257-
has_property.some_c_style_enum = 10
257+
has_property.some_c_style_enum = 10 # Should fail.
258258
enable_error_messages()
259259
assert_fail("HasCustomProperty.some_c_style_enum should only accept integers in the range `(0 ..= 2)`")
260260

261261
func test_custom_property_wrong_values_2():
262262
var has_property: HasCustomProperty = HasCustomProperty.new()
263263
disable_error_messages()
264-
has_property.not_exportable = {"a": "hello", "b": Callable()}
264+
has_property.not_exportable = {"a": "hello", "b": Callable()} # Should fail.
265265
enable_error_messages()
266266
assert_fail("HasCustomProperty.not_exportable should only accept dictionaries with float values")
267267

itest/godot/TestRunner.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class GDScriptTestCase:
8989
var script: GDScript = suite.get_script()
9090
return str(script.resource_path.get_file().get_basename(), ".gd")
9191

92-
# Standard test case used for whenever something can be tested by just running a gdscript function.
92+
# Standard test case used for whenever something can be tested by just running a GDScript function.
9393
class GDScriptExecutableTestCase extends GDScriptTestCase:
9494
func run():
9595
# This is a no-op if the suite doesn't have this property.

itest/rust/src/framework/runner.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,40 @@ impl IntegrationTests {
153153
let test_case = get_property(&test, "method_name");
154154

155155
print_test_pre(&test_case, test_file.clone(), &mut last_file, true);
156-
let result = test.call("run", &[]);
157-
// In case a test needs to disable error messages to ensure it runs properly.
156+
157+
// If GDScript invokes Rust code that fails, the panic would break through; catch it.
158+
// TODO(bromeon): use try_call() once available.
159+
let result = std::panic::catch_unwind(|| test.call("run", &[]));
160+
161+
// In case a test needs to disable error messages, to ensure it runs properly.
158162
Engine::singleton().set_print_error_messages(true);
159163

160164
if let Some(duration) = get_execution_time(&test) {
161165
extra_duration += duration;
162166
}
163-
let success = result.try_to::<bool>().unwrap_or_else(|_| {
164-
panic!("GDScript test case {test} returned non-bool: {result}")
165-
});
166-
for error in get_errors(&test).iter_shared() {
167-
godot_error!("{error}");
168-
}
169-
let outcome = TestOutcome::from_bool(success);
167+
168+
let outcome = match result {
169+
Ok(result) => {
170+
let success = result.try_to::<bool>().unwrap_or_else(|_| {
171+
// Not a failing test, but an error in the test setup.
172+
panic!("GDScript test case {test} returned non-bool: {result}")
173+
});
174+
175+
for error in get_errors(&test).iter_shared() {
176+
godot_error!("{error}");
177+
}
178+
TestOutcome::from_bool(success)
179+
}
180+
Err(e) => {
181+
// TODO(bromeon) should this be a fatal error, i.e. panicking and aborting tests -> bad test setup?
182+
// If GDScript receives panics, this can also happen in user code that is _not_ invoked from Rust, and thus a panic
183+
// could not be caught, causing UB at the Godot FFI boundary (in practice, this will be a defined Godot crash with
184+
// stack trace though).
185+
godot_error!("GDScript test panicked");
186+
godot::private::print_panic(e);
187+
TestOutcome::Failed
188+
}
189+
};
170190

171191
self.update_stats(&outcome, &test_file, &test_case);
172192
print_test_post(&test_case, outcome);

0 commit comments

Comments
 (0)