Skip to content

Commit 9299f56

Browse files
committed
Custom test runner, env vars, tests in demo
1 parent 38967ec commit 9299f56

File tree

10 files changed

+371
-81
lines changed

10 files changed

+371
-81
lines changed

project/mobile.gd

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,8 @@
11
extends CanvasLayer
22

3-
const MOBILE_TESTS_FILE := "res://test/mobile_tests.gd"
4-
53

64
func _ready() -> void:
75
get_viewport().size = Vector2(810, 1530) # simulate mobile screen on desktop
86
get_viewport().get_window().content_scale_factor = 3.0
97
get_viewport().get_window().content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
108
get_viewport().get_window().content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
11-
12-
%RunTestsButton.visible = FileAccess.file_exists(MOBILE_TESTS_FILE)
13-
14-
15-
func _on_run_tests_button_pressed() -> void:
16-
var tests = load(MOBILE_TESTS_FILE).new()
17-
tests.run_tests()
18-
19-
20-
func _on_test_diverse_context_button_pressed() -> void:
21-
var context := {
22-
"null": null,
23-
"bool": true,
24-
"int": 42,
25-
"float": 42.42,
26-
"string": "hello, world!",
27-
"Vector2": Vector2(123.45, 67.89),
28-
"Vector2i": Vector2i(123, 45),
29-
"Rect2": Rect2(123.45, 67.89, 98.76, 54.32),
30-
"Rect2i": Rect2i(12, 34, 56, 78),
31-
"Vector3": Vector3(12.34, 56.78, 90.12),
32-
"Vector3i": Vector3i(12, 34, 56),
33-
"Transform2D": Transform2D().translated(Vector2(12.34, 56.78)),
34-
"Vector4": Vector4(12.34, 56.78, 90.12, 34.56),
35-
"Vector4i": Vector4i(12, 34, 56, 78),
36-
"Plane": Plane(Vector3(1, 2, 3), 4),
37-
"Quaternion": Quaternion(Vector3(0, 1, 0), 4),
38-
"AABB": AABB(Vector3(1, 2, 3), Vector3(4, 5, 6)),
39-
"Basis": Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)),
40-
"Transform3D": Transform3D().translated(Vector3(12.34, 56.78, 90.12)),
41-
"Projection": Projection(Vector4(73.21, 19.47, 85.63, 73.02), Vector4(41.92, 67.38, 22.14, 59.81), Vector4(93.76, 15.49, 38.72, 84.25), Vector4(26.58, 71.93, 47.16, 62.84)),
42-
"Color": Color(12.34, 56.78, 90.12, 34.56),
43-
"StringName": StringName("hello, world!"),
44-
"NodePath": NodePath("/root"),
45-
"RID": RID(),
46-
"Object": self,
47-
"Callable": _on_test_diverse_context_button_pressed,
48-
"Signal": get_tree().process_frame,
49-
"Dictionary": {"key1": "value1", "key2": 42, "key3": self},
50-
"Array": [1, self, {"hello": "world"}],
51-
"PackedByteArray": PackedByteArray([1, 2, 3, 4, 5]),
52-
"PackedInt32Array": PackedInt32Array([1, 2, 3, 4, 5]),
53-
"PackedInt64Array": PackedInt64Array([1, 2, 3, 4, 5]),
54-
"PackedFloat32Array": PackedFloat32Array([1.23, 4.56, 7.89]),
55-
"PackedFloat64Array": PackedFloat64Array([1.23, 4.56, 7.89]),
56-
"PackedStringArray": PackedStringArray(["hello", "world"]),
57-
"PackedVector2Array": PackedVector2Array([Vector2(1, 2), Vector2(3, 4)]),
58-
"PackedVector3Array": PackedVector3Array([Vector3(1, 2, 3), Vector3(4, 5, 6)]),
59-
"PackedColorArray": PackedColorArray([Color(1, 2, 3, 4), Color(5, 6, 7, 8)]),
60-
"PackedVector4Array": PackedVector4Array([Vector4(1, 2, 3, 4), Vector4(5, 6, 7, 8)]),
61-
}
62-
SentrySDK.set_context("diverse_context", context)
63-
DemoOutput.print_info("Added context with diverse values.")
64-
SentrySDK.capture_message("Test diverse context")

project/mobile.tscn

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
[gd_scene load_steps=6 format=3 uid="uid://dpppyaeqgrcn1"]
1+
[gd_scene load_steps=7 format=3 uid="uid://dpppyaeqgrcn1"]
22

