Skip to content

Commit 48fbb94

Browse files
bors[bot]Bromeon
andauthored
Merge #164
164: Inject scene tree into `#[itest]` r=Bromeon a=Bromeon Also adds another test for `Gd::eq()` in the case of dead instances, and a stub for testing #23. Simplifies the proc-macro machinery further. Co-authored-by: Jan Haller <[email protected]>
2 parents ac5f78c + b150928 commit 48fbb94

File tree

9 files changed

+191
-56
lines changed

9 files changed

+191
-56
lines changed

godot-core/src/obj/gd.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ impl<T: GodotClass> Gd<T> {
220220
// Initialize instance ID cache
221221
let id = unsafe { interface_fn!(object_get_instance_id)(obj.obj_sys()) };
222222
let instance_id = InstanceId::try_from_u64(id)
223-
.expect("instance ID must be non-zero at time of initialization");
223+
.expect("Gd initialization failed; did you call share() on a dead instance?");
224224
obj.cached_instance_id.set(Some(instance_id));
225225

226226
obj
@@ -452,7 +452,7 @@ where
452452
// If ref_counted returned None, that means the instance was destroyed
453453
assert!(
454454
ref_counted == Some(false) && self.is_instance_valid(),
455-
"called free() on already destroyed obj"
455+
"called free() on already destroyed object"
456456
);
457457

458458
// This destroys the Storage instance, no need to run destructor again
@@ -671,3 +671,8 @@ impl<T: GodotClass> VariantMetadata for Gd<T> {
671671
ClassName::of::<T>()
672672
}
673673
}
674+
675+
// Gd unwinding across panics does not invalidate any invariants;
676+
// its mutability is anyway present, in the Godot engine.
677+
impl<T: GodotClass> std::panic::UnwindSafe for Gd<T> {}
678+
impl<T: GodotClass> std::panic::RefUnwindSafe for Gd<T> {}

godot-macros/src/gdextension.rs

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

7-
use crate::util::{bail, ident, parse_kv_group, path_is_single, validate_impl, KvValue};
87
use proc_macro2::TokenStream;
98
use quote::quote;
109
use venial::Declaration;
1110

