Skip to content

Commit e5b215b

Browse files
committed
Async Tests
1 parent 1edd5e8 commit e5b215b

File tree

6 files changed

+504
-64
lines changed

6 files changed

+504
-64
lines changed

Diff for: godot-macros/src/itest.rs

+45-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use proc_macro2::TokenStream;
99
use quote::{quote, ToTokens};
1010

11-
use crate::util::{bail, path_ends_with, KvParser};
11+
use crate::util::{bail, extract_typename, ident, path_ends_with, KvParser};
1212
use crate::ParseResult;
1313

1414
pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
@@ -17,20 +17,21 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
1717
_ => return bail!(&input_item, "#[itest] can only be applied to functions"),
1818
};
1919

20+
let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
21+
let skipped = attr.handle_alone("skip")?;
22+
let focused = attr.handle_alone("focus")?;
23+
let is_async = attr.handle_alone("async")?;
24+
attr.finish()?;
25+
2026
// Note: allow attributes for things like #[rustfmt] or #[clippy]
2127
if func.generic_params.is_some()
2228
|| func.params.len() > 1
23-
|| func.return_ty.is_some()
29+
|| (func.return_ty.is_some() && !is_async)
2430
|| func.where_clause.is_some()
2531
{
2632
return bad_signature(&func);
2733
}
2834