33
[ext_resource type="Script" uid="uid://cw2874mkwddfr" path="res://mobile.gd" id="1_bj8h8"]
44
[ext_resource type="PackedScene" uid="uid://bxi26vu5tlqas" path="res://views/capture_events.tscn" id="2_xux57"]
55
[ext_resource type="PackedScene" uid="uid://dyoaec2d7uung" path="res://views/enrich_events.tscn" id="3_p64qd"]
6+
[ext_resource type="PackedScene" uid="uid://cywnvytpa2bec" path="res://views/tools.tscn" id="4_p64qd"]
67
[ext_resource type="PackedScene" uid="uid://dllqhtd731wtc" path="res://views/output_pane.tscn" id="4_xux57"]
78

89
[sub_resource type="Theme" id="Theme_bj8h8"]
@@ -49,35 +50,14 @@ metadata/_tab_index = 0
4950
layout_mode = 2
5051
metadata/_tab_index = 1
5152

52-
[node name="Tools" type="VBoxContainer" parent="VBoxContainer/VBoxContainer/TabContainer"]
53+
[node name="Tools" parent="VBoxContainer/VBoxContainer/TabContainer" instance=ExtResource("4_p64qd")]
5354
unique_name_in_owner = true
5455
visible = false
5556
layout_mode = 2
56-
metadata/_tab_index = 2
57-
58-
[node name="Header - Actions" type="Label" parent="VBoxContainer/VBoxContainer/TabContainer/Tools"]
59-
custom_minimum_size = Vector2(0, 40.505)
60-
layout_mode = 2
61-
text = "ACTIONS"
62-
horizontal_alignment = 1
63-
vertical_alignment = 2
64-
65-
[node name="RunTestsButton" type="Button" parent="VBoxContainer/VBoxContainer/TabContainer/Tools"]
66-
unique_name_in_owner = true
67-
layout_mode = 2
68-
text = "Run mobile tests"
69-
70-
[node name="TestDiverseContextButton" type="Button" parent="VBoxContainer/VBoxContainer/TabContainer/Tools"]
71-
unique_name_in_owner = true
72-
layout_mode = 2
73-
text = "Test diverse context"
7457

7558
[node name="OutputPane" parent="VBoxContainer/VBoxContainer" instance=ExtResource("4_xux57")]
7659
layout_mode = 2
7760

7861
[node name="Spacer2" type="Control" parent="VBoxContainer"]
7962
custom_minimum_size = Vector2(0, 20)
8063
layout_mode = 2
81-
82-
[connection signal="pressed" from="VBoxContainer/VBoxContainer/TabContainer/Tools/RunTestsButton" to="." method="_on_run_tests_button_pressed"]
83-
[connection signal="pressed" from="VBoxContainer/VBoxContainer/TabContainer/Tools/TestDiverseContextButton" to="." method="_on_test_diverse_context_button_pressed"]

project/project_main_loop.gd

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ signal before_send_log(log_entry)
1111

1212

1313
func _initialize() -> void:
14-
if _is_running_tests():
14+
if _is_running_tests_from_editor():
15+
return
16+
if _should_run_tests():
17+
_run_tests()
1518
return
1619

