Replace the broken test suite with working GdUnit4 tests

The previous tests never compiled: they used GUT's assert functions,
Godot 3 APIs and reassigned autoload functions, so no test ran.

- 90 tests: unit tests for the playlist, songs, player scoring, time
  formatting, search and the server client, plus integration tests
  that run MainWindow and SyncWindow against a fake server
- Keybind tests check that Alt+<number> and Ctrl+Alt+<number> keep
  following the player rows while players are added and removed
- run_tests.sh finds Godot and runs the suite headless (~10 seconds)
- CI runs the tests before the build job in gitea-build.yaml; the
  separate test.yml used a GitHub action that does not exist
- GdUnit4 is committed instead of ignored, since the project enables
  it, and .gitea/ is no longer ignored
- Exported builds leave out tests/ and addons/gdUnit4/
This commit is contained in:
Sansan committed 2026-10-05 19:33:41 +02:00
1 parent 87ff3d899b
commit 91adf52cf4
559 files changed
+32474 -2100

No files matched your search

@@ -0,0 +1,172 @@
extends GdUnitFuncAssert
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const DEFAULT_TIMEOUT := 2000
var _current_value_provider :ValueProvider
var _custom_failure_message :String = ""
var _additional_failure_message: String = ""
var _line_number := -1
var _timeout := DEFAULT_TIMEOUT
var _interrupted := false
var _sleep_timer :Timer = null
func _init(instance :Object, func_name :String, args := Array()) -> void:
_line_number = GdUnitStackTrace.new().get_line_number()
GdAssertReports.reset_last_error_line_number()
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
# verify at first the function name exists
if not instance.has_method(func_name):
@warning_ignore("return_value_discarded")
report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance])
_interrupted = true
else:
_current_value_provider = CallBackValueProvider.new(instance, func_name, args)
func _notification(what :int) -> void:
if what == NOTIFICATION_PREDELETE:
_interrupted = true
var main_node :Node = (Engine.get_main_loop() as SceneTree).root
if is_instance_valid(_current_value_provider):
_current_value_provider.dispose()
_current_value_provider = null
if is_instance_valid(_sleep_timer):
_sleep_timer.set_wait_time(0.0001)
_sleep_timer.stop()
main_node.remove_child(_sleep_timer)
_sleep_timer.free()
_sleep_timer = null
func report_success() -> GdUnitFuncAssert:
GdAssertReports.report_success()
return self
func report_error(failure :String) -> GdUnitFuncAssert:
var failure_message := GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
GdAssertReports.report_error(GdUnitError.new(failure_message, _line_number, GdUnitStackTrace.new()))
return self
func override_failure_message(message: String) -> GdUnitFuncAssert:
_custom_failure_message = message
return self
func append_failure_message(message: String) -> GdUnitFuncAssert:
_additional_failure_message = message
return self
func wait_until(timeout := 2000) -> GdUnitFuncAssert:
if timeout <= 0:
push_warning("Invalid timeout param, alloed timeouts must be grater than 0. Use default timeout instead")
_timeout = DEFAULT_TIMEOUT
else:
_timeout = timeout
return self
func is_null() -> GdUnitFuncAssert:
await _validate_callback(cb_is_null)
return self
func is_not_null() -> GdUnitFuncAssert:
await _validate_callback(cb_is_not_null)
return self
func is_false() -> GdUnitFuncAssert:
await _validate_callback(cb_is_false)
return self
func is_true() -> GdUnitFuncAssert:
await _validate_callback(cb_is_true)
return self
func is_equal(expected: Variant) -> GdUnitFuncAssert:
await _validate_callback(cb_is_equal, expected)
return self
func is_not_equal(expected: Variant) -> GdUnitFuncAssert:
await _validate_callback(cb_is_not_equal, expected)
return self
# we need actually to define this Callable as functions otherwise we results into leaked scripts here
# this is actually a Godot bug and needs this kind of workaround
func cb_is_null(c :Variant, _e :Variant) -> bool: return c == null
func cb_is_not_null(c :Variant, _e :Variant) -> bool: return c != null
func cb_is_false(c :Variant, _e :Variant) -> bool: return c == false
func cb_is_true(c :Variant, _e :Variant) -> bool: return c == true
func cb_is_equal(c :Variant, e :Variant) -> bool: return GdObjects.equals(c,e)
func cb_is_not_equal(c :Variant, e :Variant) -> bool: return not GdObjects.equals(c, e)
func do_interrupt() -> void:
_interrupted = true
func _validate_callback(predicate :Callable, expected :Variant = null) -> void:
if _interrupted:
return
GdUnitMemoryObserver.guard_instance(self)
var time_scale := Engine.get_time_scale()
var timer := Timer.new()
timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id())
var scene_tree := Engine.get_main_loop() as SceneTree
scene_tree.root.add_child(timer)
timer.add_to_group("GdUnitTimers")
@warning_ignore("return_value_discarded")
timer.timeout.connect(do_interrupt, CONNECT_DEFERRED)
timer.set_one_shot(true)
timer.start((_timeout/1000.0)*time_scale)
_sleep_timer = Timer.new()
_sleep_timer.set_name("gdunit_funcassert_sleep_timer_%d" % _sleep_timer.get_instance_id() )
scene_tree.root.add_child(_sleep_timer)
while true:
var current :Variant = await next_current_value()
# is interupted or predicate success
if _interrupted or predicate.call(current, expected):
break
if is_instance_valid(_sleep_timer):
_sleep_timer.start(0.05)
await _sleep_timer.timeout
_sleep_timer.stop()
await scene_tree.process_frame
if _interrupted:
# https://github.com/godotengine/godot/issues/73052
#var predicate_name = predicate.get_method()
var predicate_name :String = str(predicate).split('::')[1]
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_interrupted(
predicate_name.strip_edges().trim_prefix("cb_"),
expected,
LocalTime.elapsed(_timeout)
)
)
else:
@warning_ignore("return_value_discarded")
report_success()
_sleep_timer.free()
timer.free()
GdUnitMemoryObserver.unguard_instance(self)
func next_current_value() -> Variant:
@warning_ignore("redundant_await")
if is_instance_valid(_current_value_provider):
return await _current_value_provider.get_value()
return "invalid value"