Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
142: Rework integration tests: self-registering `#[itest]`, Rust runner r=Bromeon a=Bromeon

Main changes:
* `#[itest]` is now self-registering, just like Rust's `#[test]`. 
   * No more `ok &= test_xy();` shenanigans. When declared, it runs.
* A large part of the runner logic is moved from GDScript to Rust.
   * Unifies printing, statistics and time measurement.
   * Significantly speeds up test execution, from around 0.28s to 0.13s on my machine. Not that this matters much at the moment, but it may once we have more tests, or tests are generated at larger scale.
* No longer prints the panic and stack trace during `expect_panic`, reducing the stdout spam during integration tests.

And unrelated to testing, but needed for this PR:
* New `Variant::call()` method
* .gitignore fixes

Co-authored-by: Jan Haller <[email protected]>
  • Loading branch information
bors[bot] and Bromeon authored Mar 5, 2023
2 parents 72870d1 + 5d6218f commit 55c4d3a
Show file tree
Hide file tree
Showing 36 changed files with 426 additions and 474 deletions.
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) {
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

0 comments on commit 55c4d3a

Please sign in to comment.