Skip to content

Commit 6b44f16

Browse files
bors[bot]ttencate
andauthored
Merge #103
103: Add a simple test runner in GDScript r=Bromeon a=ttencate I made the output look like `cargo test` to reduce the cognitive burden while switching between the two languages: ![2023-01-31T20:52:24_1057x1384](https://user-images.githubusercontent.com/90930/215867771-ace18fd1-d1f6-4af2-8f59-b3d98b012328.png) Nothing has been done on the Rust side yet, so there is one big test `IntegrationTests::test_all()` (renamed from `run()`). Co-authored-by: Thomas ten Cate <[email protected]>
2 parents 3182bd4 + dd7ee64 commit 6b44f16

File tree

11 files changed

+179
-87
lines changed

11 files changed

+179
-87
lines changed

.gitignore

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
target
88
Cargo.lock
99

10-
# Godot (extension list needed to run tests without prior editor opening)
11-
.godot
12-
godot.log
13-
!/itest/.godot/extension_list.cfg
14-
*.import
10+
# Godot
11+
**/.import/
12+
**/.godot/**
13+
# Needed to run projects without having to open the editor first.
14+
!**/.godot/extension_list.cfg
15+
!**/.godot/global_script_class_cache.cfg
1516

1617
# This project: input JSONs and code generation
1718
gen
@@ -20,4 +21,4 @@ gen
2021
/script
2122
*.bat
2223
config.toml
23-
exp.rs
24+
exp.rs
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
.import
1+
.godot/
2+
.import/
23
logs/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
list=[{
2+
"base": &"RefCounted",
3+
"class": &"TestStats",
4+
"icon": "",
5+
"language": &"GDScript",
6+
"path": "res://TestStats.gd"
7+
}, {
8+
"base": &"Node",
9+
"class": &"TestSuite",
10+
"icon": "",
11+
"language": &"GDScript",
12+
"path": "res://TestSuite.gd"
13+
}]

itest/godot/ManualFfiTests.gd

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,39 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
extends Node
5+
extends TestSuite
66

7-
func run() -> bool:
8-
print("[GD] Test ManualFfi...")
9-
var ok = true
10-
ok = ok && test_missing_init()
11-
ok = ok && test_to_string()
12-
ok = ok && test_export()
13-
14-
print("[GD] ManualFfi tested (passed=", ok, ")")
15-
return ok
16-
17-
func test_missing_init() -> bool:
18-
return true # TODO: fix dynamic eval
7+
func test_missing_init():
8+
return # TODO: fix dynamic eval
199

2010
var expr = Expression.new()
2111
var error = expr.parse("WithoutInit.new()")
22-
if error != OK:
23-
print("Failed to parse dynamic expression")
24-
return false
12+
if !assert_eq(error, OK, "Failed to parse dynamic expression"):
13+
return
2514

2615
var instance = expr.execute()
27-
if expr.has_execute_failed():
28-
print("Failed to evaluate dynamic expression")
29-
return false
16+
if !assert_that(!expr.has_execute_failed(), "Failed to evaluate dynamic expression"):
17+
return
3018

3119
print("[GD] WithoutInit is: ", instance)
32-
return true
3320

34-
func test_to_string() -> bool:
21+
func test_to_string():
3522
var ffi = VirtualMethodTest.new()
36-
var s = str(ffi)
37-
38-
print("to_string: ", s)
39-
print("to_string: ", ffi)
40-
return true
4123

42-
func test_export() -> bool:
24+
assert_eq(str(ffi), "VirtualMethodTest[integer=0]")
25+
26+
func test_export():
4327
var obj = HasProperty.new()
4428

4529
obj.int_val = 5
46-
print("[GD] HasProperty's int_val property is: ", obj.int_val, " and should be 5")
47-
var int_val_correct = obj.int_val == 5
30+
assert_eq(obj.int_val, 5)
4831

4932
obj.string_val = "test val"
50-
print("[GD] HasProperty's string_val property is: ", obj.string_val, " and should be \"test val\"")
51-
var string_val_correct = obj.string_val == "test val"
33+
assert_eq(obj.string_val, "test val")
5234

5335
var node = Node.new()
5436
obj.object_val = node
55-
print("[GD] HasProperty's object_val property is: ", obj.object_val, " and should be ", node)
56-
var object_val_correct = obj.object_val == node
37+
assert_eq(obj.object_val, node)
5738

5839
obj.free()
5940
node.free()
60-
61-
return int_val_correct && string_val_correct && object_val_correct

itest/godot/TestRunner.gd

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,74 @@
55
extends Node
66

77
func _ready():
8-
var rust_tests := IntegrationTests.new()
9-
10-
var rust = rust_tests.run()
11-
var manual = $ManualFfiTests.run()
12-
var generated = $GenFfiTests.run()
13-
14-
var status: bool = rust && manual && generated
15-
8+
var test_suites: Array = [
9+
IntegrationTests.new(),
10+
preload("res://ManualFfiTests.gd").new(),
11+
preload("res://gen/GenFfiTests.gd").new(),
12+
]
13+
14+
var tests: Array[_Test] = []
15+
for suite in test_suites:
16+
for method in suite.get_method_list():
17+
var method_name: String = method.name
18+
if method_name.begins_with("test_"):
19+
tests.push_back(_Test.new(suite, method_name))
20+
1621
print()
17-
var exit_code: int
18-
if status:
19-
print(" All tests PASSED.")
20-
exit_code = 0
21-
else:
22-
print(" Tests FAILED.")
23-
exit_code = 1
24-
25-
print(" -- exiting.")
26-
rust_tests.free()
22+
print_rich(" [b][color=green]Running[/color][/b] test project %s" % [
23+
ProjectSettings.get_setting("application/config/name", ""),
24+
])
25+
print()
26+
27+
var stats: TestStats = TestStats.new()
28+
stats.start_stopwatch()
29+
for test in tests:
30+
printraw(" -- %s ... " % [test.test_name])
31+
var ok: bool = test.run()
32+
print_rich("[color=green]ok[/color]" if ok else "[color=red]FAILED[/color]")
33+
stats.add(ok)
34+
stats.stop_stopwatch()
35+
36+
print()
37+
print_rich("test result: %s. %d passed; %d failed; finished in %.2fs" % [
38+
"[color=green]ok[/color]" if stats.all_passed() else "[color=red]FAILED[/color]",
39+
stats.num_ok,
40+
stats.num_failed,
41+
stats.runtime_seconds(),
42+
])
43+
print()
44+
45+
for suite in test_suites:
46+
suite.free()
47+
48+
var exit_code: int = 0 if stats.all_passed() else 1
2749
get_tree().quit(exit_code)
50+
51+
class _Test:
52+
var suite: Object
53+
var method_name: String
54+
var test_name: String
55+
56+
func _init(suite: Object, method_name: String):
57+
self.suite = suite
58+
self.method_name = method_name
59+
self.test_name = "%s::%s" % [_suite_name(suite), method_name]
60+
61+
func run():
62+
# This is a no-op if the suite doesn't have this property.
63+
suite.set("_assertion_failed", false)
64+
var result = suite.call(method_name)
65+
var ok: bool = (
66+
(result == true or result == null)
67+
&& !suite.get("_assertion_failed")
68+
)
69+
return ok
70+
71+
static func _suite_name(suite: Object) -> String:
72+
var script := suite.get_script()
73+
if script:
74+
# Test suite written in GDScript.
75+
return script.resource_path.get_file().get_basename()
76+
else:
77+
# Test suite written in Rust.
78+
return suite.get_class()

itest/godot/TestRunner.tscn

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
[gd_scene load_steps=4 format=3 uid="uid://dgcj68l8n6wpb"]
1+
[gd_scene load_steps=2 format=3 uid="uid://dgcj68l8n6wpb"]
22

33
[ext_resource type="Script" path="res://TestRunner.gd" id="1_wdbrf"]
4-
[ext_resource type="Script" path="res://ManualFfiTests.gd" id="2_gmtlc"]
5-
[ext_resource type="Script" path="res://gen/GenFfiTests.gd" id="3_vivae"]
64

75
[node name="TestRunner" type="Node"]
86
script = ExtResource("1_wdbrf")
9-
10-
[node name="ManualFfiTests" type="Node" parent="."]
11-
script = ExtResource("2_gmtlc")
12-
13-
[node name="GenFfiTests" type="Node" parent="."]
14-
script = ExtResource("3_vivae")

itest/godot/TestStats.gd

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
class_name TestStats
6+
extends RefCounted
7+
8+
var num_run := 0
9+
var num_ok := 0
10+
var num_failed := 0
11+
12+
var _start_time_usec := 0
13+
var _runtime_usec := 0
14+
15+
func add(ok: bool):
16+
num_run += 1
17+
if ok:
18+
num_ok += 1
19+
else:
20+
num_failed += 1
21+
22+
func all_passed() -> bool:
23+
# Consider 0 tests run as a failure too, because it's probably a problem with the run itself.
24+
return num_failed == 0 && num_run > 0
25+
26+
func start_stopwatch():
27+
_start_time_usec = Time.get_ticks_usec()
28+
29+
func stop_stopwatch():
30+
_runtime_usec += Time.get_ticks_usec() - _start_time_usec
31+
32+
func runtime_seconds() -> float:
33+
return _runtime_usec * 1.0e-6

itest/godot/TestSuite.gd

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
class_name TestSuite
6+
extends Node
7+
8+
var _assertion_failed: bool = false
9+
10+
## Asserts that `what` is `true`, but does not abort the test. Returns `what` so you can return
11+
## early from the test function if the assertion failed.
12+
func assert_that(what: bool, message: String = "") -> bool:
13+
if !what:
14+
_assertion_failed = true
15+
if message:
16+
print("assertion failed: %s" % message)
17+
else:
18+
print("assertion failed")
19+
return what
20+
21+
func assert_eq(left, right, message: String = "") -> bool:
22+
if left != right:
23+
_assertion_failed = true
24+
if message:
25+
print("assertion failed: %s\n left: %s\n right: %s" % [message, left, right])
26+
else:
27+
print("assertion failed: `(left == right)`\n left: %s\n right: %s" % [left, right])
28+
return false
29+
return true

itest/godot/input/GenFfiTests.template.gd

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,16 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
extends Node
5+
extends TestSuite
66

7-
func run() -> bool:
7+
#(
8+
func test_varcall_IDENT():
89
var ffi = GenFfi.new()
9-
print("[GD] GenFfi constructed: ", ffi.get_instance_id())
10-
var ok := true#(
11-
if !test_varcall_IDENT(ffi):
12-
ok = false
13-
push_error(" -- FFI test failed: test_varcall_IDENT")
14-
#)
15-
16-
print("[GD] GenFfi destructing...")
17-
return ok
1810

19-
#(
20-
func test_varcall_IDENT(ffi: GenFfi) -> bool:
2111
var from_rust = ffi.return_IDENT()
22-
var ok1: bool = ffi.accept_IDENT(from_rust)
12+
assert_that(ffi.accept_IDENT(from_rust))
2313

2414
var from_gdscript = VAL
2515
var mirrored = ffi.mirror_IDENT(from_gdscript)
26-
var ok2: bool = (mirrored == from_gdscript)
27-
28-
return ok1 && ok2
16+
assert_eq(mirrored, from_gdscript)
2917
#)

itest/godot/project.godot

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ config_version=5
1313
config/name="IntegrationTests"
1414
run/main_scene="res://TestRunner.tscn"
1515
config/features=PackedStringArray("4.0")
16+
run/flush_stdout_on_print=true
17+
18+
[debug]
19+
20+
gdscript/warnings/shadowed_variable=0

itest/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ struct IntegrationTests {}
6161
#[godot_api]
6262
impl IntegrationTests {
6363
#[func]
64-
fn run(&mut self) -> bool {
64+
fn test_all(&mut self) -> bool {
6565
println!("Run Godot integration tests...");
6666
run_tests()
6767
}

0 commit comments

Comments
 (0)