29-
let mut attr = KvParser::parse_required(&func.attributes, "itest", &func.name)?;
30-
let skipped = attr.handle_alone("skip")?;
31-
let focused = attr.handle_alone("focus")?;
32-
attr.finish()?;
33-
3435
if skipped && focused {
3536
return bail!(
3637
func.name,
@@ -47,24 +48,49 @@ pub fn attribute_itest(input_item: venial::Item) -> ParseResult<TokenStream> {
4748
// Correct parameter type (crude macro check) -> reuse parameter name
4849
if path_ends_with(&param.ty.tokens, "TestContext") {
4950
param.to_token_stream()
51+
} else if is_async {
52+
return bad_async_signature(&func);
5053
} else {
5154
return bad_signature(&func);
5255
}
56+
} else if is_async {
57+
return bad_async_signature(&func);
5358
} else {
5459
return bad_signature(&func);
5560
}
5661
} else {
5762
quote! { __unused_context: &crate::framework::TestContext }
5863
};
5964

65+
if is_async
66+
&& func
67+
.return_ty
68+
.as_ref()
69+
.and_then(extract_typename)
70+
.map_or(true, |segment| segment.ident != "TaskHandle")
71+
{
72+
return bad_async_signature(&func);
73+
}
74+
6075
let body = &func.body;
6176

77+
let (return_tokens, test_case_ty, plugin_name);
78+
if is_async {
79+
return_tokens = quote! { -> TaskHandle };
80+
test_case_ty = quote! { crate::framework::AsyncRustTestCase };
81+
plugin_name = ident("__GODOT_ASYNC_ITEST");
82+
} else {
83+
return_tokens = TokenStream::new();
84+
test_case_ty = quote! { crate::framework::RustTestCase };
85+
plugin_name = ident("__GODOT_ITEST");
86+
};
87+
6288
Ok(quote! {
63-
pub fn #test_name(#param) {
89+
pub fn #test_name(#param) #return_tokens {
6490
#body
6591
}
6692

67-
::godot::sys::plugin_add!(__GODOT_ITEST in crate::framework; crate::framework::RustTestCase {
93+
::godot::sys::plugin_add!(#plugin_name in crate::framework; #test_case_ty {
6894
name: #test_name_str,
6995
skipped: #skipped,
7096
focused: #focused,
@@ -84,3 +110,13 @@ fn bad_signature(func: &venial::Function) -> Result<TokenStream, venial::Error>
84110
f = func.name,
85111
)
86112
}
113+
114+
fn bad_async_signature(func: &venial::Function) -> Result<TokenStream, venial::Error> {
115+
bail!(
116+
func,
117+
"#[itest(async)] function must have one of these signatures:\
118+
\n fn {f}() -> TaskHandle {{ ... }}\
119+
\n fn {f}(ctx: &TestContext) -> TaskHandle {{ ... }}",
120+
f = func.name,
121+
)
122+
}

Diff for: itest/godot/TestRunner.gd

+11-7
Original file line numberDiff line numberDiff line change
@@ -66,20 +66,24 @@ func _ready():
6666

6767
var property_tests = load("res://gen/GenPropertyTests.gd").new()
6868

69-
var success: bool = rust_runner.run_all_tests(
69+
# Run benchmarks after all synchronous and asynchronous tests have completed.
70+
var run_benchmarks = func (success: bool):
71+
if success:
72+
rust_runner.run_all_benchmarks(self)
73+
74+
var exit_code: int = 0 if success else 1
75+
get_tree().quit(exit_code)
76+
77+
rust_runner.run_all_tests(
7078
gdscript_tests,
7179
gdscript_suites.size(),
7280
allow_focus,
7381
self,
7482
filters,
75-
property_tests
83+
property_tests,
84+
run_benchmarks
7685
)
7786

78-
if success:
79-
rust_runner.run_all_benchmarks(self)
80-
81-
var exit_code: int = 0 if success else 1
82-
get_tree().quit(exit_code)
8387

8488

8589
class GDScriptTestCase:

Diff for: itest/rust/src/engine_tests/async_test.rs

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use std::ops::Deref;
9+
10+
use godot::builtin::{Callable, Signal, Variant};
11+
use godot::classes::{Object, RefCounted};
12+
use godot::meta::ToGodot;
13+
use godot::obj::{Base, Gd, NewAlloc, NewGd};
14+
use godot::prelude::{godot_api, GodotClass};
15+
use godot::task::{self, SignalFuture, SignalFutureResolver, TaskHandle};
16+
17+
use crate::framework::{itest, TestContext};
18+
19+
#[derive(GodotClass)]
20+
#[class(init)]
21+
struct AsyncRefCounted {
22+
base: Base<RefCounted>,
23+
}
24+
25+
#[godot_api]
26+
impl AsyncRefCounted {
27+
#[signal]
28+
fn custom_signal(value: u32);
29+
}
30+
31+
#[itest(async)]
32+
fn start_async_task() -> TaskHandle {
33+
let mut object = RefCounted::new_gd();
34+
let object_ref = object.clone();
35+
let signal = Signal::from_object_signal(&object, "custom_signal");
36+
37+
object.add_user_signal("custom_signal");
38+
39+
let task_handle = task::spawn(async move {
40+
let signal_future: SignalFuture<(u8,)> = signal.to_future();
41+
let (result,) = signal_future.await;
42+
43+
assert_eq!(result, 10);
44+
drop(object_ref);
45+
});
46+
47+
object.emit_signal("custom_signal", &[10.to_variant()]);
48+
49+
task_handle
50+
}
51+
52+
#[itest]
53+
fn cancel_async_task(ctx: &TestContext) {
54+
let tree = ctx.scene_tree.get_tree().unwrap();
55+
let signal = Signal::from_object_signal(&tree, "process_frame");
56+
57+
let handle = task::spawn(async move {
58+
let _: () = signal.to_future().await;
59+
60+
unreachable!();
61+
});
62+
63+
handle.cancel();
64+
}
65+
66+
#[itest(async)]
67+
fn async_task_fallible_signal_future() -> TaskHandle {
68+
let mut obj = Object::new_alloc();
69+
70+
let signal = Signal::from_object_signal(&obj, "script_changed");
71+
72+
let handle = task::spawn(async move {
73+
let result = signal.to_fallible_future::<()>().await;
74+
75+
assert!(result.is_err());
76+
});
77+
78+
obj.call_deferred("free", &[]);
79+
80+
handle
81+
}
82+
83+
// Test that two callables created from the same future resolver (but cloned) are equal, while they are not equal to an unrelated
84+
// callable.
85+
#[itest]
86+
fn resolver_callabable_equality() {
87+
let resolver = SignalFutureResolver::<(u8,)>::default();
88+
89+
let callable = Callable::from_custom(resolver.clone());
90+
let cloned_callable = Callable::from_custom(resolver.clone());
91+
let unrelated_callable = Callable::from_local_fn("unrelated", |_| Ok(Variant::nil()));
92+
93+
assert_eq!(callable, cloned_callable);
94+
assert_ne!(callable, unrelated_callable);
95+
assert_ne!(cloned_callable, unrelated_callable);
96+
}
97+
98+
#[itest(async)]
99+
fn async_typed_signal() -> TaskHandle {
100+
let object = AsyncRefCounted::new_gd();
101+
let object_ref = object.clone();
102+
103+
let task_handle = task::spawn(async move {
104+
let (result,) = object.signals().custom_signal().deref().await;
105+
106+
assert_eq!(result, 66);
107+
});
108+
109+
object_ref.signals().custom_signal().emit(66);
110+
111+
task_handle
112+
}

Diff for: itest/rust/src/engine_tests/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
#[cfg(since_api = "4.2")]
9+
mod async_test;
810
mod codegen_enums_test;
911
mod codegen_test;
1012
mod engine_enum_test;

Diff for: itest/rust/src/framework/mod.rs

+49-3
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ pub use godot::test::{bench, itest};
2424

2525
// Registers all the `#[itest]` tests and `#[bench]` benchmarks.
2626
sys::plugin_registry!(pub(crate) __GODOT_ITEST: RustTestCase);
27+
#[cfg(since_api = "4.2")]
28+
sys::plugin_registry!(pub(crate) __GODOT_ASYNC_ITEST: AsyncRustTestCase);
2729
sys::plugin_registry!(pub(crate) __GODOT_BENCH: RustBenchmark);
2830

2931
/// Finds all `#[itest]` tests.
30-
fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, usize, bool) {
32+
fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, HashSet<&str>, bool) {
3133
let mut all_files = HashSet::new();
3234
let mut tests: Vec<RustTestCase> = vec![];
3335
let mut is_focus_run = false;
@@ -50,7 +52,38 @@ fn collect_rust_tests(filters: &[String]) -> (Vec<RustTestCase>, usize, bool) {
5052
// Sort alphabetically for deterministic run order
5153
tests.sort_by_key(|test| test.file);
5254

53-
(tests, all_files.len(), is_focus_run)
55+
(tests, all_files, is_focus_run)
56+
}
57+
58+
/// Finds all `#[itest(async)]` tests.
59+
#[cfg(since_api = "4.2")]
60+
fn collect_async_rust_tests(
61+
filters: &[String],
62+
sync_focus_run: bool,
63+
) -> (Vec<AsyncRustTestCase>, HashSet<&str>, bool) {
64+
let mut all_files = HashSet::new();
65+
let mut tests = vec![];
66+
let mut is_focus_run = sync_focus_run;
67+
68+
sys::plugin_foreach!(__GODOT_ASYNC_ITEST; |test: &AsyncRustTestCase| {
69+
// First time a focused test is encountered, switch to "focused" mode and throw everything away.
70+
if !is_focus_run && test.focused {
71+
tests.clear();
72+
all_files.clear();
73+
is_focus_run = true;
74+
}
75+
76+
// Only collect tests if normal mode, or focus mode and test is focused.
77+
if (!is_focus_run || test.focused) && passes_filter(filters, test.name) {
78+
all_files.insert(test.file);
79+
tests.push(*test);
80+
}
81+
});
82+
83+
// Sort alphabetically for deterministic run order
84+
tests.sort_by_key(|test| test.file);
85+
86+
(tests, all_files, is_focus_run)
5487
}
5588

5689
/// Finds all `#[bench]` benchmarks.
@@ -71,7 +104,7 @@ fn collect_rust_benchmarks() -> (Vec<RustBenchmark>, usize) {
71104

72105
// ----------------------------------------------------------------------------------------------------------------------------------------------
73106
// Shared types
74-
107+
#[derive(Clone)]
75108
pub struct TestContext {
76109
pub scene_tree: Gd<Node>,
77110
pub property_tests: Gd<Node>,
@@ -108,6 +141,19 @@ pub struct RustTestCase {
108141
pub function: fn(&TestContext),
109142
}
110143

144+
#[cfg(since_api = "4.2")]
145+
#[derive(Copy, Clone)]
146+
pub struct AsyncRustTestCase {
147+
pub name: &'static str,
148+
pub file: &'static str,
149+
pub skipped: bool,
150+
/// If one or more tests are focused, only they will be executed. Helpful for debugging and working on specific features.
151+
pub focused: bool,
152+
#[allow(dead_code)]
153+
pub line: u32,
154+
pub function: fn(&TestContext) -> godot::task::TaskHandle,
155+
}
156+
111157
#[derive(Copy, Clone)]
112158
pub struct RustBenchmark {
113159
pub name: &'static str,

0 commit comments

Comments
 (0)