Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework integration tests: self-registering #[itest], Rust runner #142

Merged
merged 8 commits into from
Mar 5, 2023
Merged
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ target
Cargo.lock

# Godot
**/.import/
# .godot needs to be a pattern like this and not a directory, otherwise the negative statements below don't apply
**/.godot/**
*.import

# Needed to run projects without having to open the editor first.
!**/.godot/extension_list.cfg
!**/.godot/global_script_class_cache.cfg
Expand Down
3 changes: 0 additions & 3 deletions examples/dodge-the-creeps/godot/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
list=[]
4 changes: 2 additions & 2 deletions godot-codegen/src/class_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ fn make_return(
let variant = Variant::#from_sys_init_method(|return_ptr| {
let mut __err = sys::default_call_error();
#varcall_invocation
assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK);
sys::panic_on_call_error(&__err);
});
#return_expr
}
Expand All @@ -863,7 +863,7 @@ fn make_return(
let mut __err = sys::default_call_error();
let return_ptr = std::ptr::null_mut();
#varcall_invocation
assert_eq!(__err.error, sys::GDEXTENSION_CALL_OK);
sys::panic_on_call_error(&__err);
}
}
(None, Some(RustTy::EngineClass { tokens, .. })) => {
Expand Down
2 changes: 1 addition & 1 deletion godot-codegen/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn test_pascal_conversion() {
fn test_snake_conversion() {
// More in line with Rust identifiers, and eases recognition of other automation (like enumerator mapping).
#[rustfmt::skip]
let mappings = [
let mappings = [
("AABB", "aabb"),
("AESContext", "aes_context"),
("AStar3D", "a_star_3d"),
Expand Down
40 changes: 37 additions & 3 deletions godot-core/src/builtin/variant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use crate::builtin::GodotString;
use crate::builtin::{GodotString, StringName};
use godot_ffi as sys;
use godot_ffi::GodotFfi;
use std::{fmt, ptr};
Expand Down Expand Up @@ -91,12 +91,46 @@ impl Variant {
}
}

// TODO test
#[allow(unused_mut)]
/// ⚠️ Calls the specified `method` with the given `args`.
///
/// Supports `Object` as well as built-ins with methods (e.g. `Array`, `Vector3`, `GodotString`, etc).
///
/// # Panics
/// * If `self` is not a variant type which supports method calls.
/// * If the method does not exist or the signature is not compatible with the passed arguments.
/// * If the call causes an error.
#[inline]
pub fn call(&self, method: impl Into<StringName>, args: &[Variant]) -> Variant {
self.call_inner(method.into(), args)
}

fn call_inner(&self, method: StringName, args: &[Variant]) -> Variant {
let args_sys: Vec<_> = args.iter().map(|v| v.var_sys_const()).collect();
let mut error = sys::default_call_error();

#[allow(unused_mut)]
let mut result = Variant::nil();

unsafe {
interface_fn!(variant_call)(
self.var_sys(),
method.string_sys(),
args_sys.as_ptr(),
args_sys.len() as i64,
result.var_sys(),
ptr::addr_of_mut!(error),
)
};

sys::panic_on_call_error(&error);
result
}

pub fn evaluate(&self, rhs: &Variant, op: VariantOperator) -> Option<Variant> {
let op_sys = op.sys();
let mut is_valid = false as u8;

#[allow(unused_mut)]
let mut result = Variant::nil();
unsafe {
interface_fn!(variant_evaluate)(
Expand Down
2 changes: 1 addition & 1 deletion godot-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub mod private {

use crate::{log, sys};

sys::plugin_registry!(__GODOT_PLUGIN_REGISTRY: ClassPlugin);
sys::plugin_registry!(pub __GODOT_PLUGIN_REGISTRY: ClassPlugin);

pub(crate) fn iterate_plugins(mut visitor: impl FnMut(&ClassPlugin)) {
sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
Expand Down
27 changes: 27 additions & 0 deletions godot-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ unsafe fn unwrap_ref_unchecked_mut<T>(opt: &mut Option<T>) -> &mut T {
}

#[doc(hidden)]
#[inline]
pub fn default_call_error() -> GDExtensionCallError {
GDExtensionCallError {
error: GDEXTENSION_CALL_OK,
Expand All @@ -148,6 +149,32 @@ pub fn default_call_error() -> GDExtensionCallError {
}
}

#[doc(hidden)]
#[inline]
pub fn panic_on_call_error(err: &GDExtensionCallError) {
Copy link
Contributor

@hydrolarus hydrolarus Mar 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a little late, but this seems like a great candidate for using #[track_caller], doesn't it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great advice, thanks! I was actually considering to make it a macro for this very reason, but totally forgot about this attribute 👍

No worries about "late", 2nd round of test improvements already on the way 😎

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tested it btw, indeed it works and shows the occurrence of the panic (with RUST_BACKTRACE=0) on the call site.

let actual = err.error;

assert_eq!(
actual,
GDEXTENSION_CALL_OK,
"encountered Godot error code {}",
call_error_to_string(actual)
);
}

fn call_error_to_string(err: GDExtensionCallErrorType) -> &'static str {
match err {
GDEXTENSION_CALL_OK => "OK",
GDEXTENSION_CALL_ERROR_INVALID_METHOD => "ERROR_INVALID_METHOD",
GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => "ERROR_INVALID_ARGUMENT",
GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => "ERROR_TOO_MANY_ARGUMENTS",
GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => "ERROR_TOO_FEW_ARGUMENTS",
GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "ERROR_INSTANCE_IS_NULL",
GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "ERROR_METHOD_NOT_CONST",
_ => "(unknown)",
}
}

#[macro_export]
#[doc(hidden)]
macro_rules! builtin_fn {
Expand Down
4 changes: 2 additions & 2 deletions godot-ffi/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
#[doc(hidden)]
#[macro_export]
macro_rules! plugin_registry {
($registry:ident: $Type:ty) => {
($vis:vis $registry:ident: $Type:ty) => {
$crate::paste::paste! {
#[used]
#[allow(non_upper_case_globals)]
#[doc(hidden)]
pub static [< __godot_rust_plugin_ $registry >]:
$vis static [< __godot_rust_plugin_ $registry >]:
std::sync::Mutex<Vec<$Type>> = std::sync::Mutex::new(Vec::new());
}
};
Expand Down
17 changes: 14 additions & 3 deletions godot-macros/src/itest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ pub fn transform(input: TokenStream) -> Result<TokenStream, Error> {
}

let test_name = &func.name;
let init_msg = format!(" -- {test_name}");
let error_msg = format!(" !! Test {test_name} failed");
let test_name_str = func.name.to_string();
let body = &func.body;

Ok(quote! {
#[doc(hidden)]
/*#[doc(hidden)]
#[must_use]
pub fn #test_name() -> bool {
println!(#init_msg);
Expand All @@ -47,6 +46,18 @@ pub fn transform(input: TokenStream) -> Result<TokenStream, Error> {
);

success.is_some()
}*/

pub fn #test_name() {
#body
}

::godot::sys::plugin_add!(__GODOT_ITEST in crate; crate::RustTestCase {
name: #test_name_str,
skipped: false,
file: std::file!(),
line: std::line!(),
function: #test_name,
});
})
}
65 changes: 17 additions & 48 deletions itest/godot/TestRunner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,43 @@
extends Node

func _ready():
var test_suites: Array = [
IntegrationTests.new(),
var rust_runner = IntegrationTests.new()

var gdscript_suites: Array = [
preload("res://ManualFfiTests.gd").new(),
preload("res://gen/GenFfiTests.gd").new(),
]

var tests: Array[_Test] = []
for suite in test_suites:
var gdscript_tests: Array = []
for suite in gdscript_suites:
for method in suite.get_method_list():
var method_name: String = method.name
if method_name.begins_with("test_"):
tests.push_back(_Test.new(suite, method_name))

print()
print_rich(" [b][color=green]Running[/color][/b] test project %s" % [
ProjectSettings.get_setting("application/config/name", ""),
])
print()

var stats: TestStats = TestStats.new()
stats.start_stopwatch()
for test in tests:
printraw(" -- %s ... " % [test.test_name])
var ok: bool = test.run()
print_rich("[color=green]ok[/color]" if ok else "[color=red]FAILED[/color]")
stats.add(ok)
stats.stop_stopwatch()

print()
print_rich("test result: %s. %d passed; %d failed; finished in %.2fs" % [
"[color=green]ok[/color]" if stats.all_passed() else "[color=red]FAILED[/color]",
stats.num_ok,
stats.num_failed,
stats.runtime_seconds(),
])
print()

for suite in test_suites:
suite.free()

var exit_code: int = 0 if stats.all_passed() else 1
gdscript_tests.push_back(GDScriptTestCase.new(suite, method_name))

var success: bool = rust_runner.run_all_tests(gdscript_tests, gdscript_suites.size())

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

class _Test:

class GDScriptTestCase:
var suite: Object
var method_name: String
var test_name: String
var suite_name: String

func _init(suite: Object, method_name: String):
self.suite = suite
self.method_name = method_name
self.test_name = "%s::%s" % [_suite_name(suite), method_name]
self.suite_name = _suite_name(suite)

func run():
# This is a no-op if the suite doesn't have this property.
suite.set("_assertion_failed", false)
var result = suite.call(method_name)
var ok: bool = (
(result == true || result == null)
&& !suite.get("_assertion_failed")
)
var ok: bool = (result == true || result == null) && !suite.get("_assertion_failed")
return ok

static func _suite_name(suite: Object) -> String:
var script: GDScript = suite.get_script()
if script:
# Test suite written in GDScript.
return script.resource_path.get_file().get_basename()
else:
# Test suite written in Rust.
return suite.get_class()
return str(script.resource_path.get_file().get_basename(), ".gd")
33 changes: 0 additions & 33 deletions itest/godot/TestStats.gd

This file was deleted.

35 changes: 19 additions & 16 deletions itest/godot/TestSuite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

class_name TestSuite
extends Node
extends RefCounted

var _assertion_failed: bool = false

## Asserts that `what` is `true`, but does not abort the test. Returns `what` so you can return
## early from the test function if the assertion failed.
func assert_that(what: bool, message: String = "") -> bool:
if !what:
_assertion_failed = true
if message:
print("assertion failed: %s" % message)
else:
print("assertion failed")
return what
if what:
return true

_assertion_failed = true
if message:
print("assertion failed: %s" % message)
else:
print("assertion failed")
return false

func assert_eq(left, right, message: String = "") -> bool:
if left != right:
_assertion_failed = true
if message:
print("assertion failed: %s\n left: %s\n right: %s" % [message, left, right])
else:
print("assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right])
return false
return true
if left == right:
return true

_assertion_failed = true
if message:
print("assertion failed: %s\n left: %s\n right: %s" % [message, left, right])
else:
print("assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right])
return false
Loading