1720
SentrySDK.init(func(options: SentryOptions) -> void:
@@ -51,5 +54,15 @@ func _on_before_send_log_to_sentry(entry: SentryLog) -> SentryLog:
5154
return entry
5255

5356

54-
func _is_running_tests() -> bool:
57+
func _is_running_tests_from_editor() -> bool:
5558
return "res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn" in OS.get_cmdline_args()
59+
60+
61+
func _should_run_tests() -> bool:
62+
return OS.get_environment("SENTRY_TEST") == "1"
63+
64+
65+
func _run_tests() -> void:
66+
if FileAccess.file_exists("res://test/util/test_run.gd"):
67+
var test_run = load("res://test/util/test_run.gd").new()
68+
await test_run.execute()

project/test/util/test_run.gd

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
extends RefCounted
2+
## Execute tests based on ENV variables and quit.
3+
##
4+
## Environment variables:
5+
## - SENTRY_TEST_INCLUDE ';'-separated list of paths to include in testing
6+
7+
8+
func execute() -> void:
9+
print(">>> Initializing testing")
10+
var scene_tree := Engine.get_main_loop() as SceneTree
11+
var included_paths: PackedStringArray = _get_included_paths()
12+
print(" -- Tests included: ", included_paths)
13+
14+
# Remove all existing nodes.
15+
print(" -- Cleaning scene tree...")
16+
for n: Node in scene_tree.root.get_children():
17+
n.queue_free()
18+
await scene_tree.process_frame
19+
20+
# Add test runner node.
21+
print(" -- Adding test runner...")
22+
var test_runner: Node = load("res://test/util/test_runner.gd").new()
23+
scene_tree.root.add_child(test_runner)
24+
for path in included_paths:
25+
test_runner.include_tests(path)
26+
27+
# Wait for completion.
28+
await test_runner.finished
29+
print(">>> Test run complete with code: ", str(test_runner.result_code))
30+
scene_tree.quit(test_runner.result_code)
31+
32+
33+
func _get_included_paths() -> PackedStringArray:
34+
var include_paths: String = OS.get_environment("SENTRY_TEST_INCLUDE")
35+
return include_paths.split(";", false)

project/test/util/test_run.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://bgqnue8f5gt3o

project/test/util/test_runner.gd

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
class_name MyTestRunner
2+
extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd"
3+
4+
# Usage:
5+
# var TestRunner := load("res://my_test_runner.gd")
6+
# var runner = TestRunner.new()
7+
# add_child(runner)
8+
# runner.include_tests("res://tests/")
9+
10+
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
11+
12+
signal finished(result_code)
13+
14+
enum Result {
15+
SUCCESS = 0,
16+
FAILURE = 100,
17+
DIDNT_RUN = 200,
18+
TESTS_NOT_FOUND = 204
19+
}
20+
21+
22+
class Stats:
23+
var num_total: int = 0
24+
var num_failed: int = 0
25+
var num_errors: int = 0
26+
var num_skipped: int = 0
27+
var num_flaky: int = 0
28+
29+
func clear():
30+
num_total = 0
31+
num_errors = 0
32+
num_failed = 0
33+
num_skipped = 0
34+
num_flaky = 0
35+
36+
37+
var stats := Stats.new()
38+
var suite_stats := Stats.new()
39+
var result_code: int = Result.DIDNT_RUN
40+
41+
var _included_tests := PackedStringArray()
42+
43+
44+
## Initialize test execution
45+
func init_runner() -> void:
46+
print_rich(_step(), _prominent("Initializing test runner..."))
47+
_test_cases = _discover_tests()
48+
if _test_cases.is_empty():
49+
print_rich(_error("No test cases found!"))
50+
_finish(Result.TESTS_NOT_FOUND)
51+
return
52+
_state = RUN
53+
54+
55+
## Returns the exit code based on test results.[br]
56+
## Maps test report status to process exit codes.
57+
func get_exit_code() -> int:
58+
if stats.num_total == 0:
59+
return Result.DIDNT_RUN
60+
elif stats.num_failed > 0 or stats.num_errors > 0:
61+
return Result.FAILURE
62+
return Result.SUCCESS
63+
64+
65+
## Cleanup and quit the runner.
66+
func quit(_exit_code: int) -> void:
67+
_finish(_exit_code)
68+
69+
70+
## Add file or dir for test discovery.
71+
func include_tests(path: String) -> void:
72+
_included_tests.append(path)
73+
74+
75+
## Discover tests added with include_tests()
76+
func _discover_tests() -> Array[GdUnitTestCase]:
77+
var gdunit_test_discover_added := GdUnitSignals.instance().gdunit_test_discover_added
78+
79+
var scanner := GdUnitTestSuiteScanner.new()
80+
for path in _included_tests:
81+
var scripts := scanner.scan(path)
82+
for script in scripts:
83+
print_rich(_step(), "Scanning: ", script.resource_path)
84+
GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void:
85+
print_rich(_substep(), "Discovered %s" % test.display_name)
86+
_test_cases.append(test)
87+
gdunit_test_discover_added.emit(test)
88+
)
89+
90+
return _test_cases
91+
92+
93+
func _finish(code: int) -> void:
94+
_state = EXIT
95+
result_code = code
96+
GdUnitTools.dispose_all()
97+
await GdUnitMemoryObserver.gc_on_guarded_instances()
98+
await get_tree().process_frame
99+
await get_tree().physics_frame
100+
finished.emit(result_code)
101+
102+
103+
## Process test events.
104+
func _on_gdunit_event(event: GdUnitEvent) -> void:
105+
match event.type():
106+
GdUnitEvent.INIT:
107+
print_rich(_step(), _prominent("Initializing..."))
108+
stats.clear()
109+
suite_stats.clear()
110+
111+
GdUnitEvent.STOP:
112+
print_rich(_step(), _prominent("Finished all tests."))
113+
_print_stats(stats, "Overall Summary")
114+
115+
GdUnitEvent.TESTSUITE_BEFORE:
116+
print_rich(_step(), _prominent("Loading: "), _suite(event.resource_path()))
117+
suite_stats.clear()
118+
119+
GdUnitEvent.TESTSUITE_AFTER:
120+
print_rich(_substep(), _prominent("Finished: "), _suite(event.resource_path()))
121+
_print_failure_report(event.reports())
122+
_print_stats(suite_stats, "Summary")
123+
stats.num_total += suite_stats.num_total
124+
stats.num_errors += suite_stats.num_errors
125+
stats.num_failed += suite_stats.num_failed
126+
stats.num_skipped += suite_stats.num_skipped
127+
stats.num_flaky += suite_stats.num_flaky
128+
129+
GdUnitEvent.TESTCASE_BEFORE:
130+
var test := _test_session.find_test_by_id(event.guid())
131+
print_rich(_substep(), _prominent("Started: "),
132+
_suite(test.suite_name), " > ", _case(test.display_name))
133+
134+
GdUnitEvent.TESTCASE_AFTER:
135+
suite_stats.num_total += 1
136+
suite_stats.num_errors += event.error_count()
137+
suite_stats.num_failed += event.failed_count()
138+
suite_stats.num_skipped += event.skipped_count()
139+
suite_stats.num_flaky += 1 if event.is_flaky() else 0
140+
141+
var test := _test_session.find_test_by_id(event.guid())
142+
if event.is_success():
143+
_print_result(_bold(_success("PASSED")), test.suite_name, test.display_name)
144+
elif event.is_skipped():
145+
_print_result(_bold(_muted("SKIPPED")), test.suite_name, test.display_name)
146+
elif event.is_failed() or event.is_error():
147+
_print_result(_bold(_error("FAILED")), test.suite_name, test.display_name)
148+
_print_failure_report(event.reports())
149+
elif event.is_warning():
150+
_print_result(_bold(_warning("WARNING")), test.suite_name, test.display_name)
151+
_print_failure_report(event.reports())
152+
153+
154+
# *** CONSOLE OUTPUT
155+
156+
static func _colored(text: String, color: Color) -> String:
157+
return "[color=%s]%s[/color]" % [color.to_html(), text]
158+
159+
160+
static func _bold(text: String) -> String:
161+
return "[b]%s[/b]" % text
162+
163+
164+
static func _step() -> String: return _bold(_colored("==> ", Color.WHITE))
165+
static func _substep() -> String: return _bold(_colored("--> ", Color.WHITE))
166+
static func _prominent(text: String) -> String: return _bold(_colored(text, Color.WHITE))
167+
168+
static func _primary(text: String) -> String: return _colored(text, Color.WHITE)
169+
static func _accent(text: String) -> String: return _colored(text, Color.DODGER_BLUE)
170+
static func _muted(text: String) -> String: return _colored(text, Color.GRAY)
171+
static func _success(text: String) -> String: return _colored(text, Color.GREEN)
172+
static func _warning(text: String) -> String: return _colored(text, Color.GOLDENROD)
173+
static func _error(text: String) -> String: return _colored(text, Color.RED)
174+
175+
static func _suite(text: String) -> String: return _colored(text, Color.DARK_CYAN)
176+
static func _case(text: String) -> String: return _colored(text, Color.CYAN)
177+
178+
179+
static func _print_result(status: String, test_suite: String, test_case: String) -> void:
180+
print_rich(_substep(), status, ": ",
181+
_suite(test_suite), " > ", _case(test_case))
182+
183+
184+
static func _print_failure_report(reports: Array[GdUnitReport]) -> void:
185+
for report in reports:
186+
if (
187+
report.is_failure()
188+
or report.is_error()
189+
or report.is_warning()
190+
or report.is_skipped()
191+
):
192+
var text: String = str(report).indent(" ".repeat(4))
193+
print_rich(text)
194+
195+
196+
static func _print_stats(p_stats: Stats, p_header: String) -> void:
197+
var total: String = " %d total" % p_stats.num_total
198+
var errors: String = " %d errors" % p_stats.num_errors
199+
var failed: String = " %d failed" % p_stats.num_failed
200+
var skipped: String = " %d skipped" % p_stats.num_skipped
201+
var flaky: String = " %d flaky" % p_stats.num_flaky
202+
print_rich(
203+
_substep(),
204+
_bold(_accent(p_header)), ": ",
205+
_primary(total),
206+
_error(errors) if p_stats.num_errors > 0 else _primary(errors),
207+
_error(failed) if p_stats.num_failed > 0 else _primary(failed),
208+
_primary(skipped),
209+
_warning(flaky) if p_stats.num_flaky > 0 else _primary(flaky)
210+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://dfywf08xk1gra

0 commit comments

Comments
 (0)