12-
pub fn transform(decl: Declaration) -> Result<TokenStream, venial::Error> {
11+
use crate::util::{bail, ident, validate_impl, KvParser};
12+
use crate::ParseResult;
13+
14+
pub fn transform(decl: Declaration) -> ParseResult<TokenStream> {
1315
let mut impl_decl = match decl {
1416
Declaration::Impl(item) => item,
1517
_ => return bail("#[gdextension] can only be applied to trait impls", &decl),
@@ -23,18 +25,10 @@ pub fn transform(decl: Declaration) -> Result<TokenStream, venial::Error> {
2325
);
2426
}
2527

26-
let mut entry_point = None;
27-
for attr in impl_decl.attributes.drain(..) {
28-
if path_is_single(&attr.path, "gdextension") {
29-
for (k, v) in parse_kv_group(&attr.value).expect("#[gdextension] has invalid arguments")
30-
{
31-
match (k.as_str(), v) {
32-
("entry_point", KvValue::Ident(f)) => entry_point = Some(f),
33-
_ => return bail(format!("#[gdextension]: invalid argument `{k}`"), attr),
34-
}
35-
}
36-
}
37-
}
28+
let drained_attributes = std::mem::take(&mut impl_decl.attributes);
29+
let mut parser = KvParser::parse_required(&drained_attributes, "gdextension", &impl_decl)?;
30+
let entry_point = parser.handle_ident("entry_point")?;
31+
parser.finish()?;
3832

3933
let entry_point = entry_point.unwrap_or_else(|| ident("gdextension_rust_init"));
4034
let impl_ty = &impl_decl.self_ty;

godot-macros/src/itest.rs

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

7-
use crate::util::{bail, KvParser};
8-
use crate::ParseResult;
97
use proc_macro2::TokenStream;
10-
use quote::quote;
11-
use venial::Declaration;
8+
use quote::{quote, ToTokens};
9+
use venial::{Declaration, Error, FnParam, Function};
10+
11+
use crate::util::{bail, path_ends_with, KvParser};
12+
use crate::ParseResult;
1213

1314
pub fn transform(input_decl: Declaration) -> ParseResult<TokenStream> {
1415
let func = match input_decl {
@@ -18,14 +19,11 @@ pub fn transform(input_decl: Declaration) -> ParseResult<TokenStream> {
1819

1920
// Note: allow attributes for things like #[rustfmt] or #[clippy]
2021
if func.generic_params.is_some()
21-
|| !func.params.is_empty()
22+
|| func.params.len() > 1
2223
|| func.return_ty.is_some()
2324
|| func.where_clause.is_some()
2425
{
25-
return bail(
26-
format!("#[itest] must be of form: fn {}() {{ ... }}", func.name),
27-
&func,
28-
);
26+
return bad_signature(&func);
2927
}
3028

3129
let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
@@ -42,10 +40,27 @@ pub fn transform(input_decl: Declaration) -> ParseResult<TokenStream> {
4240

4341
let test_name = &func.name;
4442
let test_name_str = func.name.to_string();
43+
44+
// Detect parameter name chosen by user, or unused fallback
45+
let param = if let Some((param, _punct)) = func.params.first() {
46+
if let FnParam::Typed(param) = param {
47+
// Correct parameter type (crude macro check) -> reuse parameter name
48+
if path_ends_with(&param.ty.tokens, "TestContext") {
49+
param.to_token_stream()
50+
} else {
51+
return bad_signature(&func);
52+
}
53+
} else {
54+
return bad_signature(&func);
55+
}
56+
} else {
57+
quote! { __unused_context: &crate::TestContext }
58+
};
59+
4560
let body = &func.body;
4661

4762
Ok(quote! {
48-
pub fn #test_name() {
63+
pub fn #test_name(#param) {
4964
#body
5065
}
5166

@@ -59,3 +74,15 @@ pub fn transform(input_decl: Declaration) -> ParseResult<TokenStream> {
5974
});
6075
})
6176
}
77+
78+
fn bad_signature(func: &Function) -> Result<TokenStream, Error> {
79+
bail(
80+
format!(
81+
"#[itest] function must have one of these signatures:\
82+
\n fn {f}() {{ ... }}\
83+
\n fn {f}(ctx: &TestContext) {{ ... }}",
84+
f = func.name
85+
),
86+
&func,
87+
)
88+
}

godot-macros/src/util.rs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,22 @@ pub(crate) struct KvParser {
6767
finished: bool,
6868
}
6969

70+
#[allow(dead_code)] // some functions will be used later
7071
impl KvParser {
7172
/// Create a new parser which requires a `#[expected]` attribute.
7273
///
7374
/// `context` is used for the span in error messages.
74-
#[allow(dead_code)] // will be used later
7575
pub fn parse_required(
7676
attributes: &[Attribute],
7777
expected: &str,
7878
context: impl ToTokens,
7979
) -> ParseResult<Self> {
8080
match Self::parse(attributes, expected) {
8181
Ok(Some(result)) => Ok(result),
82-
Ok(None) => {
83-
return bail(
84-
format!("expected attribute #[{expected}], but not present"),
85-
context,
86-
)
87-
}
82+
Ok(None) => bail(
83+
format!("expected attribute #[{expected}], but not present"),
84+
context,
85+
),
8886
Err(e) => Err(e),
8987
}
9088
}
@@ -147,11 +145,26 @@ impl KvParser {
147145
}
148146
}
149147

148+
/// `#[attr(key="string", key2=123, key3=true)]`, with a given key being required
149+
pub fn handle_ident_required(&mut self, key: &str) -> ParseResult<Ident> {
150+
self.inner_required(key, "ident", Self::handle_ident)
151+
}
152+
150153
/// `#[attr(key="string", key2=123, key3=true)]`, with a given key being required
151154
pub fn handle_lit_required(&mut self, key: &str) -> ParseResult<String> {
152-
match self.handle_lit(key) {
155+
self.inner_required(key, "literal", Self::handle_lit)
156+
}
157+
158+
fn inner_required<T, F>(&mut self, key: &str, context: &str, mut f: F) -> ParseResult<T>
159+
where
160+
F: FnMut(&mut Self, &str) -> ParseResult<Option<T>>,
161+
{
162+
match f(self, key) {
153163
Ok(Some(string)) => Ok(string),
154-
Ok(None) => self.bail_key(key, "expected to have literal value, but is absent"),
164+
Ok(None) => self.bail_key(
165+
key,
166+
&format!("expected to have {context} value, but is absent"),
167+
),
155168
Err(err) => Err(err),
156169
}
157170
}
@@ -171,21 +184,21 @@ impl KvParser {
171184
let keys = self.map.keys().cloned().collect::<Vec<_>>().join(", ");
172185

173186
let s = if self.map.len() > 1 { "s" } else { "" }; // plural
174-
return bail(
187+
bail(
175188
format!(
176189
"#[{attr}]: unrecognized key{s}: {keys}",
177190
attr = self.attr_name
178191
),
179192
self.span,
180-
);
193+
)
181194
}
182195
}
183196

184197
fn bail_key<R>(&self, key: &str, msg: &str) -> ParseResult<R> {
185-
return bail(
198+
bail(
186199
format!("#[{attr}]: key `{key}` {msg}", attr = self.attr_name),
187200
self.span,
188-
);
201+
)
189202
}
190203
}
191204

@@ -486,3 +499,10 @@ mod tests {
486499
pub(crate) fn path_is_single(path: &Vec<TokenTree>, expected: &str) -> bool {
487500
path.len() == 1 && path[0].to_string() == expected
488501
}
502+
503+
pub(crate) fn path_ends_with(path: &Vec<TokenTree>, expected: &str) -> bool {
504+
// could also use .as_path()
505+
path.last()
506+
.map(|last| last.to_string() == expected)
507+
.unwrap_or(false)
508+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
list=[{
1+
list=Array[Dictionary]([{
22
"base": &"RefCounted",
33
"class": &"TestStats",
44
"icon": "",
55
"language": &"GDScript",
66
"path": "res://TestStats.gd"
77
}, {
8-
"base": &"Node",
8+
"base": &"RefCounted",
99
"class": &"TestSuite",
1010
"icon": "",
1111
"language": &"GDScript",
1212
"path": "res://TestSuite.gd"
13-
}]
13+
}])

itest/godot/TestRunner.gd

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ func _ready():
3333
if method_name.begins_with("test_"):
3434
gdscript_tests.push_back(GDScriptTestCase.new(suite, method_name))
3535

36-
var success: bool = rust_runner.run_all_tests(gdscript_tests, gdscript_suites.size(), allow_focus)
36+
var success: bool = rust_runner.run_all_tests(
37+
gdscript_tests,
38+
gdscript_suites.size(),
39+
allow_focus,
40+
self,
41+
)
3742

3843
var exit_code: int = 0 if success else 1
3944
get_tree().quit(exit_code)

itest/rust/src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
55
*/
66

7+
use godot::engine::Node;
78
use godot::init::{gdextension, ExtensionLibrary};
9+
use godot::obj::Gd;
810
use godot::sys;
911

1012
mod array_test;
@@ -89,6 +91,10 @@ fn collect_rust_tests() -> (Vec<RustTestCase>, usize, bool) {
8991
(tests, all_files.len(), is_focus_run)
9092
}
9193

94+
pub struct TestContext {
95+
scene_tree: Gd<Node>,
96+
}
97+
9298
#[derive(Copy, Clone)]
9399
struct RustTestCase {
94100
name: &'static str,
@@ -98,5 +104,5 @@ struct RustTestCase {
98104
focused: bool,
99105
#[allow(dead_code)]
100106
line: u32,
101-
function: fn(),
107+
function: fn(&TestContext),
102108
}

0 commit comments

Comments
 (0)