Skip to content

Commit b150928

Browse files
committed
Inject Godot scene-tree into #[itest] cases
Also a few proc-macro refactorings
1 parent 81a33a3 commit b150928

File tree

9 files changed

+130
-54
lines changed

9 files changed

+130
-54
lines changed

godot-core/src/obj/gd.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

itest/rust/src/object_test.rs

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

7-
use crate::{expect_panic, itest};
7+
use std::cell::RefCell;
8+
use std::mem;
9+
use std::rc::Rc;
10+
811
use godot::bind::{godot_api, GodotClass, GodotExt};
912
use godot::builtin::{
1013
FromVariant, GodotString, StringName, ToVariant, Variant, VariantConversionError, Vector3,
1114
};
15+
use godot::engine::node::InternalMode;
1216
use godot::engine::{file_access, Area2D, Camera3D, FileAccess, Node, Node3D, Object, RefCounted};
1317
use godot::obj::{Base, Gd, InstanceId};
1418
use godot::obj::{Inherits, Share};
1519
use godot::sys::GodotFfi;
1620

17-
use std::cell::RefCell;
18-
use std::mem;
19-
use std::rc::Rc;
21+
use crate::{expect_panic, itest, TestContext};
2022

2123
// TODO:
2224
// * make sure that ptrcalls are used when possible (ie. when type info available; maybe GDScript integration test)
@@ -596,6 +598,17 @@ fn object_call_with_args() {
596598
node.free();
597599
}
598600

601+
#[itest]
602+
fn object_get_scene_tree(ctx: &TestContext) {
603+
let node = Node3D::new_alloc();
604+
605+
let mut tree = ctx.scene_tree.share();
606+
tree.add_child(node.upcast(), false, InternalMode::INTERNAL_MODE_DISABLED);
607+
608+
let count = tree.get_child_count(false);
609+
assert_eq!(count, 1);
610+
} // implicitly tested: node does not leak
611+
599612
// ----------------------------------------------------------------------------------------------------------------------------------------------
600613

601614
#[inline(never)] // force to move "out of scope", can trigger potential dangling pointer errors

itest/rust/src/runner.rs

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

7+
use std::time::{Duration, Instant};
8+
79
use godot::bind::{godot_api, GodotClass};
810
use godot::builtin::{ToVariant, Variant, VariantArray};
11+
use godot::engine::Node;
12+
use godot::obj::Gd;
913

10-
use crate::RustTestCase;
11-
use std::time::{Duration, Instant};
14+
use crate::{RustTestCase, TestContext};
1215

1316
#[derive(GodotClass, Debug)]
1417
#[class(init)]
@@ -28,6 +31,7 @@ impl IntegrationTests {
2831
gdscript_tests: VariantArray,
2932
gdscript_file_count: i64,
3033
allow_focus: bool,
34+
scene_tree: Gd<Node>,
3135
) -> bool {
3236
println!("{}Run{} Godot integration tests...", FMT_CYAN_BOLD, FMT_END);
3337

@@ -50,7 +54,7 @@ impl IntegrationTests {
5054
}
5155

5256
let clock = Instant::now();
53-
self.run_rust_tests(rust_tests);
57+
self.run_rust_tests(rust_tests, scene_tree);
5458
let rust_time = clock.elapsed();
5559
let gdscript_time = if !focus_run {
5660
self.run_gdscript_tests(gdscript_tests);
@@ -62,10 +66,12 @@ impl IntegrationTests {
6266
self.conclude(rust_time, gdscript_time, allow_focus)
6367
}
6468

65-
fn run_rust_tests(&mut self, tests: Vec<RustTestCase>) {
69+
fn run_rust_tests(&mut self, tests: Vec<RustTestCase>, scene_tree: Gd<Node>) {
70+
let ctx = TestContext { scene_tree };
71+
6672
let mut last_file = None;
6773
for test in tests {
68-
let outcome = run_rust_test(&test);
74+
let outcome = run_rust_test(&test, &ctx);
6975

7076
self.update_stats(&outcome);
7177
print_test(test.file.to_string(), test.name, outcome, &mut last_file);
@@ -160,14 +166,14 @@ const FMT_YELLOW: &str = "\x1b[33m";
160166
const FMT_RED: &str = "\x1b[31m";
161167
const FMT_END: &str = "\x1b[0m";
162168

163-
fn run_rust_test(test: &RustTestCase) -> TestOutcome {
169+
fn run_rust_test(test: &RustTestCase, ctx: &TestContext) -> TestOutcome {
164170
if test.skipped {
165171
return TestOutcome::Skipped;
166172
}
167173

168174
// Explicit type to prevent tests from returning a value
169-
let success: Option<()> =
170-
godot::private::handle_panic(|| format!(" !! Test {} failed", test.name), test.function);
175+
let err_context = || format!(" !! Test {} failed", test.name);
176+
let success: Option<()> = godot::private::handle_panic(err_context, || (test.function)(ctx));
171177

172178
TestOutcome::from_bool(success.is_some())
173179
}

0 commit comments

Comments
 (0)