Updated gdUnit4

main
Martin Felis 2024-12-03 08:17:48 +01:00
parent b6d402eb93
commit a521cf4e96
163 changed files with 5422 additions and 3378 deletions

View File

@ -1 +0,0 @@
{"included":{"res://tests/scenes/game_tests.gd":["test_save_game"]},"server_port":31002,"skipped":{},"version":"1.0"}

View File

@ -68,7 +68,7 @@ func _idle(_delta :float) -> void:
exit(RETURN_ERROR, result.error_message())
return
_console.prints_color("Added testcase: %s" % result.value(), Color.CORNFLOWER_BLUE)
print_json_result(result.value())
print_json_result(result.value() as Dictionary)
exit(RETURN_SUCCESS)
@ -85,7 +85,7 @@ func exit(code :int, message :String = "") -> void:
func print_json_result(result :Dictionary) -> void:
# convert back to system path
var path := ProjectSettings.globalize_path(result["path"]);
var path := ProjectSettings.globalize_path(result["path"] as String)
var json := 'JSON_RESULT:{"TestCases" : [{"line":%d, "path": "%s"}]}' % [result["line"], path]
prints(json)

View File

@ -33,6 +33,7 @@ class CLIRunner:
var _headless_mode_ignore := false
var _runner_config := GdUnitRunnerConfig.new()
var _runner_config_file := ""
var _debug_cmd_args: = PackedStringArray()
var _console := CmdConsole.new()
var _cmd_options := CmdOptions.new([
CmdOption.new(
@ -105,9 +106,10 @@ class CLIRunner:
func _ready() -> void:
_state = INIT
_report_dir = GdUnitFileAccess.current_dir() + "reports"
_executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new()
_executor = GdUnitTestSuiteExecutor.new()
# stop checked first test failure to fail fast
_executor.fail_fast(true)
@warning_ignore("unsafe_cast")
(_executor as GdUnitTestSuiteExecutor).fail_fast(true)
if GdUnit4CSharpApiLoader.is_mono_supported():
prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
_cs_executor = GdUnit4CSharpApiLoader.create_executor(self)
@ -123,6 +125,7 @@ class CLIRunner:
prints("Finallize .. done")
@warning_ignore("unsafe_method_access")
func _process(_delta :float) -> void:
match _state:
INIT:
@ -135,7 +138,8 @@ class CLIRunner:
else:
set_process(false)
# process next test suite
var test_suite := _test_suites_to_process.pop_front() as Node
var test_suite: Node = _test_suites_to_process.pop_front()
if _cs_executor != null and _cs_executor.IsExecutable(test_suite):
_cs_executor.Execute(test_suite)
await _cs_executor.ExecutionCompleted
@ -185,6 +189,7 @@ class CLIRunner:
"Disabled fail fast!",
Color.DEEP_SKY_BLUE
)
@warning_ignore("unsafe_method_access")
_executor.fail_fast(false)
@ -199,13 +204,13 @@ class CLIRunner:
func show_version() -> void:
_console.prints_color(
"Godot %s" % Engine.get_version_info().get("string"),
"Godot %s" % Engine.get_version_info().get("string") as String,
Color.DARK_SALMON
)
var config := ConfigFile.new()
config.load("addons/gdUnit4/plugin.cfg")
_console.prints_color(
"GdUnit4 %s" % config.get_value("plugin", "version"),
"GdUnit4 %s" % config.get_value("plugin", "version") as String,
Color.DARK_SALMON
)
quit(RETURN_SUCCESS)
@ -274,6 +279,12 @@ class CLIRunner:
quit(RETURN_SUCCESS)
func get_cmdline_args() -> PackedStringArray:
if _debug_cmd_args.is_empty():
return OS.get_cmdline_args()
return _debug_cmd_args
func init_gd_unit() -> void:
_console.prints_color(
"""
@ -284,7 +295,7 @@ class CLIRunner:
).new_line()
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
var result := cmd_parser.parse(OS.get_cmdline_args())
var result := cmd_parser.parse(get_cmdline_args())
if result.is_error():
show_options()
_console.prints_error(result.error_message())
@ -297,7 +308,8 @@ class CLIRunner:
return
# build runner config by given commands
var commands :Array[CmdCommand] = []
commands.append_array(result.value())
@warning_ignore("unsafe_cast")
commands.append_array(result.value() as Array)
result = (
CmdCommandHandler.new(_cmd_options)
.register_cb("-help", Callable(self, "show_help"))
@ -385,7 +397,7 @@ class CLIRunner:
var skipped := config.skipped()
if skipped.is_empty():
return
_console.prints_warning("Found excluded test suite's configured at '%s'" % _runner_config_file)
for test_suite in test_suites:
# skipp c# testsuites for now
if test_suite.get_script() == null:
@ -395,24 +407,26 @@ class CLIRunner:
# Dictionary[String, PackedStringArray]
func skip_suite(test_suite: Node, skipped: Dictionary) -> void:
var skipped_suites :Array[String] = skipped.keys()
var skipped_suites :Array = skipped.keys()
var suite_name := test_suite.get_name()
var test_suite_path: String = (
test_suite.get_meta("ResourcePath") if test_suite.get_script() == null
else test_suite.get_script().resource_path
)
for suite_to_skip in skipped_suites:
for suite_to_skip: String in skipped_suites:
# if suite skipped by path or name
if (
suite_to_skip == test_suite_path
or (suite_to_skip.is_valid_filename() and suite_to_skip == suite_name)
):
var skipped_tests: Array[String] = skipped.get(suite_to_skip)
var skip_reason := "Excluded by config '%s'" % _runner_config_file
var skipped_tests: PackedStringArray = skipped.get(suite_to_skip)
var skip_reason := "Excluded by configuration"
# if no tests skipped test the complete suite is skipped
if skipped_tests.is_empty():
_console.prints_warning("Mark test suite '%s' as skipped!" % suite_to_skip)
_console.prints_warning("Mark the entire test suite '%s' as skipped!" % test_suite_path)
@warning_ignore("unsafe_property_access")
test_suite.__is_skipped = true
@warning_ignore("unsafe_property_access")
test_suite.__skip_reason = skip_reason
else:
# skip tests
@ -443,10 +457,8 @@ class CLIRunner:
func _on_gdunit_event(event: GdUnitEvent) -> void:
match event.type():
GdUnitEvent.INIT:
_report = GdUnitHtmlReport.new(_report_dir)
_report = GdUnitHtmlReport.new(_report_dir, _report_max)
GdUnitEvent.STOP:
if _report == null:
_report = GdUnitHtmlReport.new(_report_dir)
var report_path := _report.write()
_report.delete_history(_report_max)
JUnitXmlReport.new(_report._report_path, _report.iteration()).write(_report)
@ -464,45 +476,31 @@ class CLIRunner:
Color.CORNFLOWER_BLUE
)
GdUnitEvent.TESTSUITE_BEFORE:
_report.add_testsuite_report(
GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name(), event.total_count())
)
_report.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count())
GdUnitEvent.TESTSUITE_AFTER:
_report.update_test_suite_report(
_report.add_testsuite_reports(
event.resource_path(),
event.elapsed_time(),
event.is_error(),
event.is_failed(),
event.is_warning(),
event.is_skipped(),
event.skipped_count(),
event.error_count(),
event.failed_count(),
event.orphan_nodes(),
event.elapsed_time(),
event.reports()
)
GdUnitEvent.TESTCASE_BEFORE:
_report.add_testcase_report(
event.resource_path(),
GdUnitTestCaseReport.new(
event.resource_path(),
event.suite_name(),
event.test_name()
)
)
_report.add_testcase(event.resource_path(), event.suite_name(), event.test_name())
GdUnitEvent.TESTCASE_AFTER:
var test_report := GdUnitTestCaseReport.new(
event.resource_path(),
event.suite_name(),
_report.set_testcase_counters(event.resource_path(),
event.test_name(),
event.is_error(),
event.is_failed(),
event.failed_count(),
event.orphan_nodes(),
event.is_skipped(),
event.reports(),
event.elapsed_time()
)
_report.update_testcase_report(event.resource_path(), test_report)
event.is_flaky(),
event.elapsed_time())
_report.add_testcase_reports(event.resource_path(), event.test_name(), event.reports())
GdUnitEvent.TESTCASE_STATISTICS:
_report.update_testsuite_counters(event.resource_path(), event.is_error(), event.failed_count(), event.orphan_nodes(),\
event.is_skipped(), event.is_flaky(), event.elapsed_time())
print_status(event)
@ -556,11 +554,12 @@ class CLIRunner:
_print_failure_report(event.reports())
_print_status(event)
_console.prints_color(
"Statistics: | %d tests cases | %d error | %d failed | %d skipped | %d orphans |\n"
"Statistics: | %d tests cases | %d error | %d failed | %d flaky | %d skipped | %d orphans |\n"
% [
_report.test_count(),
_report.error_count(),
_report.failure_count(),
_report.flaky_count(),
_report.skipped_count(),
_report.orphan_count()
],
@ -587,14 +586,22 @@ class CLIRunner:
func _print_status(event: GdUnitEvent) -> void:
if event.is_skipped():
if event.is_flaky() and event.is_success():
var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT)
_console.print_color("FLAKY (%d retries)" % retries, Color.GREEN_YELLOW, CmdConsole.BOLD | CmdConsole.ITALIC)
elif event.is_success():
_console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD)
elif event.is_skipped():
_console.print_color("SKIPPED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.ITALIC)
elif event.is_failed() or event.is_error():
_console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD)
elif event.orphan_nodes() > 0:
_console.print_color("PASSED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE)
else:
_console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD)
var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT)
if retries > 1:
_console.print_color("FAILED (retry %d)" % retries, Color.FIREBRICK, CmdConsole.BOLD)
else:
_console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD)
elif event.is_warning():
_console.print_color("WARNING", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE)
_console.prints_color(
" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE
)
@ -615,6 +622,7 @@ func _initialize() -> void:
# do not use print statements on _finalize it results in random crashes
func _finalize() -> void:
queue_delete(_cli_runner)
if OS.is_stdout_verbose():
prints("Finallize ..")
prints("-Orphan nodes report-----------------------")

View File

@ -4,23 +4,30 @@ extends MainLoop
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
# gdlint: disable=max-line-length
const NO_LOG_TEMPLATE = """
const LOG_FRAME_TEMPLATE = """
<!DOCTYPE html>
<html>
<html style="display: inline-grid;">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<title>Logging</title>
<link href="css/style.css" rel="stylesheet" type="text/css"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Godot Logging</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div>
<h1>No logging available!</h1>
</br>
<p>For logging to occur, you must check Enable File Logging in Project Settings.</p>
<p>You can enable Logging <b>Project Settings</b> > <b>Logging</b> > <b>File Logging</b> > <b>Enable File Logging</b> in the Project Settings.</p>
<body style="background-color: #eee;">
<div class="godot-report-frame"">
${content}
</div>
</body>
</html>
"""
const NO_LOG_MESSAGE = """
<h3>No logging available!</h3>
</br>
<p>In order for logging to take place, you must activate the Activate file logging option in the project settings.</p>
<p>You can enable the logging under:
<b>Project Settings</b> > <b>Debug</b> > <b>File Logging</b> > <b>Enable File Logging</b> in the project settings.</p>
"""
#warning-ignore-all:return_value_discarded
@ -34,48 +41,65 @@ var _cmd_options := CmdOptions.new([
)
])
var _report_root_path: String
var _current_report_path: String
var _debug_cmd_args := PackedStringArray()
func _init() -> void:
_report_root_path = GdUnitFileAccess.current_dir() + "reports"
set_report_directory(GdUnitFileAccess.current_dir() + "reports")
set_current_report_path()
func _process(_delta :float) -> bool:
func _process(_delta: float) -> bool:
# check if reports exists
if not reports_available():
prints("no reports found")
return true
# scan for latest report path
var iteration := GdUnitFileAccess.find_last_path_index(
_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX
)
var report_path := "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration]
# only process if godot logging is enabled
if not GdUnitSettings.is_log_enabled():
_patch_report(report_path, "")
write_report(NO_LOG_MESSAGE, "")
return true
# parse possible custom report path,
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
# ignore erros and exit quitly
if cmd_parser.parse(OS.get_cmdline_args(), true).is_error():
if cmd_parser.parse(get_cmdline_args(), true).is_error():
return true
CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory)
# scan for latest godot log and copy to report
var godot_log := _scan_latest_godot_log()
var result := _copy_and_pach(godot_log, report_path)
var godot_log_file := scan_latest_godot_log()
var result := read_log_file_content(godot_log_file)
if result.is_error():
push_error(result.error_message())
write_report(result.error_message(), godot_log_file)
return true
_patch_report(report_path, godot_log)
write_report(result.value_as_string(), godot_log_file)
return true
func set_current_report_path() -> void:
# scan for latest report directory
var iteration := GdUnitFileAccess.find_last_path_index(
_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX
)
_current_report_path = "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration]
func set_report_directory(path: String) -> void:
_report_root_path = path
func _scan_latest_godot_log() -> String:
func get_log_report_html() -> String:
return _current_report_path + "/godot_report_log.html"
func reports_available() -> bool:
return DirAccess.dir_exists_absolute(_report_root_path)
func scan_latest_godot_log() -> String:
var path := GdUnitSettings.get_log_path().get_base_dir()
var files_sorted := Array()
for file in GdUnitFileAccess.scan_dir(path):
@ -83,59 +107,60 @@ func _scan_latest_godot_log() -> String:
files_sorted.append(file_name)
# sort by name, the name contains the timestamp so we sort at the end by timestamp
files_sorted.sort()
return files_sorted[-1]
return files_sorted.back()
func _patch_report(report_path: String, godot_log: String) -> void:
var index_file := FileAccess.open("%s/index.html" % report_path, FileAccess.READ_WRITE)
func read_log_file_content(log_file: String) -> GdUnitResult:
var file := FileAccess.open(log_file, FileAccess.READ)
if file == null:
return GdUnitResult.error(
"Can't find log file '%s'. Error: %s"
% [log_file, error_string(FileAccess.get_open_error())]
)
var content := "<pre>" + file.get_as_text()
# patch out console format codes
for color_index in range(0, 256):
var to_replace := "[38;5;%dm" % color_index
content = content.replace(to_replace, "")
content += "</pre>"
content = content\
.replace("", "")\
.replace(CmdConsole.CSI_BOLD, "")\
.replace(CmdConsole.CSI_ITALIC, "")\
.replace(CmdConsole.CSI_UNDERLINE, "")
return GdUnitResult.success(content)
func write_report(content: String, godot_log_file: String) -> GdUnitResult:
var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE)
if file == null:
return GdUnitResult.error(
"Can't open to write '%s'. Error: %s"
% [get_log_report_html(), error_string(FileAccess.get_open_error())]
)
var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content)
file.store_string(report_html)
_update_index_html(godot_log_file)
return GdUnitResult.success(file)
func _update_index_html(godot_log_file: String) -> void:
var index_file := FileAccess.open("%s/index.html" % _current_report_path, FileAccess.READ_WRITE)
if index_file == null:
push_error(
"Can't add log path to index.html. Error: %s"
% error_string(FileAccess.get_open_error())
)
return
# if no log file available than add a information howto enable it
if godot_log.is_empty():
FileAccess.open(
"%s/logging_not_available.html" % report_path,
FileAccess.WRITE).store_string(NO_LOG_TEMPLATE)
var log_file := "logging_not_available.html" if godot_log.is_empty() else godot_log.get_file()
var content := index_file.get_as_text().replace("${log_file}", log_file)
var content := index_file.get_as_text()\
.replace("${log_report}", get_log_report_html())\
.replace("${godot_log_file}", godot_log_file)
# overide it
index_file.seek(0)
index_file.store_string(content)
func _copy_and_pach(from_file: String, to_dir: String) -> GdUnitResult:
var result := GdUnitFileAccess.copy_file(from_file, to_dir)
if result.is_error():
return result
var file := FileAccess.open(from_file, FileAccess.READ)
if file == null:
return GdUnitResult.error(
"Can't find file '%s'. Error: %s"
% [from_file, error_string(FileAccess.get_open_error())]
)
var content := file.get_as_text()
# patch out console format codes
for color_index in range(0, 256):
var to_replace := "[38;5;%dm" % color_index
content = content.replace(to_replace, "")
content = content\
.replace("", "")\
.replace(CmdConsole.CSI_BOLD, "")\
.replace(CmdConsole.CSI_ITALIC, "")\
.replace(CmdConsole.CSI_UNDERLINE, "")
var to_file := to_dir + "/" + from_file.get_file()
file = FileAccess.open(to_file, FileAccess.WRITE)
if file == null:
return GdUnitResult.error(
"Can't open to write '%s'. Error: %s"
% [to_file, error_string(FileAccess.get_open_error())]
)
file.store_string(content)
return GdUnitResult.empty()
func reports_available() -> bool:
return DirAccess.dir_exists_absolute(_report_root_path)
func get_cmdline_args() -> PackedStringArray:
if _debug_cmd_args.is_empty():
return OS.get_cmdline_args()
return _debug_cmd_args

View File

@ -1,99 +0,0 @@
#!/usr/bin/env -S godot -s
@tool
extends SceneTree
const CmdConsole = preload("res://addons/gdUnit4/src/cmd/CmdConsole.gd")
func _initialize() -> void:
set_auto_accept_quit(false)
var scanner := SourceScanner.new(self)
root.add_child(scanner)
# gdlint: disable=trailing-whitespace
class SourceScanner extends Node:
enum {
INIT,
STARTUP,
SCAN,
QUIT,
DONE
}
var _state := INIT
var _console := CmdConsole.new()
var _elapsed_time := 0.0
var _plugin: EditorPlugin
var _fs: EditorFileSystem
var _scene: SceneTree
func _init(scene :SceneTree) -> void:
_scene = scene
_console.prints_color("""
========================================================================
Running project scan:""".dedent(),
Color.CORNFLOWER_BLUE
)
_state = INIT
func _process(delta :float) -> void:
_elapsed_time += delta
set_process(false)
await_inital_scan()
await scan_project()
set_process(true)
# !! don't use any await in this phase otherwise the editor will be instable !!
func await_inital_scan() -> void:
if _state == INIT:
_console.prints_color("Wait initial scanning ...", Color.DARK_GREEN)
_plugin = EditorPlugin.new()
_fs = _plugin.get_editor_interface().get_resource_filesystem()
_plugin.get_editor_interface().set_plugin_enabled("gdUnit4", false)
_state = STARTUP
if _state == STARTUP:
if _fs.is_scanning():
_console.progressBar(_fs.get_scanning_progress() * 100 as int)
# we wait 10s in addition to be on the save site the scanning is done
if _elapsed_time > 10.0:
_console.progressBar(100)
_console.new_line()
_console.prints_color("initial scanning ... done", Color.DARK_GREEN)
_state = SCAN
func scan_project() -> void:
if _state != SCAN:
return
_console.prints_color("Scan project: ", Color.SANDY_BROWN)
await get_tree().process_frame
_fs.scan_sources()
await get_tree().create_timer(5).timeout
_console.prints_color("Scan: ", Color.SANDY_BROWN)
_console.progressBar(0)
await get_tree().process_frame
_fs.scan()
while _fs.is_scanning():
await get_tree().process_frame
_console.progressBar(_fs.get_scanning_progress() * 100 as int)
await get_tree().create_timer(10).timeout
_console.progressBar(100)
_console.new_line()
_plugin.free()
_console.prints_color("""
Scan project done.
========================================================================""".dedent(),
Color.CORNFLOWER_BLUE
)
await get_tree().process_frame
await get_tree().physics_frame
queue_free()
# force quit editor
_state = DONE
_scene.quit(0)

View File

@ -3,5 +3,5 @@
name="gdUnit4"
description="Unit Testing Framework for Godot Scripts"
author="Mike Schulze"
version="4.3.1"
version="4.4.3"
script="plugin.gd"

View File

@ -1,17 +1,20 @@
@tool
extends EditorPlugin
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const GdUnitTestDiscoverGuard := preload("res://addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd")
const GdUnitTools := preload ("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const GdUnitTestDiscoverGuard := preload ("res://addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd")
var _gd_inspector :Node
var _server_node :Node
var _gd_console :Node
var _gd_inspector: Control
var _gd_console: Control
var _guard: GdUnitTestDiscoverGuard
func _enter_tree() -> void:
if check_running_in_test_env():
@warning_ignore("return_value_discarded")
CmdConsole.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!")
return
if Engine.get_version_info().hex < 0x40200:
prints("GdUnit4 plugin requires a minimum of Godot 4.2.x Version!")
return
@ -21,34 +24,39 @@ func _enter_tree() -> void:
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector)
# install the GdUnit Console
_gd_console = load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn").instantiate()
@warning_ignore("return_value_discarded")
add_control_to_bottom_panel(_gd_console, "gdUnitConsole")
_server_node = load("res://addons/gdUnit4/src/network/GdUnitServer.tscn").instantiate()
Engine.get_main_loop().root.add_child.call_deferred(_server_node)
prints("Loading GdUnit4 Plugin success")
if GdUnitSettings.is_update_notification_enabled():
var update_tool :Node = load("res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn").instantiate()
var update_tool: Node = load("res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn").instantiate()
Engine.get_main_loop().root.add_child.call_deferred(update_tool)
if GdUnit4CSharpApiLoader.is_mono_supported():
prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
# connect to be notified for script changes to be able to discover new tests
_guard = GdUnitTestDiscoverGuard.new()
@warning_ignore("return_value_discarded")
resource_saved.connect(_on_resource_saved)
func _exit_tree() -> void:
if check_running_in_test_env():
return
if is_instance_valid(_gd_inspector):
remove_control_from_docks(_gd_inspector)
GodotVersionFixures.free_fix(_gd_inspector)
_gd_inspector.free()
if is_instance_valid(_gd_console):
remove_control_from_bottom_panel(_gd_console)
_gd_console.free()
if is_instance_valid(_server_node):
Engine.get_main_loop().root.remove_child.call_deferred(_server_node)
_server_node.queue_free()
GdUnitTools.dispose_all.call_deferred()
GdUnitTools.dispose_all(true)
prints("Unload GdUnit4 Plugin success")
func _on_resource_saved(resource :Resource) -> void:
func check_running_in_test_env() -> bool:
var args := OS.get_cmdline_args()
args.append_array(OS.get_cmdline_user_args())
return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import")
func _on_resource_saved(resource: Resource) -> void:
if resource is Script:
_guard.discover(resource)
await _guard.discover(resource as Script)

View File

@ -158,3 +158,14 @@ func extractv(
extractor8 :GdUnitValueExtractor = null,
extractor9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert:
return self
@warning_ignore("unused_parameter")
func override_failure_message(message :String) -> GdUnitArrayAssert:
return self
@warning_ignore("unused_parameter")
func append_failure_message(message :String) -> GdUnitArrayAssert:
return self

View File

@ -18,19 +18,19 @@ func is_not_null():
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
@warning_ignore("untyped_declaration")
func is_equal(expected):
func is_equal(expected: Variant):
return self
## Verifies that the current value is not equal to expected one.
@warning_ignore("unused_parameter")
@warning_ignore("untyped_declaration")
func is_not_equal(expected):
func is_not_equal(expected: Variant):
return self
@warning_ignore("untyped_declaration")
func test_fail():
func do_fail():
return self
@ -39,3 +39,11 @@ func test_fail():
@warning_ignore("untyped_declaration")
func override_failure_message(message :String):
return self
## Appends a custom message to the failure message.
## This can be used to add additional infromations to the generated failure message.
@warning_ignore("unused_parameter")
@warning_ignore("untyped_declaration")
func append_failure_message(message :String):
return self

View File

@ -1,8 +1,6 @@
class_name GdUnitAwaiter
extends RefCounted
const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
# Waits for a specified signal in an interval of 50ms sent from the <source>, and terminates with an error after the specified timeout has elapsed.
# source: the object from which the signal is emitted
@ -14,16 +12,19 @@ func await_signal_on(source :Object, signal_name :String, args :Array = [], time
var assert_that := GdUnitAssertImpl.new(signal_name)
var line_number := GdUnitAssertions.get_line_number()
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await Engine.get_main_loop().process_frame
return await (Engine.get_main_loop() as SceneTree).process_frame
# fail fast if the given source instance invalid
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await await_idle_frame()
var awaiter := GdUnitSignalAwaiter.new(timeout_millis)
var value :Variant = await awaiter.on_signal(source, signal_name, args)
if awaiter.is_interrupted():
var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
@warning_ignore("return_value_discarded")
assert_that.report_error(failure, line_number)
return value
@ -37,6 +38,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array =
var line_number := GdUnitAssertions.get_line_number()
# fail fast if the given source instance invalid
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
GdUnitAssertImpl.new(signal_name)\
.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await await_idle_frame()
@ -44,6 +46,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array =
var value :Variant = await awaiter.on_signal(source, signal_name, args)
if awaiter.is_interrupted():
var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
@warning_ignore("return_value_discarded")
GdUnitAssertImpl.new(signal_name).report_error(failure, line_number)
return value
@ -56,7 +59,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array =
func await_millis(milliSec :int) -> void:
var timer :Timer = Timer.new()
timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id())
Engine.get_main_loop().root.add_child(timer)
(Engine.get_main_loop() as SceneTree).root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
timer.start(milliSec / 1000.0)
@ -66,4 +69,4 @@ func await_millis(milliSec :int) -> void:
# Waits until the next idle frame
func await_idle_frame() -> void:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame

View File

@ -29,3 +29,9 @@ func has_message(expected: String) -> GdUnitFailureAssert:
@warning_ignore("unused_parameter")
func starts_with_message(expected: String) -> GdUnitFailureAssert:
return self
## Verifies that the failure message contains the expected message.
@warning_ignore("unused_parameter")
func contains_message(expected: String) -> GdUnitFailureAssert:
return self

View File

@ -3,15 +3,15 @@ class_name GdUnitFloatAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
## Verifies that the current String is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :float) -> GdUnitFloatAssert:
func is_equal(expected :Variant) -> GdUnitFloatAssert:
return self
## Verifies that the current value is not equal to expected one.
## Verifies that the current String is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :float) -> GdUnitFloatAssert:
func is_not_equal(expected :Variant) -> GdUnitFloatAssert:
return self

View File

@ -5,39 +5,39 @@ extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that the current value is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that the current value is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that the current value is true.
func is_true() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that the current value is false.
func is_false() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self

View File

@ -9,7 +9,7 @@ extends GdUnitAssert
## await assert_error(<callable>).is_success()
## [/codeblock]
func is_success() -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -20,7 +20,7 @@ func is_success() -> GdUnitGodotErrorAssert:
## [/codeblock]
@warning_ignore("unused_parameter")
func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -31,7 +31,7 @@ func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert:
## [/codeblock]
@warning_ignore("unused_parameter")
func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -42,5 +42,5 @@ func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert:
## [/codeblock]
@warning_ignore("unused_parameter")
func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self

View File

@ -2,16 +2,15 @@
class_name GdUnitIntAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
## Verifies that the current String is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :int) -> GdUnitIntAssert:
func is_equal(expected :Variant) -> GdUnitIntAssert:
return self
## Verifies that the current value is not equal to expected one.
## Verifies that the current String is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :int) -> GdUnitIntAssert:
func is_not_equal(expected :Variant) -> GdUnitIntAssert:
return self

View File

@ -1,17 +1,81 @@
## The scene runner for GdUnit to simmulate scene interactions
## The Scene Runner is a tool used for simulating interactions on a scene.
## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames.
## This tool is typically used for integration testing a scene.
class_name GdUnitSceneRunner
extends RefCounted
const NO_ARG = GdUnitConstants.NO_ARG
## Sets the mouse cursor to given position relative to the viewport.
## Simulates that an action has been pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
func simulate_action_pressed(action: String) -> GdUnitSceneRunner:
return self
## Gets the current mouse position of the current viewport
## Simulates that an action is pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func simulate_action_press(action: String) -> GdUnitSceneRunner:
return self
## Simulates that an action has been released.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func simulate_action_release(action: String) -> GdUnitSceneRunner:
return self
## Simulates that a key has been pressed.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
## [codeblock]
## func test_key_presssed():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_key_pressed(KEY_SPACE)
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Simulates that a key is pressed.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
@warning_ignore("unused_parameter")
func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
return self
## Simulates that a key has been released.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
@warning_ignore("unused_parameter")
func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
return self
## Sets the mouse cursor to given position relative to the viewport.
## @deprecated: Use [set_mouse_position] instead.
@warning_ignore("unused_parameter")
func set_mouse_pos(position: Vector2) -> GdUnitSceneRunner:
return self
## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br]
## [member position] : The absolute position in pixels as Vector2
@warning_ignore("unused_parameter")
func set_mouse_position(position: Vector2) -> GdUnitSceneRunner:
return self
## Returns the mouse's position in this Viewport using the coordinate system of this Viewport.
func get_mouse_position() -> Vector2:
return Vector2.ZERO
@ -21,58 +85,10 @@ func get_global_mouse_position() -> Vector2:
return Vector2.ZERO
## Simulates that an action has been pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func simulate_action_pressed(action :String) -> GdUnitSceneRunner:
return self
## Simulates that an action is pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func simulate_action_press(action :String) -> GdUnitSceneRunner:
return self
## Simulates that an action has been released.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
@warning_ignore("unused_parameter")
func simulate_action_release(action :String) -> GdUnitSceneRunner:
return self
## Simulates that a key has been pressed.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
@warning_ignore("unused_parameter")
func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
return self
## Simulates that a key is pressed.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
@warning_ignore("unused_parameter")
func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
return self
## Simulates that a key has been released.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
@warning_ignore("unused_parameter")
func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
return self
## Simulates a mouse moved to final position.[br]
## [member pos] : The final mouse position
## [member position] : The final mouse position
@warning_ignore("unused_parameter")
func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner:
return self
@ -89,7 +105,7 @@ func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -106,36 +122,149 @@ func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_ty
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Simulates a mouse button pressed.[br]
## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member double_click] : Set to true to simulate a double-click
@warning_ignore("unused_parameter")
func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
return self
## Simulates a mouse button press (holding)[br]
## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member double_click] : Set to true to simulate a double-click
@warning_ignore("unused_parameter")
func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
return self
## Simulates a mouse button released.[br]
## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
@warning_ignore("unused_parameter")
func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner:
func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner:
return self
## Simulates a screen touch is pressed.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The position to touch the screen.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@warning_ignore("unused_parameter")
func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
return self
## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The position to touch the screen.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@warning_ignore("unused_parameter")
func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
return self
## Simulates a screen touch is released.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@warning_ignore("unused_parameter")
func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner:
return self
## Simulates a touch drag and drop event to a relative position.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member relative] : The relative position, indicating the drag&drop position offset.[br]
## [member time] : The time to move to the relative position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
## # and drop it at final at 150,50 relative (50,50 + 100,0)
## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0))
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Simulates a touch screen drop to the absolute coordinates (offset).[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The final position, indicating the drop position.[br]
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
## # and drop it at 100,50
## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50))
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Simulates a complete drag and drop event from one position to another.[br]
## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The drag start position, indicating the drag position.[br]
## [member drop_position] : The drop position, indicating the drop position.[br]
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50 and drop it at 100,50
## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50))
## [/codeblock]
@warning_ignore("unused_parameter")
func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Simulates a touch screen drag event to given position.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The drag start position, indicating the drag position.[br]
@warning_ignore("unused_parameter")
func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner:
return self
## Returns the actual position of the touchscreen drag position by given index.
## [member index] : The touch index in the case of a multi-touch event.[br]
@warning_ignore("unused_parameter")
func get_screen_touch_drag_position(index: int) -> Vector2:
return Vector2.ZERO
## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br]
## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life,
## whilst a value of 0.5 means the game moves at half the regular speed.
## Sets the time factor for the scene simulation.
## [member time_factor] : A float representing the simulation speed.[br]
## - Default is 1.0, meaning the simulation runs at normal speed.[br]
## - A value of 2.0 means the simulation runs twice as fast as real time.[br]
## - A value of 0.5 means the simulation runs at half the regular speed.[br]
@warning_ignore("unused_parameter")
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner:
return self
@ -143,8 +272,8 @@ func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
## [member frames] : amount of frames to process[br]
## [member delta_milli] : the time delta between a frame in milliseconds
@warning_ignore("unused_parameter")
func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
await Engine.get_main_loop().process_frame
func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -153,18 +282,18 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
## [member args] : optional signal arguments to be matched for stop[br]
@warning_ignore("unused_parameter")
func simulate_until_signal(
signal_name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
await Engine.get_main_loop().process_frame
signal_name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
@ -174,64 +303,103 @@ func simulate_until_signal(
## [member args] : optional signal arguments to be matched for stop
@warning_ignore("unused_parameter")
func simulate_until_object_signal(
source :Object,
signal_name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
await Engine.get_main_loop().process_frame
source: Object,
signal_name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> GdUnitSceneRunner:
await (Engine.get_main_loop() as SceneTree).process_frame
return self
### Waits for all input events are processed
## Waits for all input events to be processed by flushing any buffered input events
## and then awaiting a full cycle of both the process and physics frames.[br]
## [br]
## This is typically used to ensure that any simulated or queued inputs are fully
## processed before proceeding with the next steps in the scene.[br]
## It's essential for reliable input simulation or when synchronizing logic based
## on inputs.[br]
##
## Usage Example:
## [codeblock]
## await await_input_processed() # Ensure all inputs are processed before continuing
## [/codeblock]
func await_input_processed() -> void:
await Engine.get_main_loop().process_frame
await Engine.get_main_loop().physics_frame
if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED:
Input.flush_buffered_events()
await (Engine.get_main_loop() as SceneTree).process_frame
await (Engine.get_main_loop() as SceneTree).physics_frame
## Waits for the function return value until specified timeout or fails.[br]
## [member args] : optional function arguments
## The await_func function pauses execution until a specified function in the scene returns a value.[br]
## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br]
## [member func_name] : The name of the function to wait for.[br]
## [member args] : Optional function arguments
## [br]
## Usage Example:
## [codeblock]
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
## await_func("calculate_score").is_equal(100)
## [/codeblock]
@warning_ignore("unused_parameter")
func await_func(func_name :String, args := []) -> GdUnitFuncAssert:
func await_func(func_name: String, args := []) -> GdUnitFuncAssert:
return null
## Waits for the function return value of specified source until specified timeout or fails.[br]
## [member source : the object where implements the function[br]
## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br]
## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br]
## [member source] : The object where implements the function.[br]
## [member func_name] : The name of the function to wait for.[br]
## [member args] : optional function arguments
## [br]
## Usage Example:
## [codeblock]
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
## var my_instance := ScoreCalculator.new()
## await_func(my_instance, "calculate_score").is_equal(100)
## [/codeblock]
@warning_ignore("unused_parameter")
func await_func_on(source :Object, func_name :String, args := []) -> GdUnitFuncAssert:
func await_func_on(source: Object, func_name: String, args := []) -> GdUnitFuncAssert:
return null
## Waits for given signal is emited by the scene until a specified timeout to fail.[br]
## [member signal_name] : signal name[br]
## [member args] : the expected signal arguments as an array[br]
## [member timeout] : the timeout in ms, default is set to 2000ms
## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br]
## [member signal_name] : The name of the signal to wait for[br]
## [member args] : The signal arguments as an array[br]
## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing
@warning_ignore("unused_parameter")
func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void:
await Engine.get_main_loop().process_frame
func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void:
await (Engine.get_main_loop() as SceneTree).process_frame
pass
## Waits for given signal is emited by the <source> until a specified timeout to fail.[br]
## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br]
## [member source] : the object from which the signal is emitted[br]
## [member signal_name] : signal name[br]
## [member args] : the expected signal arguments as an array[br]
## [member timeout] : the timeout in ms, default is set to 2000ms
## [member signal_name] : The name of the signal to wait for[br]
## [member args] : The signal arguments as an array[br]
## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing
@warning_ignore("unused_parameter")
func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void:
func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void:
pass
## maximizes the window to bring the scene visible
## Restores the scene window to a windowed mode and brings it to the foreground.[br]
## This ensures that the scene is visible and active during testing, making it easier to observe and interact with.
func move_window_to_foreground() -> GdUnitSceneRunner:
return self
## Restores the scene window to a windowed mode and brings it to the foreground.[br]
## This ensures that the scene is visible and active during testing, making it easier to observe and interact with.
## @deprecated: Use [move_window_to_foreground] instead.
func maximize_view() -> GdUnitSceneRunner:
return self
@ -240,7 +408,7 @@ func maximize_view() -> GdUnitSceneRunner:
## [member name] : name of property[br]
## [member return] : the value of the property
@warning_ignore("unused_parameter")
func get_property(name :String) -> Variant:
func get_property(name: String) -> Variant:
return null
## Set the value <value> of the property with the name <name>.[br]
@ -248,7 +416,7 @@ func get_property(name :String) -> Variant:
## [member value] : value of property[br]
## [member return] : true|false depending on valid property name.
@warning_ignore("unused_parameter")
func set_property(name :String, value :Variant) -> bool:
func set_property(name: String, value: Variant) -> bool:
return false
@ -258,17 +426,17 @@ func set_property(name :String, value :Variant) -> bool:
## [member return] : the function result
@warning_ignore("unused_parameter")
func invoke(
name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> Variant:
name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> Variant:
return null
@ -277,7 +445,7 @@ func invoke(
## [member recursive] : enables/disables seraching recursive[br]
## [member return] : the node if find otherwise null
@warning_ignore("unused_parameter")
func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node:
func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node:
return null

View File

@ -6,14 +6,14 @@ extends GdUnitAssert
## Verifies that given signal is emitted until waiting time
@warning_ignore("unused_parameter")
func is_emitted(name :String, args := []) -> GdUnitSignalAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self
## Verifies that given signal is NOT emitted until waiting time
@warning_ignore("unused_parameter")
func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return self

View File

@ -10,7 +10,7 @@
## [/codeblock]
## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/
@icon("res://addons/gdUnit4/src/ui/assets/TestSuite.svg")
@icon("res://addons/gdUnit4/src/ui/settings/logo.png")
class_name GdUnitTestSuite
extends Node
@ -23,8 +23,6 @@ var __is_skipped := false
var __skip_reason :String = "Unknow."
var __active_test_case :String
var __awaiter := __gdunit_awaiter()
# holds the actual execution context
var __execution_context :RefCounted
### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
@ -100,25 +98,25 @@ func error_as_string(error_number :int) -> String:
## A litle helper to auto freeing your created objects after test execution
func auto_free(obj :Variant) -> Variant:
if __execution_context != null:
return __execution_context.register_auto_free(obj)
else:
if is_instance_valid(obj):
obj.queue_free()
return obj
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
assert(execution_context != null, "INTERNAL ERROR: The current execution_context is null! Please report this as bug.")
return execution_context.register_auto_free(obj)
@warning_ignore("native_method_override")
func add_child(node :Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void:
super.add_child(node, force_readable_name, internal)
if __execution_context != null:
__execution_context.orphan_monitor_start()
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
if execution_context != null:
execution_context.orphan_monitor_start()
## Discard the error message triggered by a timeout (interruption).[br]
## By default, an interrupted test is reported as an error.[br]
## This function allows you to change the message to Success when an interrupted error is reported.
func discard_error_interupted_by_timeout() -> void:
@warning_ignore("unsafe_method_access")
__gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case)
@ -126,12 +124,14 @@ func discard_error_interupted_by_timeout() -> void:
## Useful for storing data during test execution. [br]
## The directory is automatically deleted after test suite execution
func create_temp_dir(relative_path :String) -> String:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().create_temp_dir(relative_path)
## Deletes the temporary base directory[br]
## Is called automatically after each execution of the test suite
func clean_temp_dir() -> void:
@warning_ignore("unsafe_method_access")
__gdunit_file_access().clear_tmp()
@ -139,28 +139,26 @@ func clean_temp_dir() -> void:
## with given name <file_name> and given file <mode> (default = File.WRITE)[br]
## If success the returned File is automatically closed after the execution of the test suite
func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().create_temp_file(relative_path, file_name, mode)
## Reads a resource by given path <resource_path> into a PackedStringArray.
func resource_as_array(resource_path :String) -> PackedStringArray:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().resource_as_array(resource_path)
## Reads a resource by given path <resource_path> and returned the content as String.
func resource_as_string(resource_path :String) -> String:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().resource_as_string(resource_path)
## Reads a resource by given path <resource_path> and return Variand translated by str_to_var
func resource_as_var(resource_path :String) -> Variant:
return str_to_var(__gdunit_file_access().resource_as_string(resource_path))
## clears the debuger error list[br]
## PROTOTYPE!!!! Don't use it for now
func clear_push_errors() -> void:
__gdunit_tools().clear_push_errors()
@warning_ignore("unsafe_method_access", "unsafe_cast")
return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String)
## Waits for given signal is emited by the <source> until a specified timeout to fail[br]
@ -169,11 +167,13 @@ func clear_push_errors() -> void:
## args: the expected signal arguments as an array[br]
## timeout: the timeout in ms, default is set to 2000ms
func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> Variant:
@warning_ignore("unsafe_method_access")
return await __awaiter.await_signal_on(source, signal_name, args, timeout)
## Waits until the next idle frame
func await_idle_frame() -> void:
@warning_ignore("unsafe_method_access")
await __awaiter.await_idle_frame()
@ -185,6 +185,7 @@ func await_idle_frame() -> void:
## [/codeblock][br]
## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
func await_millis(timeout :int) -> void:
@warning_ignore("unsafe_method_access")
await __awaiter.await_millis(timeout)
@ -216,11 +217,13 @@ const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB
## Creates a mock for given class name
func mock(clazz :Variant, mock_mode := RETURN_DEFAULTS) -> Variant:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode)
## Creates a spy checked given object instance
func spy(instance :Variant) -> Variant:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance)
@ -236,21 +239,25 @@ func do_return(value :Variant) -> GdUnitMock:
## Verifies certain behavior happened at least once or exact number of times
func verify(obj :Variant, times := 1) -> Variant:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify(obj, times)
## Verifies no interactions is happen checked this mock or spy
func verify_no_interactions(obj :Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify_no_interactions(obj)
## Verifies the given mock or spy has any unverified interaction.
func verify_no_more_interactions(obj :Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify_no_more_interactions(obj)
## Resets the saved function call counters checked a mock or spy
func reset(obj :Variant) -> void:
@warning_ignore("unsafe_method_access")
__gdunit_object_interactions().reset(obj)
@ -267,6 +274,7 @@ func reset(obj :Variant) -> void:
## await assert_signal(emitter).is_emitted('my_signal')
## [/codeblock]
func monitor_signals(source :Object, _auto_free := true) -> Object:
@warning_ignore("unsafe_method_access")
__lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\
.get_current_context()\
.get_signal_collector()\
@ -277,36 +285,43 @@ func monitor_signals(source :Object, _auto_free := true) -> Object:
# === Argument matchers ========================================================
## Argument matcher to match any argument
func any() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().any()
## Argument matcher to match any boolean value
func any_bool() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_BOOL)
## Argument matcher to match any integer value
func any_int() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_INT)
## Argument matcher to match any float value
func any_float() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_FLOAT)
## Argument matcher to match any string value
func any_string() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_STRING)
## Argument matcher to match any Color value
func any_color() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_COLOR)
## Argument matcher to match any Vector typed value
func any_vector() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_types([
TYPE_VECTOR2,
TYPE_VECTOR2I,
@ -319,141 +334,169 @@ func any_vector() -> GdUnitArgumentMatcher:
## Argument matcher to match any Vector2 value
func any_vector2() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2)
## Argument matcher to match any Vector2i value
func any_vector2i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I)
## Argument matcher to match any Vector3 value
func any_vector3() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3)
## Argument matcher to match any Vector3i value
func any_vector3i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I)
## Argument matcher to match any Vector4 value
func any_vector4() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4)
## Argument matcher to match any Vector3i value
func any_vector4i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I)
## Argument matcher to match any Rect2 value
func any_rect2() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_RECT2)
## Argument matcher to match any Plane value
func any_plane() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PLANE)
## Argument matcher to match any Quaternion value
func any_quat() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_QUATERNION)
## Argument matcher to match any AABB value
func any_aabb() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_AABB)
## Argument matcher to match any Basis value
func any_basis() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_BASIS)
## Argument matcher to match any Transform2D value
func any_transform_2d() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D)
## Argument matcher to match any Transform3D value
func any_transform_3d() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D)
## Argument matcher to match any NodePath value
func any_node_path() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH)
## Argument matcher to match any RID value
func any_rid() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_RID)
## Argument matcher to match any Object value
func any_object() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_OBJECT)
## Argument matcher to match any Dictionary value
func any_dictionary() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY)
## Argument matcher to match any Array value
func any_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_ARRAY)
## Argument matcher to match any PackedByteArray value
func any_packed_byte_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY)
## Argument matcher to match any PackedInt32Array value
func any_packed_int32_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY)
## Argument matcher to match any PackedInt64Array value
func any_packed_int64_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY)
## Argument matcher to match any PackedFloat32Array value
func any_packed_float32_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY)
## Argument matcher to match any PackedFloat64Array value
func any_packed_float64_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY)
## Argument matcher to match any PackedStringArray value
func any_packed_string_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY)
## Argument matcher to match any PackedVector2Array value
func any_packed_vector2_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY)
## Argument matcher to match any PackedVector3Array value
func any_packed_vector3_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY)
## Argument matcher to match any PackedColorArray value
func any_packed_color_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY)
## Argument matcher to match any instance of given class
func any_class(clazz :Object) -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().any_class(clazz)
@ -492,13 +535,13 @@ func assert_that(current :Variant) -> GdUnitAssert:
TYPE_STRING:
return assert_str(current)
TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I:
return assert_vector(current)
return assert_vector(current, false)
TYPE_DICTIONARY:
return assert_dict(current)
TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\
TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY:
return assert_array(current)
return assert_array(current, false)
TYPE_OBJECT, TYPE_NIL:
return assert_object(current)
_:
@ -531,13 +574,13 @@ func assert_float(current :Variant) -> GdUnitFloatAssert:
## [codeblock]
## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001))
## [/codeblock]
func assert_vector(current :Variant) -> GdUnitVectorAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current)
func assert_vector(current :Variant, type_check := true) -> GdUnitVectorAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current, type_check)
## An assertion tool to verify arrays.
func assert_array(current :Variant) -> GdUnitArrayAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current)
func assert_array(current :Variant, type_check := true) -> GdUnitArrayAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current, type_check)
## An assertion tool to verify dictionaries.
@ -577,6 +620,7 @@ func assert_signal(instance :Object) -> GdUnitSignalAssert:
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure(assertion :Callable) -> GdUnitFailureAssert:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion)
@ -588,6 +632,7 @@ func assert_failure(assertion :Callable) -> GdUnitFailureAssert:
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert:
@warning_ignore("unsafe_method_access")
return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion)
@ -608,10 +653,12 @@ func assert_error(current :Callable) -> GdUnitGodotErrorAssert:
func assert_not_yet_implemented() -> void:
__gdunit_assert().new(null).test_fail()
@warning_ignore("unsafe_method_access")
__gdunit_assert().new(null).do_fail()
func fail(message :String) -> void:
@warning_ignore("unsafe_method_access")
__gdunit_assert().new(null).report_error(message)

View File

@ -8,8 +8,12 @@ const SUB_COLOR := Color(1, 0, 0, .3)
const ADD_COLOR := Color(0, 1, 0, .3)
static func format_dict(value :Dictionary) -> String:
if value.is_empty():
static func format_dict(value :Variant) -> String:
if not value is Dictionary:
return str(value)
var dict_value: Dictionary = value
if dict_value.is_empty():
return "{ }"
var as_rows := var_to_str(value).split("\n")
for index in range( 1, as_rows.size()-1):
@ -22,15 +26,22 @@ static func format_dict(value :Dictionary) -> String:
static func input_event_as_text(event :InputEvent) -> String:
var text := ""
if event is InputEventKey:
var key_event := event as InputEventKey
text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [
event.as_text(), event.pressed, event.keycode, event.physical_keycode]
event.as_text(), key_event.pressed, key_event.keycode, key_event.physical_keycode]
else:
text += event.as_text()
if event is InputEventMouse:
text += ", global_position %s" % event.global_position
var mouse_event := event as InputEventMouse
text += ", global_position %s" % mouse_event.global_position
if event is InputEventWithModifiers:
var mouse_event := event as InputEventWithModifiers
text += ", shift=%s, alt=%s, control=%s, meta=%s, command=%s" % [
event.shift_pressed, event.alt_pressed, event.ctrl_pressed, event.meta_pressed, event.command_or_control_autoremap]
mouse_event.shift_pressed,
mouse_event.alt_pressed,
mouse_event.ctrl_pressed,
mouse_event.meta_pressed,
mouse_event.command_or_control_autoremap]
return text
@ -51,9 +62,11 @@ static func colored_array_div(characters :PackedByteArray) -> String:
match character:
GdDiffTool.DIV_ADD:
index += 1
@warning_ignore("return_value_discarded")
additional_chars.append(characters[index])
GdDiffTool.DIV_SUB:
index += 1
@warning_ignore("return_value_discarded")
missing_chars.append(characters[index])
_:
if not missing_chars.is_empty():
@ -62,6 +75,7 @@ static func colored_array_div(characters :PackedByteArray) -> String:
if not additional_chars.is_empty():
result.append_array(format_chars(additional_chars, ADD_COLOR))
additional_chars = PackedByteArray()
@warning_ignore("return_value_discarded")
result.append(character)
index += 1
@ -95,7 +109,7 @@ static func _nerror(number :Variant) -> String:
static func _colored_value(value :Variant) -> String:
match typeof(value):
TYPE_STRING, TYPE_STRING_NAME:
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(value)]
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(str(value))]
TYPE_INT:
return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value]
TYPE_FLOAT:
@ -106,10 +120,12 @@ static func _colored_value(value :Variant) -> String:
if value == null:
return "'[color=%s]<null>[/color]'" % [VALUE_COLOR]
if value is InputEvent:
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(value)]
if value.has_method("_to_string"):
var ie: InputEvent = value
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(ie)]
var obj_value: Object = value
if obj_value.has_method("_to_string"):
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)]
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()]
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, obj_value.get_class()]
TYPE_DICTIONARY:
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)]
_:
@ -163,10 +179,10 @@ static func test_timeout(timeout :int) -> String:
static func test_suite_skipped(hint :String, skip_count :int) -> String:
return """
%s
Tests skipped: %s
Skipped %s tests
Reason: %s
""".dedent().trim_prefix("\n")\
% [_error("Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)]
% [_error("The Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)]
static func test_skipped(hint :String) -> String:
@ -336,6 +352,7 @@ static func error_ends_with(current :Variant, expected :Variant) -> String:
static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String:
@warning_ignore("unsafe_method_access")
var current_length :Variant = current.length() if current != null else null
match compare_operator:
Comparator.EQUAL:
@ -361,48 +378,50 @@ static func error_has_length(current :Variant, expected: int, compare_operator :
# - ArrayAssert specific messgaes ---------------------------------------------------
static func error_arr_contains(current :Variant, expected :Array, not_expect :Array, not_found :Array, by_reference :bool) -> String:
static func error_arr_contains(current: Variant, expected: Variant, not_expect: Variant, not_found: Variant, by_reference: bool) -> String:
var failure_message := "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:"
var error := "%s\n %s\n do contains (in any order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not not_expect.is_empty():
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not not_found.is_empty():
var prefix := "but" if not_expect.is_empty() else "and"
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_contains_exactly(
current :Variant,
expected :Variant,
not_expect :Variant,
not_found :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String:
current: Variant,
expected: Variant,
not_expect: Variant,
not_found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := (
"Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME exactly elements:"
)
if not_expect.is_empty() and not_found.is_empty():
var diff := _find_first_diff(current, expected)
if is_empty(not_expect) and is_empty(not_found):
var arr_current: Array = current
var arr_expected: Array = expected
var diff := _find_first_diff(arr_current, arr_expected)
return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected), diff]
var error := "%s\n %s\n do contains (in same order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not not_expect.is_empty():
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not not_found.is_empty():
var prefix := "but" if not_expect.is_empty() else "and"
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_contains_exactly_in_any_order(
current :Variant,
expected :Array,
not_expect :Array,
not_found :Array,
compare_mode :GdObjects.COMPARE_MODE) -> String:
current: Variant,
expected: Variant,
not_expect: Variant,
not_found: Variant,
compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := (
"Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
@ -410,19 +429,19 @@ static func error_arr_contains_exactly_in_any_order(
)
var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not not_expect.is_empty():
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not not_found.is_empty():
var prefix := "but" if not_expect.is_empty() else "and"
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_not_contains(current :Array, expected :Array, found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
static func error_arr_not_contains(current: Variant, expected: Variant, found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:"
var error := "%s\n %s\n do not contains\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not found.is_empty():
if not is_empty(found):
error += "\n but found elements:\n %s" % _colored_value(found)
return error
@ -542,20 +561,25 @@ static func result_message(result :GdUnitResult) -> String:
# - Spy|Mock specific errors ----------------------------------------------------
static func error_no_more_interactions(summary :Dictionary) -> String:
var interactions := PackedStringArray()
for args :Variant in summary.keys():
for args :Array in summary.keys():
var times :int = summary[args]
@warning_ignore("return_value_discarded")
interactions.append(_format_arguments(args, times))
return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)]
static func error_validate_interactions(current_interactions :Dictionary, expected_interactions :Dictionary) -> String:
var interactions := PackedStringArray()
for args :Variant in current_interactions.keys():
var times :int = current_interactions[args]
interactions.append(_format_arguments(args, times))
var expected_interaction := _format_arguments(expected_interactions.keys()[0], expected_interactions.values()[0])
static func error_validate_interactions(current_interactions: Dictionary, expected_interactions: Dictionary) -> String:
var collected_interactions := PackedStringArray()
for args: Array in current_interactions.keys():
var times: int = current_interactions[args]
@warning_ignore("return_value_discarded")
collected_interactions.append(_format_arguments(args, times))
var arguments: Array = expected_interactions.keys()[0]
var interactions: int = expected_interactions.values()[0]
var expected_interaction := _format_arguments(arguments, interactions)
return "%s\n%s\n%s\n%s" % [
_error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(interactions)]
_error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(collected_interactions)]
static func _format_arguments(args :Array, times :int) -> String:
@ -569,13 +593,15 @@ static func _format_arguments(args :Array, times :int) -> String:
static func _to_typed_args(args :Array) -> PackedStringArray:
var typed := PackedStringArray()
for arg :Variant in args:
@warning_ignore("return_value_discarded")
typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg)))
return typed
static func _format_arg(arg :Variant) -> String:
if arg is InputEvent:
return input_event_as_text(arg)
var ie: InputEvent = arg
return input_event_as_text(ie)
return str(arg)
@ -589,6 +615,7 @@ static func _find_first_diff(left :Array, right :Array) -> String:
static func error_has_size(current :Variant, expected: int) -> String:
@warning_ignore("unsafe_method_access")
var current_size :Variant = null if current == null else current.size()
return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)]
@ -613,3 +640,18 @@ static func format_invalid(value :String) -> String:
static func humanized(value :String) -> String:
return value.replace("_", " ")
static func build_failure_message(failure :String, additional_failure_message: String, custom_failure_message: String) -> String:
var message := failure if custom_failure_message.is_empty() else custom_failure_message
if additional_failure_message.is_empty():
return message
return """
%s
[color=LIME_GREEN][b]Additional info:[/b][/color]
%s""".dedent().trim_prefix("\n") % [message, additional_failure_message]
static func is_empty(value: Variant) -> bool:
var arry_value: Array = value
return arry_value != null and arry_value.is_empty()

View File

@ -51,5 +51,4 @@ static func current_failure() -> String:
static func send_report(report :GdUnitReport) -> void:
var execution_context_id := GdUnitThreadManager.get_current_context().get_execution_context_id()
GdUnitSignals.instance().gdunit_report.emit(execution_context_id, report)
GdUnitThreadManager.get_current_context().get_execution_context().add_report(report)

View File

@ -1,21 +1,24 @@
class_name GdUnitArrayAssertImpl
extends GdUnitArrayAssert
var _base :GdUnitAssert
var _current_value_provider :ValueProvider
var _base: GdUnitAssertImpl
var _current_value_provider: ValueProvider
var _type_check: bool
func _init(current :Variant) -> void:
func _init(current: Variant, type_check := true) -> void:
_type_check = type_check
_current_value_provider = DefaultValueProvider.new(current)
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not _validate_value_type(current):
@warning_ignore("return_value_discarded")
report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
func _notification(event: int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
@ -27,21 +30,26 @@ func report_success() -> GdUnitArrayAssert:
return self
func report_error(error :String) -> GdUnitArrayAssert:
func report_error(error: String) -> GdUnitArrayAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitArrayAssert:
func override_failure_message(message: String) -> GdUnitArrayAssert:
_base.override_failure_message(message)
return self
func _validate_value_type(value :Variant) -> bool:
func append_failure_message(message: String) -> GdUnitArrayAssert:
_base.append_failure_message(message)
return self
func _validate_value_type(value: Variant) -> bool:
return value == null or GdArrayTools.is_array_type(value)
@ -49,15 +57,23 @@ func get_current_value() -> Variant:
return _current_value_provider.get_value()
func max_length(left :Variant, right :Variant) -> int:
func max_length(left: Variant, right: Variant) -> int:
var ls := str(left).length()
var rs := str(right).length()
return rs if ls < rs else ls
func _array_equals_div(current :Array, expected :Array, case_sensitive :bool = false) -> Array:
var current_value := PackedStringArray(current)
var expected_value := PackedStringArray(expected)
# gdlint: disable=function-name
func _toPackedStringArray(value: Variant) -> PackedStringArray:
if GdArrayTools.is_array_type(value):
@warning_ignore("unsafe_cast")
return PackedStringArray(value as Array)
return PackedStringArray([str(value)])
func _array_equals_div(current: Variant, expected: Variant, case_sensitive: bool = false) -> Array[Array]:
var current_value := _toPackedStringArray(current)
var expected_value := _toPackedStringArray(expected)
var index_report := Array()
for index in current_value.size():
var c := current_value[index]
@ -67,25 +83,25 @@ func _array_equals_div(current :Array, expected :Array, case_sensitive :bool = f
var length := max_length(c, e)
current_value[index] = GdAssertMessages.format_invalid(c.lpad(length))
expected_value[index] = e.lpad(length)
index_report.push_back({"index" : index, "current" :c, "expected": e})
index_report.push_back({"index": index, "current": c, "expected": e})
else:
current_value[index] = GdAssertMessages.format_invalid(c)
index_report.push_back({"index" : index, "current" :c, "expected": "<N/A>"})
index_report.push_back({"index": index, "current": c, "expected": "<N/A>"})
for index in range(current.size(), expected_value.size()):
for index in range(current_value.size(), expected_value.size()):
var value := expected_value[index]
expected_value[index] = GdAssertMessages.format_invalid(value)
index_report.push_back({"index" : index, "current" : "<N/A>", "expected": value})
index_report.push_back({"index": index, "current": "<N/A>", "expected": value})
return [current_value, expected_value, index_report]
func _array_div(compare_mode :GdObjects.COMPARE_MODE, left :Array[Variant], right :Array[Variant], _same_order := false) -> Array[Variant]:
func _array_div(compare_mode: GdObjects.COMPARE_MODE, left: Array[Variant], right: Array[Variant], _same_order := false) -> Array[Variant]:
var not_expect := left.duplicate(true)
var not_found := right.duplicate(true)
for index_c in left.size():
var c :Variant = left[index_c]
var c: Variant = left[index_c]
for index_e in right.size():
var e :Variant = right[index_e]
var e: Variant = right[index_e]
if GdObjects.equals(c, e, false, compare_mode):
GdArrayTools.erase_value(not_expect, e)
GdArrayTools.erase_value(not_found, c)
@ -93,241 +109,262 @@ func _array_div(compare_mode :GdObjects.COMPARE_MODE, left :Array[Variant], righ
return [not_expect, not_found]
func _contains(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
func _contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], expected, by_reference))
var diffs := _array_div(compare_mode, current_value, expected)
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant])
#var not_expect := diffs[0] as Array
var not_found := diffs[1] as Array
var not_found: Array = diffs[1]
if not not_found.is_empty():
return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], not_found, by_reference))
return report_success()
func _contains_exactly(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
func _contains_exactly(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], expected, compare_mode))
return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected, [], expected, compare_mode))
# has same content in same order
if GdObjects.equals(Array(current_value), Array(expected), false, compare_mode):
if _is_equal(current_value, expected, false, compare_mode):
return report_success()
# check has same elements but in different order
if GdObjects.equals_sorted(Array(current_value), Array(expected), false, compare_mode):
if _is_equals_sorted(current_value, expected, false, compare_mode):
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], [], compare_mode))
# find the difference
var diffs := _array_div(compare_mode, current_value, expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
var not_expect := diffs[0] as Array[Variant]
var not_found := diffs[1] as Array[Variant]
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode,
current_value as Array[Variant],
expected as Array[Variant],
GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
var not_expect: Array[Variant] = diffs[0]
var not_found: Array[Variant] = diffs[1]
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, not_expect, not_found, compare_mode))
func _contains_exactly_in_any_order(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
func _contains_exactly_in_any_order(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode))
# find the difference
var diffs := _array_div(compare_mode, current_value, expected, false)
var not_expect := diffs[0] as Array
var not_found := diffs[1] as Array
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant], false)
var not_expect: Array[Variant] = diffs[0]
var not_found: Array[Variant] = diffs[1]
if not_expect.is_empty() and not_found.is_empty():
return report_success()
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, not_expect, not_found, compare_mode))
func _not_contains(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
func _not_contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode))
var diffs := _array_div(compare_mode, current_value, expected)
var found := diffs[0] as Array
if found.size() == current_value.size():
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant])
var found: Array[Variant] = diffs[0]
@warning_ignore("unsafe_cast")
if found.size() == (current_value as Array).size():
return report_success()
var diffs2 := _array_div(compare_mode, expected, diffs[1])
@warning_ignore("unsafe_cast")
var diffs2 := _array_div(compare_mode, expected as Array[Variant], diffs[1] as Array[Variant])
return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected, diffs2[0], compare_mode))
func is_null() -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
# Verifies that the current String is equal to the given one.
func is_equal(expected :Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
func is_equal(expected: Variant) -> GdUnitArrayAssert:
if _type_check and not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null and expected != null:
return report_error(GdAssertMessages.error_equal(null, expected))
if not GdObjects.equals(current_value, expected):
if not _is_equal(current_value, expected):
var diff := _array_equals_div(current_value, expected)
var expected_as_list := GdArrayTools.as_string(diff[0], false)
var current_as_list := GdArrayTools.as_string(diff[1], false)
var index_report :Variant = diff[2]
var index_report: Array = diff[2]
return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
return report_success()
# Verifies that the current Array is equal to the given one, ignoring case considerations.
func is_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
func is_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert:
if _type_check and not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
var current_value: Variant = get_current_value()
if current_value == null and expected != null:
return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected)))
if not GdObjects.equals(current_value, expected, true):
var diff := _array_equals_div(current_value, expected, true)
@warning_ignore("unsafe_cast")
return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected as Array)))
if not _is_equal(current_value, expected, true):
@warning_ignore("unsafe_cast")
var diff := _array_equals_div(current_value as Array[Variant], expected as Array[Variant], true)
var expected_as_list := GdArrayTools.as_string(diff[0])
var current_as_list := GdArrayTools.as_string(diff[1])
var index_report :Variant = diff[2]
var index_report: Array = diff[2]
return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
return report_success()
func is_not_equal(expected :Variant) -> GdUnitArrayAssert:
func is_not_equal(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
if GdObjects.equals(current_value, expected):
var current_value: Variant = get_current_value()
if _is_equal(current_value, expected):
return report_error(GdAssertMessages.error_not_equal(current_value, expected))
return report_success()
func is_not_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert:
func is_not_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current_value :Variant = get_current_value()
if GdObjects.equals(current_value, expected, true):
var c := GdArrayTools.as_string(current_value)
var e := GdArrayTools.as_string(expected)
var current_value: Variant = get_current_value()
if _is_equal(current_value, expected, true):
@warning_ignore("unsafe_cast")
var c := GdArrayTools.as_string(current_value as Array)
@warning_ignore("unsafe_cast")
var e := GdArrayTools.as_string(expected as Array)
return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e))
return report_success()
func is_empty() -> GdUnitArrayAssert:
var current_value :Variant = get_current_value()
if current_value == null or current_value.size() > 0:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value == null or (current_value as Array).size() > 0:
return report_error(GdAssertMessages.error_is_empty(current_value))
return report_success()
func is_not_empty() -> GdUnitArrayAssert:
var current_value :Variant = get_current_value()
if current_value != null and current_value.size() == 0:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value != null and (current_value as Array).size() == 0:
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected :Variant) -> GdUnitArrayAssert:
func is_same(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current :Variant = get_current_value()
var current: Variant = get_current_value()
if not is_same(current, expected):
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_is_same(current, expected))
return self
func is_not_same(expected :Variant) -> GdUnitArrayAssert:
func is_not_same(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current :Variant = get_current_value()
var current: Variant = get_current_value()
if is_same(current, expected):
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_not_same(current, expected))
return self
func has_size(expected: int) -> GdUnitArrayAssert:
var current_value :Variant= get_current_value()
if current_value == null or current_value.size() != expected:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value == null or (current_value as Array).size() != expected:
return report_error(GdAssertMessages.error_has_size(current_value, expected))
return report_success()
func contains(expected :Variant) -> GdUnitArrayAssert:
func contains(expected: Variant) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_exactly(expected :Variant) -> GdUnitArrayAssert:
func contains_exactly(expected: Variant) -> GdUnitArrayAssert:
return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert:
func contains_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_same(expected :Variant) -> GdUnitArrayAssert:
func contains_same(expected: Variant) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func contains_same_exactly(expected :Variant) -> GdUnitArrayAssert:
func contains_same_exactly(expected: Variant) -> GdUnitArrayAssert:
return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func contains_same_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert:
func contains_same_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func not_contains(expected :Variant) -> GdUnitArrayAssert:
func not_contains(expected: Variant) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func not_contains_same(expected :Variant) -> GdUnitArrayAssert:
func not_contains_same(expected: Variant) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func is_instanceof(expected :Variant) -> GdUnitAssert:
func is_instanceof(expected: Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
_base.is_instanceof(expected)
return self
func extract(func_name :String, args := Array()) -> GdUnitArrayAssert:
func extract(func_name: String, args := Array()) -> GdUnitArrayAssert:
var extracted_elements := Array()
var extractor :GdUnitValueExtractor = ResourceLoader.load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd",
"GDScript", ResourceLoader.CACHE_MODE_REUSE).new(func_name, args)
var current :Variant = get_current_value()
var extractor := GdUnitFuncValueExtractor.new(func_name, args)
var current: Variant = get_current_value()
if current == null:
_current_value_provider = DefaultValueProvider.new(null)
else:
for element :Variant in current:
for element: Variant in current:
extracted_elements.append(extractor.extract_value(element))
_current_value_provider = DefaultValueProvider.new(extracted_elements)
return self
func extractv(
extr0 :GdUnitValueExtractor,
extr1 :GdUnitValueExtractor = null,
extr2 :GdUnitValueExtractor = null,
extr3 :GdUnitValueExtractor = null,
extr4 :GdUnitValueExtractor = null,
extr5 :GdUnitValueExtractor = null,
extr6 :GdUnitValueExtractor = null,
extr7 :GdUnitValueExtractor = null,
extr8 :GdUnitValueExtractor = null,
extr9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert:
var extractors :Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null)
extr0: GdUnitValueExtractor,
extr1: GdUnitValueExtractor = null,
extr2: GdUnitValueExtractor = null,
extr3: GdUnitValueExtractor = null,
extr4: GdUnitValueExtractor = null,
extr5: GdUnitValueExtractor = null,
extr6: GdUnitValueExtractor = null,
extr7: GdUnitValueExtractor = null,
extr8: GdUnitValueExtractor = null,
extr9: GdUnitValueExtractor = null) -> GdUnitArrayAssert:
var extractors: Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null)
var extracted_elements := Array()
var current :Variant = get_current_value()
var current: Variant = get_current_value()
if current == null:
_current_value_provider = DefaultValueProvider.new(null)
else:
for element: Variant in current:
var ev :Array[Variant] = [
var ev: Array[Variant] = [
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
@ -339,12 +376,44 @@ func extractv(
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG
]
for index :int in extractors.size():
var extractor :GdUnitValueExtractor = extractors[index]
@warning_ignore("unsafe_cast")
for index: int in (extractors as Array).size():
var extractor: GdUnitValueExtractor = extractors[index]
ev[index] = extractor.extract_value(element)
if extractors.size() > 1:
@warning_ignore("unsafe_cast")
if (extractors as Array).size() > 1:
extracted_elements.append(GdUnitTuple.new(ev[0], ev[1], ev[2], ev[3], ev[4], ev[5], ev[6], ev[7], ev[8], ev[9]))
else:
extracted_elements.append(ev[0])
_current_value_provider = DefaultValueProvider.new(extracted_elements)
return self
@warning_ignore("incompatible_ternary")
func _is_equal(
left: Variant,
right: Variant,
case_sensitive := false,
compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
@warning_ignore("unsafe_cast")
return GdObjects.equals(
(left as Array) if GdArrayTools.is_array_type(left) else left,
(right as Array) if GdArrayTools.is_array_type(right) else right,
case_sensitive,
compare_mode
)
func _is_equals_sorted(
left: Variant,
right: Variant,
case_sensitive := false,
compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
@warning_ignore("unsafe_cast")
return GdObjects.equals_sorted(
left as Array,
right as Array,
case_sensitive,
compare_mode)

View File

@ -1,9 +1,11 @@
class_name GdUnitAssertImpl
extends GdUnitAssert
var _current :Variant
var _current_error_message :String = ""
var _current_failure_message :String = ""
var _custom_failure_message :String = ""
var _additional_failure_message: String = ""
func _init(current :Variant) -> void:
@ -13,8 +15,9 @@ func _init(current :Variant) -> void:
GdAssertReports.reset_last_error_line_number()
func failure_message() -> String:
return _current_error_message
return _current_failure_message
func current_value() -> Variant:
@ -26,16 +29,16 @@ func report_success() -> GdUnitAssert:
return self
func report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert:
func report_error(failure :String, failure_line_number: int = -1) -> GdUnitAssert:
var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
GdAssertReports.set_last_error_line_number(line_number)
_current_error_message = error_message if _custom_failure_message.is_empty() else _custom_failure_message
GdAssertReports.report_error(_current_error_message, line_number)
_current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
GdAssertReports.report_error(_current_failure_message, line_number)
Engine.set_meta("GD_TEST_FAILURE", true)
return self
func test_fail() -> GdUnitAssert:
func do_fail() -> GdUnitAssert:
return report_error(GdAssertMessages.error_not_implemented())
@ -44,6 +47,11 @@ func override_failure_message(message :String) -> GdUnitAssert:
return self
func append_failure_message(message :String) -> GdUnitAssert:
_additional_failure_message = message
return self
func is_equal(expected :Variant) -> GdUnitAssert:
var current :Variant = current_value()
if not GdObjects.equals(current, expected):

View File

@ -3,6 +3,7 @@ class_name GdUnitAssertions
extends RefCounted
@warning_ignore("return_value_discarded")
func _init() -> void:
# preload all gdunit assertions to speedup testsuite loading time
# gdlint:disable=private-method-call

View File

@ -1,14 +1,14 @@
extends GdUnitBoolAssert
var _base: GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not GdUnitAssertions.validate_value_type(current, TYPE_BOOL):
@warning_ignore("return_value_discarded")
report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -34,32 +34,43 @@ func report_error(error :String) -> GdUnitBoolAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
# Verifies that the current value is null.
func is_null() -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
# Verifies that the current value is not null.
func is_not_null() -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitBoolAssert:
func is_equal(expected: Variant) -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitBoolAssert:
func is_not_equal(expected: Variant) -> GdUnitBoolAssert:
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self

View File

@ -1,14 +1,14 @@
extends GdUnitDictionaryAssert
var _base :GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not GdUnitAssertions.validate_value_type(current, TYPE_DICTIONARY):
@warning_ignore("return_value_discarded")
report_error("GdUnitDictionaryAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -30,24 +30,33 @@ func report_error(error :String) -> GdUnitDictionaryAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitDictionaryAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitDictionaryAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func current_value() -> Variant:
return _base.current_value()
func is_null() -> GdUnitDictionaryAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitDictionaryAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
@ -96,14 +105,16 @@ func is_not_same(expected :Variant) -> GdUnitDictionaryAssert:
func is_empty() -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null or not current.is_empty():
@warning_ignore("unsafe_cast")
if current == null or not (current as Dictionary).is_empty():
return report_error(GdAssertMessages.error_is_empty(current))
return report_success()
func is_not_empty() -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null or current.is_empty():
@warning_ignore("unsafe_cast")
if current == null or (current as Dictionary).is_empty():
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
@ -112,7 +123,8 @@ func has_size(expected: int) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
if current.size() != expected:
@warning_ignore("unsafe_cast")
if (current as Dictionary).size() != expected:
return report_error(GdAssertMessages.error_has_size(current, expected))
return report_success()
@ -122,9 +134,11 @@ func _contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> Gd
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
# find expected keys
var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode))
@warning_ignore("unsafe_cast")
var keys_not_found :Array = expected.filter(_filter_by_key.bind((current as Dictionary).keys(), compare_mode))
if not keys_not_found.is_empty():
return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode))
@warning_ignore("unsafe_cast")
return report_error(GdAssertMessages.error_contains_keys((current as Dictionary).keys() as Array, expected, keys_not_found, compare_mode))
return report_success()
@ -133,11 +147,12 @@ func _contains_key_value(key :Variant, value :Variant, compare_mode :GdObjects.C
var expected := [key]
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode))
var dict_current: Dictionary = current
var keys_not_found :Array = expected.filter(_filter_by_key.bind(dict_current.keys(), compare_mode))
if not keys_not_found.is_empty():
return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode))
if not GdObjects.equals(current[key], value, false, compare_mode):
return report_error(GdAssertMessages.error_contains_key_value(key, value, current[key], compare_mode))
return report_error(GdAssertMessages.error_contains_keys(dict_current.keys() as Array, expected, keys_not_found, compare_mode))
if not GdObjects.equals(dict_current[key], value, false, compare_mode):
return report_error(GdAssertMessages.error_contains_key_value(key, value, dict_current[key], compare_mode))
return report_success()
@ -145,9 +160,10 @@ func _not_contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
var keys_found :Array = current.keys().filter(_filter_by_key.bind(expected, compare_mode, true))
var dict_current: Dictionary = current
var keys_found :Array = dict_current.keys().filter(_filter_by_key.bind(expected, compare_mode, true))
if not keys_found.is_empty():
return report_error(GdAssertMessages.error_not_contains_keys(current.keys(), expected, keys_found, compare_mode))
return report_error(GdAssertMessages.error_not_contains_keys(dict_current.keys() as Array, expected, keys_found, compare_mode))
return report_success()

View File

@ -15,6 +15,7 @@ func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAs
_set_do_expect_fail(true)
var thread_context := GdUnitThreadManager.get_current_context()
thread_context.set_assert(null)
@warning_ignore("return_value_discarded")
GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed)
# execute the given assertion as callable
if do_await:
@ -28,11 +29,13 @@ func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAs
_is_failed = true
_failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'"
return self
@warning_ignore("unsafe_method_access")
_failure_message = current_assert.failure_message()
return self
func execute(assertion :Callable) -> GdUnitFailureAssert:
@warning_ignore("return_value_discarded")
execute_and_await(assertion, false)
return self
@ -42,12 +45,12 @@ func _on_test_failed(value :bool) -> void:
@warning_ignore("unused_parameter")
func is_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert:
func is_equal(_expected: Variant) -> GdUnitFailureAssert:
return _report_error("Not implemented")
@warning_ignore("unused_parameter")
func is_not_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert:
func is_not_equal(_expected: Variant) -> GdUnitFailureAssert:
return _report_error("Not implemented")
@ -79,13 +82,24 @@ func has_line(expected :int) -> GdUnitFailureAssert:
func has_message(expected :String) -> GdUnitFailureAssert:
@warning_ignore("return_value_discarded")
is_failed()
var expected_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(expected))
var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
if current_error != expected_error:
var diffs := GdDiffTool.string_diff(current_error, expected_error)
var current := GdAssertMessages.colored_array_div(diffs[1])
_report_error(GdAssertMessages.error_not_same_error(current, expected_error))
return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
return self
func contains_message(expected :String) -> GdUnitFailureAssert:
var expected_error := GdUnitTools.normalize_text(expected)
var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
if not current_error.contains(expected_error):
var diffs := GdDiffTool.string_diff(current_error, expected_error)
var current := GdAssertMessages.colored_array_div(diffs[1])
return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
return self
@ -95,7 +109,7 @@ func starts_with_message(expected :String) -> GdUnitFailureAssert:
if current_error.find(expected_error) != 0:
var diffs := GdDiffTool.string_diff(current_error, expected_error)
var current := GdAssertMessages.colored_array_div(diffs[1])
_report_error(GdAssertMessages.error_not_same_error(current, expected_error))
return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
return self

View File

@ -2,15 +2,15 @@ extends GdUnitFileAssert
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
var _base: GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not GdUnitAssertions.validate_value_type(current, TYPE_STRING):
@warning_ignore("return_value_discarded")
report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -22,7 +22,7 @@ func _notification(event :int) -> void:
func current_value() -> String:
return _base.current_value() as String
return _base.current_value()
func report_success() -> GdUnitFileAssert:
@ -36,20 +36,29 @@ func report_error(error :String) -> GdUnitFileAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitFileAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitFileAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_equal(expected :Variant) -> GdUnitFileAssert:
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitFileAssert:
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self
@ -79,17 +88,14 @@ func is_script() -> GdUnitFileAssert:
return report_success()
func contains_exactly(expected_rows :Array) -> GdUnitFileAssert:
func contains_exactly(expected_rows: Array) -> GdUnitFileAssert:
var current := current_value()
if FileAccess.open(current, FileAccess.READ) == null:
return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()])
var script := load(current)
var script: GDScript = load(current)
if script is GDScript:
var instance :Variant = script.new()
var source_code := GdScriptParser.to_unix_format(instance.get_script().source_code)
GdUnitTools.free_instance(instance)
var source_code := GdScriptParser.to_unix_format(script.source_code)
var rows := Array(source_code.split("\n"))
ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(rows).contains_exactly(expected_rows)
GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows)
return self

View File

@ -1,14 +1,14 @@
extends GdUnitFloatAssert
var _base: GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not GdUnitAssertions.validate_value_type(current, TYPE_FLOAT):
@warning_ignore("return_value_discarded")
report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -34,30 +34,41 @@ func report_error(error :String) -> GdUnitFloatAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_null() -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
func is_equal(expected :float) -> GdUnitFloatAssert:
func is_equal(expected :Variant) -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :float) -> GdUnitFloatAssert:
func is_not_equal(expected :Variant) -> GdUnitFloatAssert:
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self
@ -111,14 +122,16 @@ func is_not_negative() -> GdUnitFloatAssert:
func is_zero() -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or not is_equal_approx(0.00000000, current):
@warning_ignore("unsafe_cast")
if current == null or not is_equal_approx(0.00000000, current as float):
return report_error(GdAssertMessages.error_is_zero(current))
return report_success()
func is_not_zero() -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or is_equal_approx(0.00000000, current):
@warning_ignore("unsafe_cast")
if current == null or is_equal_approx(0.00000000, current as float):
return report_error(GdAssertMessages.error_is_not_zero())
return report_success()

View File

@ -6,8 +6,9 @@ const DEFAULT_TIMEOUT := 2000
var _current_value_provider :ValueProvider
var _current_error_message :String = ""
var _current_failure_message :String = ""
var _custom_failure_message :String = ""
var _additional_failure_message: String = ""
var _line_number := -1
var _timeout := DEFAULT_TIMEOUT
var _interrupted := false
@ -21,21 +22,26 @@ func _init(instance :Object, func_name :String, args := Array()) -> void:
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 is_instance_valid(_current_value_provider):
_current_value_provider.dispose()
_current_value_provider = null
if is_instance_valid(_sleep_timer):
Engine.get_main_loop().root.remove_child(_sleep_timer)
_sleep_timer.stop()
_sleep_timer.free()
_sleep_timer = null
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:
@ -43,18 +49,14 @@ func report_success() -> GdUnitFuncAssert:
return self
func report_error(error_message :String) -> GdUnitFuncAssert:
_current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message
GdAssertReports.report_error(_current_error_message, _line_number)
func report_error(failure :String) -> GdUnitFuncAssert:
_current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
GdAssertReports.report_error(_current_failure_message, _line_number)
return self
func failure_message() -> String:
return _current_error_message
func send_report(report :GdUnitReport)-> void:
GdUnitSignals.instance().gdunit_report.emit(report)
return _current_failure_message
func override_failure_message(message :String) -> GdUnitFuncAssert:
@ -62,6 +64,11 @@ func override_failure_message(message :String) -> GdUnitFuncAssert:
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")
@ -111,6 +118,10 @@ 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
@ -118,16 +129,16 @@ func _validate_callback(predicate :Callable, expected :Variant = null) -> void:
var time_scale := Engine.get_time_scale()
var timer := Timer.new()
timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id())
Engine.get_main_loop().root.add_child(timer)
var scene_tree := Engine.get_main_loop() as SceneTree
scene_tree.root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.timeout.connect(func do_interrupt() -> void:
_interrupted = true
, CONNECT_DEFERRED)
@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() )
Engine.get_main_loop().root.add_child(_sleep_timer)
scene_tree.root.add_child(_sleep_timer)
while true:
var current :Variant = await next_current_value()
@ -139,13 +150,20 @@ func _validate_callback(predicate :Callable, expected :Variant = null) -> void:
await _sleep_timer.timeout
_sleep_timer.stop()
await Engine.get_main_loop().process_frame
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]
report_error(GdAssertMessages.error_interrupted(predicate_name.strip_edges().trim_prefix("cb_"), expected, LocalTime.elapsed(_timeout)))
@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()

View File

@ -17,6 +17,7 @@ func _init(callable :Callable) -> void:
func _execute() -> Array[ErrorLogEntry]:
# execute the given code and monitor for runtime errors
if _callable == null or not _callable.is_valid():
@warning_ignore("return_value_discarded")
_report_error("Invalid Callable '%s'" % _callable)
else:
await _callable.call()

View File

@ -1,14 +1,14 @@
extends GdUnitIntAssert
var _base: GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not GdUnitAssertions.validate_value_type(current, TYPE_INT):
@warning_ignore("return_value_discarded")
report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -34,30 +34,41 @@ func report_error(error :String) -> GdUnitIntAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_null() -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
func is_equal(expected :int) -> GdUnitIntAssert:
func is_equal(expected :Variant) -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :int) -> GdUnitIntAssert:
func is_not_equal(expected :Variant) -> GdUnitIntAssert:
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self

View File

@ -1,11 +1,10 @@
extends GdUnitObjectAssert
var _base :GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if (current != null
@ -13,6 +12,7 @@ func _init(current :Variant) -> void:
or GdUnitAssertions.validate_value_type(current, TYPE_INT)
or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT)
or GdUnitAssertions.validate_value_type(current, TYPE_STRING))):
@warning_ignore("return_value_discarded")
report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -38,30 +38,41 @@ func report_error(error :String) -> GdUnitObjectAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_equal(expected :Variant) -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self
func is_null() -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitObjectAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
@ -70,19 +81,15 @@ func is_not_null() -> GdUnitObjectAssert:
func is_same(expected :Variant) -> GdUnitObjectAssert:
var current :Variant = current_value()
if not is_same(current, expected):
report_error(GdAssertMessages.error_is_same(current, expected))
return self
report_success()
return self
return report_error(GdAssertMessages.error_is_same(current, expected))
return report_success()
func is_not_same(expected :Variant) -> GdUnitObjectAssert:
var current :Variant = current_value()
if is_same(current, expected):
report_error(GdAssertMessages.error_not_same(current, expected))
return self
report_success()
return self
return report_error(GdAssertMessages.error_not_same(current, expected))
return report_success()
func is_instanceof(type :Object) -> GdUnitObjectAssert:
@ -90,10 +97,8 @@ func is_instanceof(type :Object) -> GdUnitObjectAssert:
if current == null or not is_instance_of(current, type):
var result_expected: = GdObjects.extract_class_name(type)
var result_current: = GdObjects.extract_class_name(current)
report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected))
return self
report_success()
return self
return report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected))
return report_success()
func is_not_instanceof(type :Variant) -> GdUnitObjectAssert:
@ -101,9 +106,8 @@ func is_not_instanceof(type :Variant) -> GdUnitObjectAssert:
if is_instance_of(current, type):
var result: = GdObjects.extract_class_name(type)
if result.is_success():
report_error("Expected not be a instance of <%s>" % result.value())
else:
push_error("Internal ERROR: %s" % result.error_message())
return report_error("Expected not be a instance of <%s>" % str(result.value()))
push_error("Internal ERROR: %s" % result.error_message())
return self
report_success()
return self
return report_success()

View File

@ -1,14 +1,14 @@
extends GdUnitResultAssert
var _base :GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not validate_value_type(current):
@warning_ignore("return_value_discarded")
report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -24,7 +24,7 @@ func validate_value_type(value :Variant) -> bool:
func current_value() -> GdUnitResult:
return _base.current_value() as GdUnitResult
return _base.current_value()
func report_success() -> GdUnitResultAssert:
@ -38,20 +38,29 @@ func report_error(error :String) -> GdUnitResultAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitResultAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitResultAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_null() -> GdUnitResultAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitResultAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
@ -59,63 +68,50 @@ func is_not_null() -> GdUnitResultAssert:
func is_empty() -> GdUnitResultAssert:
var result := current_value()
if result == null or not result.is_empty():
report_error(GdAssertMessages.error_result_is_empty(result))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_is_empty(result))
return report_success()
func is_success() -> GdUnitResultAssert:
var result := current_value()
if result == null or not result.is_success():
report_error(GdAssertMessages.error_result_is_success(result))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_is_success(result))
return report_success()
func is_warning() -> GdUnitResultAssert:
var result := current_value()
if result == null or not result.is_warn():
report_error(GdAssertMessages.error_result_is_warning(result))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_is_warning(result))
return report_success()
func is_error() -> GdUnitResultAssert:
var result := current_value()
if result == null or not result.is_error():
report_error(GdAssertMessages.error_result_is_error(result))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_is_error(result))
return report_success()
func contains_message(expected :String) -> GdUnitResultAssert:
var result := current_value()
if result == null:
report_error(GdAssertMessages.error_result_has_message("<null>", expected))
return self
return report_error(GdAssertMessages.error_result_has_message("<null>", expected))
if result.is_success():
report_error(GdAssertMessages.error_result_has_message_on_success(expected))
elif result.is_error() and result.error_message() != expected:
report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected))
elif result.is_warn() and result.warn_message() != expected:
report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_has_message_on_success(expected))
if result.is_error() and result.error_message() != expected:
return report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected))
if result.is_warn() and result.warn_message() != expected:
return report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected))
return report_success()
func is_value(expected :Variant) -> GdUnitResultAssert:
var result := current_value()
var value :Variant = null if result == null else result.value()
if not GdObjects.equals(value, expected):
report_error(GdAssertMessages.error_result_is_value(value, expected))
else:
report_success()
return self
return report_error(GdAssertMessages.error_result_is_value(value, expected))
return report_success()
func is_equal(expected :Variant) -> GdUnitResultAssert:

View File

@ -4,8 +4,9 @@ const DEFAULT_TIMEOUT := 2000
var _signal_collector :GdUnitSignalCollector
var _emitter :Object
var _current_error_message :String = ""
var _current_failure_message :String = ""
var _custom_failure_message :String = ""
var _additional_failure_message: String = ""
var _line_number := -1
var _timeout := DEFAULT_TIMEOUT
var _interrupted := false
@ -21,6 +22,14 @@ func _init(emitter :Object) -> void:
GdAssertReports.reset_last_error_line_number()
func _notification(what :int) -> void:
if what == NOTIFICATION_PREDELETE:
_interrupted = true
if is_instance_valid(_emitter):
_signal_collector.unregister_emitter(_emitter)
_emitter = null
func report_success() -> GdUnitAssert:
GdAssertReports.report_success()
return self
@ -31,18 +40,14 @@ func report_warning(message :String) -> GdUnitAssert:
return self
func report_error(error_message :String) -> GdUnitAssert:
_current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message
GdAssertReports.report_error(_current_error_message, _line_number)
func report_error(failure :String) -> GdUnitAssert:
_current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
GdAssertReports.report_error(_current_failure_message, _line_number)
return self
func failure_message() -> String:
return _current_error_message
func send_report(report :GdUnitReport)-> void:
GdUnitSignals.instance().gdunit_report.emit(report)
return _current_failure_message
func override_failure_message(message :String) -> GdUnitSignalAssert:
@ -50,8 +55,14 @@ func override_failure_message(message :String) -> GdUnitSignalAssert:
return self
func append_failure_message(message :String) -> GdUnitSignalAssert:
_additional_failure_message = message
return self
func wait_until(timeout := 2000) -> GdUnitSignalAssert:
if timeout <= 0:
@warning_ignore("return_value_discarded")
report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!")
_timeout = DEFAULT_TIMEOUT
else:
@ -62,6 +73,7 @@ func wait_until(timeout := 2000) -> GdUnitSignalAssert:
# Verifies the signal exists checked the emitter
func is_signal_exists(signal_name :String) -> GdUnitSignalAssert:
if not _emitter.has_signal(signal_name):
@warning_ignore("return_value_discarded")
report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()])
return self
@ -80,29 +92,30 @@ func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert:
func _wail_until_signal(signal_name :String, expected_args :Array, expect_not_emitted: bool) -> GdUnitSignalAssert:
if _emitter == null:
report_error("Can't wait for signal checked a NULL object.")
return self
return report_error("Can't wait for signal checked a NULL object.")
# first verify the signal is defined
if not _emitter.has_signal(signal_name):
report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()])
return self
return report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()])
_signal_collector.register_emitter(_emitter)
var time_scale := Engine.get_time_scale()
var timer := Timer.new()
Engine.get_main_loop().root.add_child(timer)
(Engine.get_main_loop() as SceneTree).root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
@warning_ignore("return_value_discarded")
timer.timeout.connect(func on_timeout() -> void: _interrupted = true)
timer.start((_timeout/1000.0)*time_scale)
var is_signal_emitted := false
while not _interrupted and not is_signal_emitted:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
if is_instance_valid(_emitter):
is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args)
if is_signal_emitted and expect_not_emitted:
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_signal_emitted(signal_name, expected_args, LocalTime.elapsed(int(_timeout-timer.time_left*1000))))
if _interrupted and not expect_not_emitted:
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout)))
timer.free()
if is_instance_valid(_emitter):

View File

@ -1,14 +1,14 @@
extends GdUnitStringAssert
var _base :GdUnitAssert
var _base: GdUnitAssertImpl
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if current != null and typeof(current) != TYPE_STRING and typeof(current) != TYPE_STRING_NAME:
@warning_ignore("return_value_discarded")
report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
@ -20,7 +20,7 @@ func _notification(event :int) -> void:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func current_value() -> Variant:
@ -38,16 +38,25 @@ func report_error(error :String) -> GdUnitStringAssert:
func override_failure_message(message :String) -> GdUnitStringAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitStringAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_null() -> GdUnitStringAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitStringAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
@ -90,49 +99,56 @@ func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
func is_empty() -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or not current.is_empty():
@warning_ignore("unsafe_cast")
if current == null or not (current as String).is_empty():
return report_error(GdAssertMessages.error_is_empty(current))
return report_success()
func is_not_empty() -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or current.is_empty():
@warning_ignore("unsafe_cast")
if current == null or (current as String).is_empty():
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
func contains(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or current.find(expected) == -1:
@warning_ignore("unsafe_cast")
if current == null or (current as String).find(expected) == -1:
return report_error(GdAssertMessages.error_contains(current, expected))
return report_success()
func not_contains(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current != null and current.find(expected) != -1:
@warning_ignore("unsafe_cast")
if current != null and (current as String).find(expected) != -1:
return report_error(GdAssertMessages.error_not_contains(current, expected))
return report_success()
func contains_ignoring_case(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or current.findn(expected) == -1:
@warning_ignore("unsafe_cast")
if current == null or (current as String).findn(expected) == -1:
return report_error(GdAssertMessages.error_contains_ignoring_case(current, expected))
return report_success()
func not_contains_ignoring_case(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current != null and current.findn(expected) != -1:
@warning_ignore("unsafe_cast")
if current != null and (current as String).findn(expected) != -1:
return report_error(GdAssertMessages.error_not_contains_ignoring_case(current, expected))
return report_success()
func starts_with(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or current.find(expected) != 0:
@warning_ignore("unsafe_cast")
if current == null or (current as String).find(expected) != 0:
return report_error(GdAssertMessages.error_starts_with(current, expected))
return report_success()
@ -141,8 +157,10 @@ func ends_with(expected :String) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_ends_with(current, expected))
var find :int = current.length() - expected.length()
if current.rfind(expected) != find:
@warning_ignore("unsafe_cast")
var find :int = (current as String).length() - expected.length()
@warning_ignore("unsafe_cast")
if (current as String).rfind(expected) != find:
return report_error(GdAssertMessages.error_ends_with(current, expected))
return report_success()
@ -152,22 +170,23 @@ func has_length(expected :int, comparator := Comparator.EQUAL) -> GdUnitStringAs
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
var str_current: String = current
match comparator:
Comparator.EQUAL:
if current.length() != expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
if str_current.length() != expected:
return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
Comparator.LESS_THAN:
if current.length() >= expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
if str_current.length() >= expected:
return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
Comparator.LESS_EQUAL:
if current.length() > expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
if str_current.length() > expected:
return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
Comparator.GREATER_THAN:
if current.length() <= expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
if str_current.length() <= expected:
return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
Comparator.GREATER_EQUAL:
if current.length() < expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
if str_current.length() < expected:
return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
_:
return report_error("Comparator '%d' not implemented!" % comparator)
return report_success()

View File

@ -1,15 +1,16 @@
extends GdUnitVectorAssert
var _base: GdUnitAssert
var _current_type :int
var _base: GdUnitAssertImpl
var _current_type: int
var _type_check: bool
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
func _init(current: Variant, type_check := true) -> void:
_type_check = type_check
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not _validate_value_type(current):
@warning_ignore("return_value_discarded")
report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current))
_current_type = typeof(current)
@ -39,6 +40,7 @@ func _validate_is_vector_type(value :Variant) -> bool:
var type := typeof(value)
if type == _current_type or _current_type == TYPE_NIL:
return true
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_is_wrong_type(_current_type, type))
return false
@ -58,34 +60,45 @@ func report_error(error :String) -> GdUnitVectorAssert:
func failure_message() -> String:
return _base._current_error_message
return _base.failure_message()
func override_failure_message(message :String) -> GdUnitVectorAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message :String) -> GdUnitVectorAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func is_null() -> GdUnitVectorAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitVectorAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
func is_equal(expected: Variant) -> GdUnitVectorAssert:
if _type_check and not _validate_is_vector_type(expected):
return self
@warning_ignore("return_value_discarded")
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
func is_not_equal(expected: Variant) -> GdUnitVectorAssert:
if _type_check and not _validate_is_vector_type(expected):
return self
@warning_ignore("return_value_discarded")
_base.is_not_equal(expected)
return self

View File

@ -4,3 +4,7 @@ extends RefCounted
func get_value() -> Variant:
return null
func dispose() -> void:
pass

View File

@ -40,22 +40,23 @@ func options() -> CmdOptions:
return _options
func _parse_cmd_arguments(option :CmdOption, args :Array) -> int:
func _parse_cmd_arguments(option: CmdOption, args: Array) -> int:
var command_name := option.short_command()
var command :CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name))
var command: CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name))
if option.has_argument():
if not option.is_argument_optional() and args.is_empty():
return -1
if _is_next_value_argument(args):
command.add_argument(args.pop_front())
var value: String = args.pop_front()
command.add_argument(value)
elif not option.is_argument_optional():
return -1
_parsed_commands[command_name] = command
return 0
func _is_next_value_argument(args :Array) -> bool:
func _is_next_value_argument(args: PackedStringArray) -> bool:
if args.is_empty():
return false
return _options.get_option(args[0]) == null

View File

@ -19,6 +19,7 @@ func arguments() -> PackedStringArray:
func add_argument(arg :String) -> void:
@warning_ignore("return_value_discarded")
_arguments.append(arg)

View File

@ -57,14 +57,17 @@ func _validate() -> GdUnitResult:
if _command_cbs[cmd_name][CB_SINGLE_ARG]
else _command_cbs[cmd_name][CB_MULTI_ARGS])
if cb != NO_CB and not cb.is_valid():
@warning_ignore("return_value_discarded")
errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name)
if _cmd_options.get_option(cmd_name) == null:
@warning_ignore("return_value_discarded")
errors.append("The command '%s' is unknown, verify your CmdOptions!" % cmd_name)
# verify for multiple registered command callbacks
if _enhanced_fr_test and cb != NO_CB:
var cb_method: = cb.get_method()
if registered_cbs.has(cb_method):
var already_registered_cmd :String = registered_cbs[cb_method]
@warning_ignore("return_value_discarded")
errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd])
else:
registered_cbs[cb_method] = cmd_name
@ -95,7 +98,8 @@ func execute(commands :Array[CmdCommand]) -> GdUnitResult:
# we need to find the method and determin the arguments to call the right function
for m in cb_m.get_object().get_method_list():
if m["name"] == cb_m.get_method():
if m["args"].size() > 1:
@warning_ignore("unsafe_cast")
if (m["args"] as Array).size() > 1:
cb_m.callv(arguments)
break
else:

View File

@ -23,14 +23,14 @@ var _color_mode := COLOR_TABLE
func color(p_color :Color) -> CmdConsole:
# using color table 16 - 231 a 6 x 6 x 6 RGB color cube (16 + R * 36 + G * 6 + B)
if _color_mode == COLOR_TABLE:
@warning_ignore("integer_division")
var c2 := 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42)
if _debug_show_color_codes:
printraw("%6d" % [c2])
printraw("[38;5;%dm" % c2 )
else:
printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] )
#if _color_mode == COLOR_TABLE:
# @warning_ignore("integer_division")
# var c2 := 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42)
# if _debug_show_color_codes:
# printraw("%6d" % [c2])
# printraw("[38;5;%dm" % c2 )
#else:
printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] )
return self
@ -59,6 +59,7 @@ func scroll_area(from :int, to :int) -> CmdConsole:
return self
@warning_ignore("return_value_discarded")
func progress_bar(p_progress :int, p_color :Color = Color.POWDER_BLUE) -> CmdConsole:
if p_progress < 0:
p_progress = 0
@ -123,6 +124,7 @@ func print_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole:
.end_color()
@warning_ignore("return_value_discarded")
func print_color_table() -> void:
prints_color("Color Table 6x6x6", Color.ANTIQUE_WHITE)
_debug_show_color_codes = true

View File

@ -28,10 +28,11 @@ static func is_type_array(type :int) -> bool:
## Filters an array by given value[br]
## If the given value not an array it returns null, will remove all occurence of given value.
static func filter_value(array :Variant, value :Variant) -> Variant:
@warning_ignore("unsafe_method_access")
static func filter_value(array: Variant, value: Variant) -> Variant:
if not is_array_type(array):
return null
var filtered_array :Variant = array.duplicate()
var filtered_array: Variant = array.duplicate()
var index :int = filtered_array.find(value)
while index != -1:
filtered_array.remove_at(index)
@ -72,13 +73,12 @@ static func scan_typed(array :Array) -> int:
## # will result in PackedString(["a", "b"])
## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN))
## [/codeblock]
static func as_string(elements :Variant, encode_value := true) -> String:
if not is_array_type(elements):
return "ERROR: Not an Array Type!"
static func as_string(elements: Variant, encode_value := true) -> String:
var delemiter := ", "
if elements == null:
return "<null>"
if elements.is_empty():
@warning_ignore("unsafe_cast")
if (elements as Array).is_empty():
return "<empty>"
var prefix := _typeof_as_string(elements) if encode_value else ""
var formatted := ""
@ -93,6 +93,14 @@ static func as_string(elements :Variant, encode_value := true) -> String:
return prefix + "[" + formatted + "]"
static func has_same_content(current: Array, other: Array) -> bool:
if current.size() != other.size(): return false
for element: Variant in current:
if not other.has(element): return false
if current.count(element) != other.count(element): return false
return true
static func _typeof_as_string(value :Variant) -> String:
var type := typeof(value)
# for untyped array we retun empty string

View File

@ -7,7 +7,7 @@ const DIV_ADD :int = 214
const DIV_SUB :int = 215
static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff: Array, rdiff: Array) -> void:
static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array[Array], ldiff: Array, rdiff: Array) -> void:
var loffset := lb.size()
var roffset := rb.size()
@ -39,17 +39,19 @@ static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff
# lookup[i][j] stores the length of LCS of substring X[0..i-1], Y[0..j-1]
static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array:
var lookup := Array()
static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]:
var lookup: Array[Array] = []
@warning_ignore("return_value_discarded")
lookup.resize(lb.size() + 1)
for i in lookup.size():
var x := []
@warning_ignore("return_value_discarded")
x.resize(rb.size() + 1)
lookup[i] = x
return lookup
static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array:
static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]:
var lookup := _createLookUp(lb, rb)
# first column of the lookup table will be all 0
for i in lookup.size():
@ -105,6 +107,7 @@ static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStri
var lcsResultList := PackedStringArray();
while (i < text1WordCount && j < text2WordCount):
if text1Words[i] == text2Words[j]:
@warning_ignore("return_value_discarded")
lcsResultList.append(text2Words[j])
i += 1
j += 1

View File

@ -39,6 +39,8 @@ const DEFAULT_TYPED_RETURN_VALUES := {
TYPE_PACKED_STRING_ARRAY: "PackedStringArray()",
TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()",
TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()",
# since Godot 4.3.beta1 TYPE_PACKED_VECTOR4_ARRAY = 38
GdObjects.TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array()",
TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()",
GdObjects.TYPE_VARIANT: "null",
GdObjects.TYPE_ENUM: "0"
@ -80,7 +82,9 @@ static func get_enum_default(value :String) -> Variant:
return %s.values()[0]
""".dedent() % value
@warning_ignore("return_value_discarded")
script.reload()
@warning_ignore("unsafe_method_access")
return script.new().call("get_enum_default")
@ -111,20 +115,19 @@ func _init(push_errors :bool = false) -> void:
@warning_ignore("unused_parameter")
func get_template(return_type :Variant, is_vararg :bool) -> String:
push_error("Must be implemented!")
func get_template(return_type: GdFunctionDescriptor, is_callable: bool) -> String:
assert(false, "'get_template' must be implemented!")
return ""
func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray:
var func_signature := func_descriptor.typeless()
func double(func_descriptor: GdFunctionDescriptor, is_callable: bool = false) -> PackedStringArray:
var is_static := func_descriptor.is_static()
var is_vararg := func_descriptor.is_vararg()
var is_coroutine := func_descriptor.is_coroutine()
var func_name := func_descriptor.name()
var args := func_descriptor.args()
var varargs := func_descriptor.varargs()
var return_value := GdFunctionDoubler.default_return_value(func_descriptor)
var arg_names := extract_arg_names(args)
var arg_names := extract_arg_names(args, true)
var vararg_names := extract_arg_names(varargs)
# save original constructor arguments
@ -133,17 +136,15 @@ func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray:
var constructor := "func _init(%s) -> void:\n super(%s)\n pass\n" % [constructor_args, ", ".join(arg_names)]
return constructor.split("\n")
var double_src := ""
double_src += '@warning_ignore("untyped_declaration")\n' if Engine.get_version_info().hex >= 0x40200 else '\n'
var double_src := "@warning_ignore('shadowed_variable', 'untyped_declaration', 'unsafe_call_argument', 'unsafe_method_access')\n"
if func_descriptor.is_engine():
double_src += '@warning_ignore("native_method_override")\n'
if func_descriptor.return_type() == GdObjects.TYPE_ENUM:
double_src += '@warning_ignore("int_as_enum_without_match")\n'
double_src += '@warning_ignore("int_as_enum_without_cast")\n'
double_src += '@warning_ignore("shadowed_variable")\n'
double_src += func_signature
double_src += GdFunctionDoubler.extract_func_signature(func_descriptor)
# fix to unix format, this is need when the template is edited under windows than the template is stored with \r\n
var func_template := get_template(func_descriptor.return_type(), is_vararg).replace("\r\n", "\n")
var func_template := get_template(func_descriptor, is_callable).replace("\r\n", "\n")
double_src += func_template\
.replace("$(arguments)", ", ".join(arg_names))\
.replace("$(varargs)", ", ".join(vararg_names))\
@ -159,25 +160,54 @@ func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray:
return double_src.split("\n")
func extract_arg_names(argument_signatures :Array[GdFunctionArgument]) -> PackedStringArray:
func extract_arg_names(argument_signatures: Array[GdFunctionArgument], add_suffix := false) -> PackedStringArray:
var arg_names := PackedStringArray()
for arg in argument_signatures:
arg_names.append(arg._name)
@warning_ignore("return_value_discarded")
arg_names.append(arg._name + ("_" if add_suffix else ""))
return arg_names
static func extract_constructor_args(args :Array[GdFunctionArgument]) -> PackedStringArray:
var constructor_args := PackedStringArray()
for arg in args:
var arg_name := arg._name
var arg_name := arg._name + "_"
var default_value := get_default(arg)
if default_value == "null":
@warning_ignore("return_value_discarded")
constructor_args.append(arg_name + ":Variant=" + default_value)
else:
@warning_ignore("return_value_discarded")
constructor_args.append(arg_name + ":=" + default_value)
return constructor_args
static func extract_func_signature(descriptor: GdFunctionDescriptor) -> String:
var func_signature := ""
if descriptor._return_type == TYPE_NIL:
func_signature = "func %s(%s) -> void:" % [descriptor.name(), typeless_args(descriptor)]
elif descriptor._return_type == GdObjects.TYPE_VARIANT:
func_signature = "func %s(%s):" % [descriptor.name(), typeless_args(descriptor)]
else:
func_signature = "func %s(%s) -> %s:" % [descriptor.name(), typeless_args(descriptor), descriptor.return_type_as_string()]
return "static " + func_signature if descriptor.is_static() else func_signature
static func typeless_args(descriptor: GdFunctionDescriptor) -> String:
var collect := PackedStringArray()
for arg in descriptor.args():
if arg.has_default():
@warning_ignore("return_value_discarded")
collect.push_back(arg.name() + "_" + "=" + arg.value_as_string())
else:
@warning_ignore("return_value_discarded")
collect.push_back(arg.name() + "_")
for arg in descriptor.varargs():
@warning_ignore("return_value_discarded")
collect.push_back(arg.name() + "=" + arg.value_as_string())
return ", ".join(collect)
static func get_default(arg :GdFunctionArgument) -> String:
if arg.has_default():
return arg.value_as_string()

View File

@ -4,17 +4,20 @@ extends Resource
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const TYPE_VOID = TYPE_MAX + 1000
const TYPE_VARARG = TYPE_MAX + 1001
const TYPE_VARIANT = TYPE_MAX + 1002
const TYPE_FUNC = TYPE_MAX + 1003
const TYPE_FUZZER = TYPE_MAX + 1004
const TYPE_NODE = TYPE_MAX + 2001
# introduced with Godot 4.3.beta1
const TYPE_PACKED_VECTOR4_ARRAY = 38 #TYPE_PACKED_VECTOR4_ARRAY
const TYPE_VOID = 1000
const TYPE_VARARG = 1001
const TYPE_VARIANT = 1002
const TYPE_FUNC = 1003
const TYPE_FUZZER = 1004
# missing Godot types
const TYPE_CONTROL = TYPE_MAX + 2002
const TYPE_CANVAS = TYPE_MAX + 2003
const TYPE_ENUM = TYPE_MAX + 2004
const TYPE_NODE = 2001
const TYPE_CONTROL = 2002
const TYPE_CANVAS = 2003
const TYPE_ENUM = 2004
# used as default value for varargs
@ -59,6 +62,7 @@ const TYPE_AS_STRING_MAPPINGS := {
TYPE_PACKED_STRING_ARRAY: "PackedStringArray",
TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array",
TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array",
TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array",
TYPE_PACKED_COLOR_ARRAY: "PackedColorArray",
TYPE_VOID: "void",
TYPE_VARARG: "VarArg",
@ -141,7 +145,8 @@ enum COMPARE_MODE {
# prototype of better object to dictionary
static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary:
@warning_ignore("unsafe_cast")
static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary:
if obj == null:
return {}
var clazz_name := obj.get_class()
@ -149,13 +154,20 @@ static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary:
var clazz_path := ""
if is_instance_valid(obj) and obj.get_script() != null:
var d := inst_to_dict(obj)
clazz_path = d["@path"]
if d["@subpath"] != NodePath(""):
clazz_name = d["@subpath"]
dict["@inner_class"] = true
var script: Script = obj.get_script()
# handle build-in scripts
if script.resource_path != null and script.resource_path.contains(".tscn"):
var path_elements := script.resource_path.split(".tscn")
clazz_name = path_elements[0].get_file()
clazz_path = script.resource_path
else:
clazz_name = clazz_path.get_file().replace(".gd", "")
var d := inst_to_dict(obj)
clazz_path = d["@path"]
if d["@subpath"] != NodePath(""):
clazz_name = d["@subpath"]
dict["@inner_class"] = true
else:
clazz_name = clazz_path.get_file().replace(".gd", "")
dict["@path"] = clazz_path
for property in obj.get_property_list():
@ -173,11 +185,14 @@ static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary:
dict[property_name] = str(property_value)
continue
hashed_objects[obj] = true
dict[property_name] = obj2dict(property_value, hashed_objects)
dict[property_name] = obj2dict(property_value as Object, hashed_objects)
else:
dict[property_name] = property_value
if obj.has_method("get_children"):
var childrens :Array = obj.get_children()
if obj is Node:
var childrens :Array = (obj as Node).get_children()
dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
if obj is TreeItem:
var childrens :Array = (obj as TreeItem).get_children()
dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
return {"%s" % clazz_name : dict}
@ -187,14 +202,15 @@ static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false,
return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0)
static func equals_sorted(obj_a :Array, obj_b :Array, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
var a := obj_a.duplicate()
var b := obj_b.duplicate()
static func equals_sorted(obj_a: Array[Variant], obj_b: Array[Variant], case_sensitive: bool = false, compare_mode: COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
var a: Array[Variant] = obj_a.duplicate()
var b: Array[Variant] = obj_b.duplicate()
a.sort()
b.sort()
return equals(a, b, case_sensitive, compare_mode)
@warning_ignore("unsafe_method_access", "unsafe_cast")
static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool:
var type_a := typeof(obj_a)
var type_b := typeof(obj_b)
@ -234,8 +250,8 @@ static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compar
return false
if obj_a.get_class() != obj_b.get_class():
return false
var a := obj2dict(obj_a)
var b := obj2dict(obj_b)
var a := obj2dict(obj_a as Object)
var b := obj2dict(obj_b as Object)
return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth)
return obj_a == obj_b
@ -293,6 +309,7 @@ static func to_pascal_case(value :String) -> String:
return value.capitalize().replace(" ", "")
@warning_ignore("return_value_discarded")
static func to_snake_case(value :String) -> String:
var result := PackedStringArray()
for ch in value:
@ -313,6 +330,8 @@ static func is_snake_case(value :String) -> bool:
static func type_as_string(type :int) -> String:
if type < TYPE_MAX:
return type_string(type)
return TYPE_AS_STRING_MAPPINGS.get(type, "Variant")
@ -345,10 +364,13 @@ static func _is_type_equivalent(type_a :int, type_b :int) -> bool:
or type_a == type_b)
static func is_engine_type(value :Object) -> bool:
static func is_engine_type(value :Variant) -> bool:
if value is GDScript or value is ScriptExtension:
return false
return value.is_class("GDScriptNativeClass")
var obj: Object = value
if is_instance_valid(obj) and obj.has_method("is_class"):
return obj.is_class("GDScriptNativeClass")
return false
static func is_type(value :Variant) -> bool:
@ -359,7 +381,8 @@ static func is_type(value :Variant) -> bool:
if is_engine_type(value):
return true
# is a custom class type
if value is GDScript and value.can_instantiate():
@warning_ignore("unsafe_cast")
if value is GDScript and (value as GDScript).can_instantiate():
return true
return false
@ -372,7 +395,8 @@ static func _is_same(left :Variant, right :Variant) -> bool:
if left_type != right_type:
return false
if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT:
return left.get_instance_id() == right.get_instance_id()
@warning_ignore("unsafe_cast")
return (left as Object).get_instance_id() == (right as Object).get_instance_id()
return equals(left, right)
@ -397,7 +421,8 @@ static func is_scene(value :Variant) -> bool:
static func is_scene_resource_path(value :Variant) -> bool:
return value is String and value.ends_with(".tscn")
@warning_ignore("unsafe_cast")
return value is String and (value as String).ends_with(".tscn")
static func is_gd_script(script :Script) -> bool:
@ -413,8 +438,8 @@ static func is_gd_testsuite(script :Script) -> bool:
if is_gd_script(script):
var stack := [script]
while not stack.is_empty():
var current := stack.pop_front() as Script
var base := current.get_base_script() as Script
var current: Script = stack.pop_front()
var base: Script = current.get_base_script()
if base != null:
if base.resource_path.find("GdUnitTestSuite") != -1:
return true
@ -422,11 +447,12 @@ static func is_gd_testsuite(script :Script) -> bool:
return false
static func is_singleton(value :Variant) -> bool:
static func is_singleton(value: Variant) -> bool:
if not is_instance_valid(value) or is_native_class(value):
return false
for name in Engine.get_singleton_list():
if value.is_class(name):
@warning_ignore("unsafe_cast")
if (value as Object).is_class(name):
return true
return false
@ -434,17 +460,19 @@ static func is_singleton(value :Variant) -> bool:
static func is_instance(value :Variant) -> bool:
if not is_instance_valid(value) or is_native_class(value):
return false
if is_script(value) and value.get_instance_base_type() == "":
@warning_ignore("unsafe_cast")
if is_script(value) and (value as Script).get_instance_base_type() == "":
return true
if is_scene(value):
return true
return not value.has_method('new') and not value.has_method('instance')
@warning_ignore("unsafe_cast")
return not (value as Object).has_method('new') and not (value as Object).has_method('instance')
# only object form type Node and attached filename
static func is_instance_scene(instance :Variant) -> bool:
if instance is Node:
var node := instance as Node
var node: Node = instance
return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty()
return false
@ -452,7 +480,8 @@ static func is_instance_scene(instance :Variant) -> bool:
static func can_be_instantiate(obj :Variant) -> bool:
if not obj or is_engine_type(obj):
return false
return obj.has_method("new")
@warning_ignore("unsafe_cast")
return (obj as Object).has_method("new")
static func create_instance(clazz :Variant) -> GdUnitResult:
@ -461,48 +490,54 @@ static func create_instance(clazz :Variant) -> GdUnitResult:
# test is given clazz already an instance
if is_instance(clazz):
return GdUnitResult.success(clazz)
@warning_ignore("unsafe_method_access")
return GdUnitResult.success(clazz.new())
TYPE_STRING:
if ClassDB.class_exists(clazz):
if Engine.has_singleton(clazz):
return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz)
if not ClassDB.can_instantiate(clazz):
return GdUnitResult.error("Can't instance Engine class '%s'." % clazz)
return GdUnitResult.success(ClassDB.instantiate(clazz))
var clazz_name: String = clazz
if ClassDB.class_exists(clazz_name):
if Engine.has_singleton(clazz_name):
return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz_name)
if not ClassDB.can_instantiate(clazz_name):
return GdUnitResult.error("Can't instance Engine class '%s'." % clazz_name)
return GdUnitResult.success(ClassDB.instantiate(clazz_name))
else:
var clazz_path :String = extract_class_path(clazz)[0]
var clazz_path :String = extract_class_path(clazz_name)[0]
if not FileAccess.file_exists(clazz_path):
return GdUnitResult.error("Class '%s' not found." % clazz)
var script := load(clazz_path)
return GdUnitResult.error("Class '%s' not found." % clazz_name)
var script: GDScript = load(clazz_path)
if script != null:
return GdUnitResult.success(script.new())
else:
return GdUnitResult.error("Can't create instance for '%s'." % clazz)
return GdUnitResult.error("Can't create instance for class '%s'." % clazz)
return GdUnitResult.error("Can't create instance for '%s'." % clazz_name)
return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz))
@warning_ignore("return_value_discarded")
static func extract_class_path(clazz :Variant) -> PackedStringArray:
var clazz_path := PackedStringArray()
if clazz is String:
clazz_path.append(clazz)
@warning_ignore("unsafe_cast")
clazz_path.append(clazz as String)
return clazz_path
if is_instance(clazz):
# is instance a script instance?
var script := clazz.script as GDScript
var script: GDScript = clazz.script
if script != null:
return extract_class_path(script)
return clazz_path
if clazz is GDScript:
if not clazz.resource_path.is_empty():
clazz_path.append(clazz.resource_path)
var script: GDScript = clazz
if not script.resource_path.is_empty():
clazz_path.append(script.resource_path)
return clazz_path
# if not found we go the expensive way and extract the path form the script by creating an instance
var arg_list := build_function_default_arguments(clazz, "_init")
var instance :Variant = clazz.callv("new", arg_list)
var arg_list := build_function_default_arguments(script, "_init")
var instance: Object = script.callv("new", arg_list)
var clazz_info := inst_to_dict(instance)
GdUnitTools.free_instance(instance)
clazz_path.append(clazz_info["@path"])
@warning_ignore("unsafe_cast")
clazz_path.append(clazz_info["@path"] as String)
if clazz_info.has("@subpath"):
var sub_path :String = clazz_info["@subpath"]
if not sub_path.is_empty():
@ -529,33 +564,38 @@ static func extract_class_name(clazz :Variant) -> GdUnitResult:
if is_instance(clazz):
# is instance a script instance?
var script := clazz.script as GDScript
var script: GDScript = clazz.script
if script != null:
return extract_class_name(script)
return GdUnitResult.success(clazz.get_class())
@warning_ignore("unsafe_cast")
return GdUnitResult.success((clazz as Object).get_class())
# extract name form full qualified class path
if clazz is String:
if ClassDB.class_exists(clazz):
return GdUnitResult.success(clazz)
var source_sript :Script = load(clazz)
var clazz_name :String = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript)
var clazz_name: String = clazz
if ClassDB.class_exists(clazz_name):
return GdUnitResult.success(clazz_name)
var source_script :GDScript = load(clazz_name)
clazz_name = GdScriptParser.new().get_class_name(source_script)
return GdUnitResult.success(to_pascal_case(clazz_name))
if is_primitive_type(clazz):
return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz)))
if is_script(clazz):
if clazz.resource_path.is_empty():
@warning_ignore("unsafe_cast")
if (clazz as Script).resource_path.is_empty():
var class_path := extract_class_name_from_class_path(extract_class_path(clazz))
return GdUnitResult.success(class_path);
return extract_class_name(clazz.resource_path)
# need to create an instance for a class typ the extract the class name
@warning_ignore("unsafe_method_access")
var instance :Variant = clazz.new()
if instance == null:
return GdUnitResult.error("Can't create a instance for class '%s'" % clazz)
return GdUnitResult.error("Can't create a instance for class '%s'" % str(clazz))
var result := extract_class_name(instance)
@warning_ignore("return_value_discarded")
GdUnitTools.free_instance(instance)
return result
@ -571,6 +611,7 @@ static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStr
var value :Variant = map.get(key)
if value is GDScript:
var class_path := extract_class_path(value)
@warning_ignore("return_value_discarded")
inner_classes.append(class_path[1])
return inner_classes
@ -649,6 +690,7 @@ static func default_value_by_type(type :int) -> Variant:
TYPE_NODE_PATH: return NodePath()
TYPE_RID: return RID()
TYPE_OBJECT: return null
TYPE_CALLABLE: return Callable()
TYPE_ARRAY: return []
TYPE_DICTIONARY: return {}
TYPE_PACKED_BYTE_ARRAY: return PackedByteArray()

View File

@ -16,6 +16,7 @@ func _init(major :int, minor :int, patch :int) -> void:
static func parse(value :String) -> GdUnit4Version:
var regex := RegEx.new()
@warning_ignore("return_value_discarded")
regex.compile("[a-zA-Z:,-]+")
var cleaned := regex.sub(value, "", true)
var parts := cleaned.split(".")
@ -27,8 +28,10 @@ static func parse(value :String) -> GdUnit4Version:
static func current() -> GdUnit4Version:
var config := ConfigFile.new()
@warning_ignore("return_value_discarded")
config.load('addons/gdUnit4/plugin.cfg')
return parse(config.get_value('plugin', 'version'))
@warning_ignore("unsafe_cast")
return parse(config.get_value('plugin', 'version') as String)
func equals(other :GdUnit4Version) -> bool:
@ -45,12 +48,13 @@ func is_greater(other :GdUnit4Version) -> bool:
static func init_version_label(label :Control) -> void:
var config := ConfigFile.new()
@warning_ignore("return_value_discarded")
config.load('addons/gdUnit4/plugin.cfg')
var version :String = config.get_value('plugin', 'version')
if label is RichTextLabel:
label.text = VERSION_PATTERN.replace('${version}', version)
(label as RichTextLabel).text = VERSION_PATTERN.replace('${version}', version)
else:
label.text = "gdUnit4 " + version
(label as Label).text = "gdUnit4 " + version
func _to_string() -> String:

View File

@ -37,12 +37,15 @@ static func check_leaked_instances() -> void:
# class_info = { "class_name": <>, "class_path" : <>}
static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray:
# store instance id
var clazz_name: String = class_info.get("class_name")
var source_code := template\
.replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\
.replace("${source_class}", class_info.get("class_name"))
.replace("${source_class}", clazz_name)
var lines := GdScriptParser.to_unix_format(source_code).split("\n")
# replace template class_name with Doubled<class> name and extends form source class
lines.insert(0, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_"))
@warning_ignore("return_value_discarded")
lines.insert(0, "class_name Doubled%s" % clazz_name.replace(".", "_"))
@warning_ignore("return_value_discarded")
lines.insert(1, extends_clazz(class_info))
# append Object interactions stuff
lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n"))
@ -74,16 +77,14 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P
push_error(result.error_message())
return PackedStringArray()
var class_descriptor :GdClassDescriptor = result.value()
while class_descriptor != null:
for func_descriptor in class_descriptor.functions():
if instance != null and not instance.has_method(func_descriptor.name()):
#prints("no virtual func implemented",clazz_name, func_descriptor.name() )
continue
if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()):
continue
doubled_source += func_doubler.double(func_descriptor)
functions.append(func_descriptor.name())
class_descriptor = class_descriptor.parent()
for func_descriptor in class_descriptor.functions():
if instance != null and not instance.has_method(func_descriptor.name()):
#prints("no virtual func implemented",clazz_name, func_descriptor.name() )
continue
if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()):
continue
doubled_source += func_doubler.double(func_descriptor, instance is CallableDoubler)
functions.append(func_descriptor.name())
# double regular class functions
var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path)
@ -103,7 +104,7 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P
#prints("no virtual func implemented",clazz_name, func_descriptor.name() )
continue
functions.append(func_descriptor.name())
doubled_source.append_array(func_doubler.double(func_descriptor))
doubled_source.append_array(func_doubler.double(func_descriptor, instance is CallableDoubler))
return doubled_source

View File

@ -23,6 +23,7 @@ static func create_temp_file(relative_path :String, file_name :String, mode := F
static func temp_dir() -> String:
if not DirAccess.dir_exists_absolute(GDUNIT_TEMP):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP)
return GDUNIT_TEMP
@ -30,6 +31,7 @@ static func temp_dir() -> String:
static func create_temp_dir(folder_name :String) -> String:
var new_folder := temp_dir() + "/" + folder_name
if not DirAccess.dir_exists_absolute(new_folder):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(new_folder)
return new_folder
@ -61,6 +63,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f
var source_dir := DirAccess.open(from_dir)
var dest_dir := DirAccess.open(to_dir)
if source_dir != null:
@warning_ignore("return_value_discarded")
source_dir.list_dir_begin()
var next := "."
@ -72,6 +75,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f
var dest := dest_dir.get_current_dir() + "/" + next
if source_dir.current_is_dir():
if recursive:
@warning_ignore("return_value_discarded")
copy_directory(source + "/", dest, recursive)
continue
var err := source_dir.copy(source, dest)
@ -88,6 +92,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f
static func delete_directory(path :String, only_content := false) -> void:
var dir := DirAccess.open(path)
if dir != null:
@warning_ignore("return_value_discarded")
dir.list_dir_begin()
var file_name := "."
while file_name != "":
@ -113,6 +118,7 @@ static func delete_path_index_lower_equals_than(path :String, prefix :String, in
if dir == null:
return 0
var deleted := 0
@warning_ignore("return_value_discarded")
dir.list_dir_begin()
var next := "."
while next != "":
@ -134,6 +140,7 @@ static func find_last_path_index(path :String, prefix :String) -> int:
if dir == null:
return 0
var last_iteration := 0
@warning_ignore("return_value_discarded")
dir.list_dir_begin()
var next := "."
while next != "":
@ -152,12 +159,14 @@ static func scan_dir(path :String) -> PackedStringArray:
if dir == null or not dir.dir_exists(path):
return PackedStringArray()
var content := PackedStringArray()
@warning_ignore("return_value_discarded")
dir.list_dir_begin()
var next := "."
while next != "":
next = dir.get_next()
if next.is_empty() or next == "." or next == "..":
continue
@warning_ignore("return_value_discarded")
content.append(next)
return content
@ -169,6 +178,7 @@ static func resource_as_array(resource_path :String) -> PackedStringArray:
return PackedStringArray()
var file_content := PackedStringArray()
while not file.eof_reached():
@warning_ignore("return_value_discarded")
file_content.append(file.get_line())
return file_content
@ -182,11 +192,12 @@ static func resource_as_string(resource_path :String) -> String:
static func make_qualified_path(path :String) -> String:
if not path.begins_with("res://"):
if path.begins_with("//"):
return path.replace("//", "res://")
if path.begins_with("/"):
return "res:/" + path
if path.begins_with("res://"):
return path
if path.begins_with("//"):
return path.replace("//", "res://")
if path.begins_with("/"):
return "res:/" + path
return path
@ -203,9 +214,11 @@ static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult:
for zip_entry in zip_entries:
var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "")
if zip_entry.ends_with("/"):
@warning_ignore("return_value_discarded")
DirAccess.make_dir_recursive_absolute(new_file_path)
continue
var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE)
file.store_buffer(zip.read_file(zip_entry))
@warning_ignore("return_value_discarded")
zip.close()
return GdUnitResult.success(dest_path)

View File

@ -5,13 +5,15 @@ extends RefCounted
static func verify(interaction_object :Object, interactions_times :int) -> Variant:
if not _is_mock_or_spy(interaction_object, "__verify"):
return interaction_object
@warning_ignore("unsafe_method_access")
return interaction_object.__do_verify_interactions(interactions_times)
static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert:
var __gd_assert :GdUnitAssert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("")
var __gd_assert := GdUnitAssertImpl.new("")
if not _is_mock_or_spy(interaction_object, "__verify"):
return __gd_assert.report_success()
@warning_ignore("unsafe_method_access")
var __summary :Dictionary = interaction_object.__verify_no_interactions()
if __summary.is_empty():
return __gd_assert.report_success()
@ -19,9 +21,10 @@ static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert:
static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAssert:
var __gd_assert :GdUnitAssert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("")
var __gd_assert := GdUnitAssertImpl.new("")
if not _is_mock_or_spy(interaction_object, "__verify_no_more_interactions"):
return __gd_assert
@warning_ignore("unsafe_method_access")
var __summary :Dictionary = interaction_object.__verify_no_more_interactions()
if __summary.is_empty():
return __gd_assert
@ -31,12 +34,14 @@ static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAss
static func reset(interaction_object :Object) -> Object:
if not _is_mock_or_spy(interaction_object, "__reset"):
return interaction_object
@warning_ignore("unsafe_method_access")
interaction_object.__reset_interactions()
return interaction_object
static func _is_mock_or_spy(interaction_object :Object, mock_function_signature :String) -> bool:
if interaction_object is GDScript and not interaction_object.get_script().has_script_method(mock_function_signature):
@warning_ignore("unsafe_cast")
if interaction_object is GDScript and not (interaction_object.get_script() as GDScript).has_method(mock_function_signature):
push_error("Error: You try to use a non mock or spy!")
return false
return true

View File

@ -1,4 +1,3 @@
const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
var __expected_interactions :int = -1
var __saved_interactions := Dictionary()
@ -47,8 +46,10 @@ func __verify_interactions(function_args :Array[Variant]) -> void:
__error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary)
else:
__error_message = GdAssertMessages.error_validate_interactions(__summary, __expected_summary)
@warning_ignore("return_value_discarded")
__gd_assert.report_error(__error_message)
else:
@warning_ignore("return_value_discarded")
__gd_assert.report_success()
__expected_interactions = -1

View File

@ -31,6 +31,10 @@ func value() -> Variant:
return _value
func value_as_string() -> String:
return _value
func value_set() -> PackedStringArray:
return _value_set
@ -44,11 +48,11 @@ func set_value(p_value :Variant) -> void:
TYPE_STRING:
_value = str(p_value)
TYPE_BOOL:
_value = bool(p_value)
_value = convert(p_value, TYPE_BOOL)
TYPE_INT:
_value = int(p_value)
_value = convert(p_value, TYPE_INT)
TYPE_FLOAT:
_value = float(p_value)
_value = convert(p_value, TYPE_FLOAT)
_:
_value = p_value

View File

@ -8,7 +8,7 @@ enum {
EMPTY
}
var _state :Variant
var _state: int
var _warn_message := ""
var _error_message := ""
var _value :Variant = null
@ -66,6 +66,10 @@ func value() -> Variant:
return _value
func value_as_string() -> String:
return _value
func or_else(p_value :Variant) -> Variant:
if not is_success():
return p_value
@ -97,7 +101,8 @@ static func serialize(result :GdUnitResult) -> Dictionary:
static func deserialize(config :Dictionary) -> GdUnitResult:
var result := GdUnitResult.new()
result._value = str_to_var(config.get("value", ""))
var cfg_value: String = config.get("value", "")
result._value = str_to_var(cfg_value)
result._warn_message = config.get("warn_msg", null)
result._error_message = config.get("err_msg", null)
result._state = config.get("state")

View File

@ -1,8 +1,5 @@
extends Node
signal sync_rpc_id_result_received
@onready var _client :GdUnitTcpClient = $GdUnitTcpClient
@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new()
@ -39,7 +36,9 @@ func _ready() -> void:
push_error(config_result.error_message())
_state = EXIT
return
@warning_ignore("return_value_discarded")
_client.connect("connection_failed", _on_connection_failed)
@warning_ignore("return_value_discarded")
GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
var result := _client.start("127.0.0.1", _config.server_port())
if result.is_error():
@ -78,11 +77,14 @@ func _process(_delta :float) -> void:
# process next test suite
set_process(false)
var test_suite :Node = _test_suites_to_process.pop_front()
@warning_ignore("unsafe_method_access")
if _cs_executor != null and _cs_executor.IsExecutable(test_suite):
@warning_ignore("unsafe_method_access")
_cs_executor.Execute(test_suite)
@warning_ignore("unsafe_property_access")
await _cs_executor.ExecutionCompleted
else:
await _executor.execute(test_suite)
await _executor.execute(test_suite as GdUnitTestSuite)
set_process(true)
STOP:
_state = EXIT
@ -136,6 +138,7 @@ func _do_filter_test_case(test_suite :Node, test_case :Node, included_tests :Pac
# we have a paremeterized test selection
if test_meta.size() > 1:
var test_param_index := test_meta[1]
@warning_ignore("unsafe_method_access")
test_case.set_test_parameter_index(test_param_index.to_int())
return
# the test is filtered out

View File

@ -38,6 +38,7 @@ func server_port() -> int:
return _config.get(SERVER_PORT, -1)
@warning_ignore("return_value_discarded")
func self_test() -> GdUnitRunnerConfig:
add_test_suite("res://addons/gdUnit4/test/")
add_test_suite("res://addons/gdUnit4/mono/test/")
@ -52,6 +53,7 @@ func add_test_suite(p_resource_path :String) -> GdUnitRunnerConfig:
func add_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig:
for resource_path_ in resource_paths:
@warning_ignore("return_value_discarded")
add_test_suite(resource_path_)
return self
@ -60,8 +62,10 @@ func add_test_case(p_resource_path :String, test_name :StringName, test_param_in
var to_execute_ := to_execute()
var test_cases :PackedStringArray = to_execute_.get(p_resource_path, PackedStringArray())
if test_param_index != -1:
@warning_ignore("return_value_discarded")
test_cases.append("%s:%d" % [test_name, test_param_index])
else:
@warning_ignore("return_value_discarded")
test_cases.append(test_name)
to_execute_[p_resource_path] = test_cases
return self
@ -72,18 +76,22 @@ func add_test_case(p_resource_path :String, test_name :StringName, test_param_in
# '/path/path', res://path/path', 'res://path/path/testsuite.gd' or 'testsuite'
# 'res://path/path/testsuite.gd:test_case' or 'testsuite:test_case'
func skip_test_suite(value :StringName) -> GdUnitRunnerConfig:
var parts :Array = GdUnitFileAccess.make_qualified_path(value).rsplit(":")
var parts: PackedStringArray = GdUnitFileAccess.make_qualified_path(value).rsplit(":")
if parts[0] == "res":
parts.pop_front()
parts.remove_at(0)
parts[0] = GdUnitFileAccess.make_qualified_path(parts[0])
match parts.size():
1: skipped()[parts[0]] = PackedStringArray()
2: skip_test_case(parts[0], parts[1])
1:
skipped()[parts[0]] = PackedStringArray()
2:
@warning_ignore("return_value_discarded")
skip_test_case(parts[0], parts[1])
return self
func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig:
for resource_path_ in resource_paths:
@warning_ignore("return_value_discarded")
skip_test_suite(resource_path_)
return self
@ -91,6 +99,7 @@ func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig:
func skip_test_case(p_resource_path :String, test_name :StringName) -> GdUnitRunnerConfig:
var to_ignore := skipped()
var test_cases :PackedStringArray = to_ignore.get(p_resource_path, PackedStringArray())
@warning_ignore("return_value_discarded")
test_cases.append(test_name)
to_ignore[p_resource_path] = test_cases
return self
@ -129,19 +138,20 @@ func load_config(path :String = CONFIG_FILE) -> GdUnitResult:
var error := test_json_conv.parse(content)
if error != OK:
return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
_config = test_json_conv.get_data() as Dictionary
_config = test_json_conv.get_data()
if not _config.has(VERSION):
return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
fix_value_types()
return GdUnitResult.success(path)
@warning_ignore("unsafe_cast")
func fix_value_types() -> void:
# fix float value to int json stores all numbers as float
var server_port_ :int = _config.get(SERVER_PORT, -1)
_config[SERVER_PORT] = server_port_
convert_Array_to_PackedStringArray(_config[INCLUDED])
convert_Array_to_PackedStringArray(_config[SKIPPED])
convert_Array_to_PackedStringArray(_config[INCLUDED] as Dictionary)
convert_Array_to_PackedStringArray(_config[SKIPPED] as Dictionary)
func convert_Array_to_PackedStringArray(data :Dictionary) -> void:

View File

@ -3,7 +3,7 @@ class_name GdUnitSceneRunnerImpl
extends GdUnitSceneRunner
var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
var GdUnitFuncAssertImpl: GDScript = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
# mapping of mouse buttons and his masks
@ -19,29 +19,36 @@ const MAP_MOUSE_BUTTON_MASKS := {
}
var _is_disposed := false
var _current_scene :Node = null
var _awaiter :GdUnitAwaiter = GdUnitAwaiter.new()
var _verbose :bool
var _simulate_start_time :LocalTime
var _last_input_event :InputEvent = null
var _current_scene: Node = null
var _awaiter: GdUnitAwaiter = GdUnitAwaiter.new()
var _verbose: bool
var _simulate_start_time: LocalTime
var _last_input_event: InputEvent = null
var _mouse_button_on_press := []
var _key_on_press := []
var _action_on_press := []
var _curent_mouse_position :Vector2
var _curent_mouse_position: Vector2
# holds the touch position for each touch index
# { index: int = position: Vector2}
var _current_touch_position: Dictionary = {}
# holds the curretn touch drag position
var _current_touch_drag_position: Vector2 = Vector2.ZERO
# time factor settings
var _time_factor := 1.0
var _saved_iterations_per_second :float
var _saved_iterations_per_second: float
var _scene_auto_free := false
func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> void:
func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> void:
_verbose = p_verbose
_saved_iterations_per_second = Engine.get_physics_ticks_per_second()
@warning_ignore("return_value_discarded")
set_time_factor(1)
# handle scene loading by resource path
if typeof(p_scene) == TYPE_STRING:
if !ResourceLoader.exists(p_scene):
@warning_ignore("unsafe_cast")
if !ResourceLoader.exists(p_scene as String):
if not p_hide_push_errors:
push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene)
return
@ -49,7 +56,8 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo
if not p_hide_push_errors:
push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene)
return
_current_scene = load(p_scene).instantiate()
@warning_ignore("unsafe_cast")
_current_scene = (load(p_scene as String) as PackedScene).instantiate()
_scene_auto_free = true
else:
# verify we have a node instance
@ -62,10 +70,14 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo
if not p_hide_push_errors:
push_error("GdUnitSceneRunner: Scene must be not null!")
return
_scene_tree().root.add_child(_current_scene)
# do finally reset all open input events when the scene is removed
@warning_ignore("return_value_discarded")
_scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void:
if child == _current_scene:
# we need to disable the processing to avoid input flush buffer errors
_current_scene.process_mode = Node.PROCESS_MODE_DISABLED
_reset_input_to_default()
)
_simulate_start_time = LocalTime.now()
@ -78,7 +90,7 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo
max_iteration_to_wait += 1
func _notification(what :int) -> void:
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE and is_instance_valid(self):
# reset time factor to normal
__deactivate_time_factor()
@ -89,45 +101,52 @@ func _notification(what :int) -> void:
_current_scene.free()
_is_disposed = true
_current_scene = null
# we hide the scene/main window after runner is finished
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
func _scene_tree() -> SceneTree:
return Engine.get_main_loop() as SceneTree
func simulate_action_pressed(action :String) -> GdUnitSceneRunner:
@warning_ignore("return_value_discarded")
func simulate_action_pressed(action: String) -> GdUnitSceneRunner:
simulate_action_press(action)
simulate_action_release(action)
return self
func simulate_action_press(action :String) -> GdUnitSceneRunner:
func simulate_action_press(action: String) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventAction.new()
event.pressed = true
event.action = action
if Engine.get_version_info().hex >= 0x40300:
@warning_ignore("unsafe_property_access")
event.event_index = InputMap.get_actions().find(action)
_action_on_press.append(action)
return _handle_input_event(event)
func simulate_action_release(action :String) -> GdUnitSceneRunner:
func simulate_action_release(action: String) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventAction.new()
event.pressed = false
event.action = action
if Engine.get_version_info().hex >= 0x40300:
@warning_ignore("unsafe_property_access")
event.event_index = InputMap.get_actions().find(action)
_action_on_press.erase(action)
return _handle_input_event(event)
func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
@warning_ignore("return_value_discarded")
func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
simulate_key_press(key_code, shift_pressed, ctrl_pressed)
await _scene_tree().process_frame
simulate_key_release(key_code, shift_pressed, ctrl_pressed)
return self
func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventKey.new()
event.pressed = true
@ -141,7 +160,7 @@ func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := f
return _handle_input_event(event)
func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventKey.new()
event.pressed = false
@ -155,7 +174,11 @@ func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed :=
return _handle_input_event(event)
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
func set_mouse_pos(pos: Vector2) -> GdUnitSceneRunner:
return set_mouse_position(pos)
func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner:
var event := InputEventMouseMotion.new()
event.position = pos
event.global_position = get_global_mouse_position()
@ -165,7 +188,7 @@ func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
func get_mouse_position() -> Vector2:
if _last_input_event is InputEventMouse:
return _last_input_event.position
return (_last_input_event as InputEventMouse).position
var current_scene := scene()
if current_scene != null:
return current_scene.get_viewport().get_mouse_position()
@ -173,19 +196,20 @@ func get_mouse_position() -> Vector2:
func get_global_mouse_position() -> Vector2:
return Engine.get_main_loop().root.get_mouse_position()
return (Engine.get_main_loop() as SceneTree).root.get_mouse_position()
func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner:
var event := InputEventMouseMotion.new()
event.position = pos
event.relative = pos - get_mouse_position()
event.position = position
event.relative = position - get_mouse_position()
event.global_position = get_global_mouse_position()
_apply_input_mouse_mask(event)
_apply_input_modifiers(event)
return _handle_input_event(event)
@warning_ignore("return_value_discarded")
func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
var tween := _scene_tree().create_tween()
_curent_mouse_position = get_mouse_position()
@ -199,6 +223,7 @@ func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_ty
return self
@warning_ignore("return_value_discarded")
func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
var tween := _scene_tree().create_tween()
_curent_mouse_position = get_mouse_position()
@ -211,36 +236,166 @@ func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_ty
return self
func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
simulate_mouse_button_press(buttonIndex, double_click)
simulate_mouse_button_release(buttonIndex)
@warning_ignore("return_value_discarded")
func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
simulate_mouse_button_press(button_index, double_click)
simulate_mouse_button_release(button_index)
return self
func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
var event := InputEventMouseButton.new()
event.button_index = buttonIndex
event.button_index = button_index
event.pressed = true
event.double_click = double_click
_apply_input_mouse_position(event)
_apply_input_mouse_mask(event)
_apply_input_modifiers(event)
_mouse_button_on_press.append(buttonIndex)
_mouse_button_on_press.append(button_index)
return _handle_input_event(event)
func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner:
func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner:
var event := InputEventMouseButton.new()
event.button_index = buttonIndex
event.button_index = button_index
event.pressed = false
_apply_input_mouse_position(event)
_apply_input_mouse_mask(event)
_apply_input_modifiers(event)
_mouse_button_on_press.erase(buttonIndex)
_mouse_button_on_press.erase(button_index)
return _handle_input_event(event)
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
@warning_ignore("return_value_discarded")
func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
simulate_screen_touch_press(index, position, double_tap)
simulate_screen_touch_release(index)
return self
@warning_ignore("return_value_discarded")
func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
if is_emulate_mouse_from_touch():
# we need to simulate in addition to the touch the mouse events
set_mouse_pos(position)
simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
# push touch press event at position
var event := InputEventScreenTouch.new()
event.window_id = scene().get_window().get_window_id()
event.index = index
event.position = position
event.double_tap = double_tap
event.pressed = true
_current_scene.get_viewport().push_input(event)
# save current drag position by index
_current_touch_position[index] = position
return self
@warning_ignore("return_value_discarded")
func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner:
if is_emulate_mouse_from_touch():
# we need to simulate in addition to the touch the mouse events
simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
# push touch release event at position
var event := InputEventScreenTouch.new()
event.window_id = scene().get_window().get_window_id()
event.index = index
event.position = get_screen_touch_drag_position(index)
event.pressed = false
event.double_tap = (_last_input_event as InputEventScreenTouch).double_tap if _last_input_event is InputEventScreenTouch else double_tap
_current_scene.get_viewport().push_input(event)
return self
func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
var current_position: Vector2 = _current_touch_position[index]
return await _do_touch_drag_at(index, current_position + relative, time, trans_type)
func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
return await _do_touch_drag_at(index, position, time, trans_type)
@warning_ignore("return_value_discarded")
func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
simulate_screen_touch_press(index, position)
return await _do_touch_drag_at(index, drop_position, time, trans_type)
@warning_ignore("return_value_discarded")
func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner:
if is_emulate_mouse_from_touch():
simulate_mouse_move(position)
var event := InputEventScreenDrag.new()
event.window_id = scene().get_window().get_window_id()
event.index = index
event.position = position
event.relative = _get_screen_touch_drag_position_or_default(index, position) - position
event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
event.pressure = 1.0
_current_touch_position[index] = position
_current_scene.get_viewport().push_input(event)
return self
func get_screen_touch_drag_position(index: int) -> Vector2:
if _current_touch_position.has(index):
return _current_touch_position[index]
push_error("No touch drag position for index '%d' is set!" % index)
return Vector2.ZERO
func is_emulate_mouse_from_touch() -> bool:
return ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch", true)
func _get_screen_touch_drag_position_or_default(index: int, default_position: Vector2) -> Vector2:
if _current_touch_position.has(index):
return _current_touch_position[index]
return default_position
@warning_ignore("return_value_discarded")
func _do_touch_drag_at(index: int, drag_position: Vector2, time: float, trans_type: Tween.TransitionType) -> GdUnitSceneRunner:
# start draging
var event := InputEventScreenDrag.new()
event.window_id = scene().get_window().get_window_id()
event.index = index
event.position = get_screen_touch_drag_position(index)
event.pressure = 1.0
_current_touch_drag_position = event.position
var tween := _scene_tree().create_tween()
tween.tween_property(self, "_current_touch_drag_position", drag_position, time).set_trans(trans_type)
tween.play()
while not _current_touch_drag_position.is_equal_approx(drag_position):
if is_emulate_mouse_from_touch():
# we need to simulate in addition to the drag the mouse move events
simulate_mouse_move(event.position)
# send touche drag event to new position
event.relative = _current_touch_drag_position - event.position
event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
event.position = _current_touch_drag_position
_current_scene.get_viewport().push_input(event)
await _scene_tree().process_frame
# finaly drop it
if is_emulate_mouse_from_touch():
simulate_mouse_move(drag_position)
simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
var touch_drop_event := InputEventScreenTouch.new()
touch_drop_event.window_id = event.window_id
touch_drop_event.index = event.index
touch_drop_event.position = drag_position
touch_drop_event.pressed = false
_current_scene.get_viewport().push_input(touch_drop_event)
await _scene_tree().process_frame
return self
func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner:
_time_factor = min(9.0, time_factor)
__activate_time_factor()
__print("set time factor: %f" % _time_factor)
@ -248,7 +403,7 @@ func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
return self
func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner:
var time_shift_frames :int = max(1, frames / _time_factor)
for frame in time_shift_frames:
if delta_milli == -1:
@ -259,74 +414,73 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner:
func simulate_until_signal(
signal_name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
signal_name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> GdUnitSceneRunner:
var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000)
return self
func simulate_until_object_signal(
source :Object,
signal_name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> GdUnitSceneRunner:
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
source: Object,
signal_name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> GdUnitSceneRunner:
var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000)
return self
func await_func(func_name :String, args := []) -> GdUnitFuncAssert:
func await_func(func_name: String, args := []) -> GdUnitFuncAssert:
return GdUnitFuncAssertImpl.new(scene(), func_name, args)
func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFuncAssert:
func await_func_on(instance: Object, func_name: String, args := []) -> GdUnitFuncAssert:
return GdUnitFuncAssertImpl.new(instance, func_name, args)
func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void:
func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void:
await _awaiter.await_signal_on(scene(), signal_name, args, timeout)
func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void:
func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void:
await _awaiter.await_signal_on(source, signal_name, args, timeout)
# maximizes the window to bring the scene visible
func maximize_view() -> GdUnitSceneRunner:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_move_to_foreground()
return self
func _property_exists(name :String) -> bool:
func _property_exists(name: String) -> bool:
return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name)
func get_property(name :String) -> Variant:
func get_property(name: String) -> Variant:
if not _property_exists(name):
return "The property '%s' not exist checked loaded scene." % name
return scene().get(name)
func set_property(name :String, value :Variant) -> bool:
func set_property(name: String, value: Variant) -> bool:
if not _property_exists(name):
push_error("The property named '%s' cannot be set, it does not exist!" % name)
return false;
@ -335,24 +489,24 @@ func set_property(name :String, value :Variant) -> bool:
func invoke(
name :String,
arg0 :Variant = NO_ARG,
arg1 :Variant = NO_ARG,
arg2 :Variant = NO_ARG,
arg3 :Variant = NO_ARG,
arg4 :Variant = NO_ARG,
arg5 :Variant = NO_ARG,
arg6 :Variant = NO_ARG,
arg7 :Variant = NO_ARG,
arg8 :Variant = NO_ARG,
arg9 :Variant = NO_ARG) -> Variant:
var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
name: String,
arg0: Variant = NO_ARG,
arg1: Variant = NO_ARG,
arg2: Variant = NO_ARG,
arg3: Variant = NO_ARG,
arg4: Variant = NO_ARG,
arg5: Variant = NO_ARG,
arg6: Variant = NO_ARG,
arg7: Variant = NO_ARG,
arg8: Variant = NO_ARG,
arg9: Variant = NO_ARG) -> Variant:
var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
if scene().has_method(name):
return scene().callv(name, args)
return "The method '%s' not exist checked loaded scene." % name
func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node:
func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node:
return scene().find_child(name, recursive, owned)
@ -377,37 +531,40 @@ func __deactivate_time_factor() -> void:
# copy over current active modifiers
func _apply_input_modifiers(event :InputEvent) -> void:
func _apply_input_modifiers(event: InputEvent) -> void:
if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers:
event.meta_pressed = event.meta_pressed or _last_input_event.meta_pressed
event.alt_pressed = event.alt_pressed or _last_input_event.alt_pressed
event.shift_pressed = event.shift_pressed or _last_input_event.shift_pressed
event.ctrl_pressed = event.ctrl_pressed or _last_input_event.ctrl_pressed
var last_input_event := _last_input_event as InputEventWithModifiers
var _event := event as InputEventWithModifiers
_event.meta_pressed = _event.meta_pressed or last_input_event.meta_pressed
_event.alt_pressed = _event.alt_pressed or last_input_event.alt_pressed
_event.shift_pressed = _event.shift_pressed or last_input_event.shift_pressed
_event.ctrl_pressed = _event.ctrl_pressed or last_input_event.ctrl_pressed
# this line results into reset the control_pressed state!!!
#event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap
# copy over current active mouse mask and combine with curren mask
func _apply_input_mouse_mask(event :InputEvent) -> void:
func _apply_input_mouse_mask(event: InputEvent) -> void:
# first apply last mask
if _last_input_event is InputEventMouse and event is InputEventMouse:
event.button_mask |= _last_input_event.button_mask
(event as InputEventMouse).button_mask |= (_last_input_event as InputEventMouse).button_mask
if event is InputEventMouseButton:
var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(event.get_button_index(), 0)
if event.is_pressed():
event.button_mask |= button_mask
var _event := event as InputEventMouseButton
var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(_event.get_button_index(), 0)
if _event.is_pressed():
_event.button_mask |= button_mask
else:
event.button_mask ^= button_mask
_event.button_mask ^= button_mask
# copy over last mouse position if need
func _apply_input_mouse_position(event :InputEvent) -> void:
func _apply_input_mouse_position(event: InputEvent) -> void:
if _last_input_event is InputEventMouse and event is InputEventMouseButton:
event.position = _last_input_event.position
(event as InputEventMouseButton).position = (_last_input_event as InputEventMouse).position
## handle input action via Input modifieres
func _handle_actions(event :InputEventAction) -> bool:
func _handle_actions(event: InputEventAction) -> bool:
if not InputMap.event_is_action(event, event.action, true):
return false
__print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()])
@ -419,20 +576,23 @@ func _handle_actions(event :InputEventAction) -> bool:
# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work
func _handle_input_event(event :InputEvent) -> GdUnitSceneRunner:
@warning_ignore("return_value_discarded")
func _handle_input_event(event: InputEvent) -> GdUnitSceneRunner:
if event is InputEventMouse:
Input.warp_mouse(event.position)
Input.warp_mouse((event as InputEventMouse).position as Vector2)
Input.parse_input_event(event)
if event is InputEventAction:
_handle_actions(event)
_handle_actions(event as InputEventAction)
Input.flush_buffered_events()
var current_scene := scene()
if is_instance_valid(current_scene):
# do not flush events if node processing disabled otherwise we run into errors at tree removed
if _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
Input.flush_buffered_events()
__print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()])
if(current_scene.has_method("_gui_input")):
current_scene._gui_input(event)
(current_scene as Control)._gui_input(event)
if(current_scene.has_method("_unhandled_input")):
current_scene._unhandled_input(event)
current_scene.get_viewport().set_input_as_handled()
@ -442,6 +602,7 @@ func _handle_input_event(event :InputEvent) -> GdUnitSceneRunner:
return self
@warning_ignore("return_value_discarded")
func _reset_input_to_default() -> void:
# reset all mouse button to inital state if need
for m_button :int in _mouse_button_on_press.duplicate():
@ -459,11 +620,12 @@ func _reset_input_to_default() -> void:
simulate_action_release(action)
_action_on_press.clear()
Input.flush_buffered_events()
if is_instance_valid(_current_scene) and _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
Input.flush_buffered_events()
_last_input_event = null
func __print(message :String) -> void:
func __print(message: String) -> void:
if _verbose:
prints(message)

View File

@ -14,8 +14,10 @@ const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes"
const GROUP_TEST = COMMON_SETTINGS + "/test"
const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds"
const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder"
const TEST_SITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention"
const TEST_SUITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention"
const TEST_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery"
const TEST_FLAKY_CHECK = GROUP_TEST + "/flaky_check_enable"
const TEST_FLAKY_MAX_RETRIES = GROUP_TEST + "/flaky_max_retries"
# Report Setiings
@ -81,7 +83,7 @@ const DEFAULT_TEST_TIMEOUT :int = 60*5
const DEFAULT_TEST_LOOKUP_FOLDER := "test"
# help texts
const HELP_TEST_LOOKUP_FOLDER := "Sets the subfolder for the search/creation of test suites. (leave empty to use source folder)"
const HELP_TEST_LOOKUP_FOLDER := "Subfolder where test suites are located (or empty to use source folder directly)"
enum NAMING_CONVENTIONS {
AUTO_DETECT,
@ -90,29 +92,36 @@ enum NAMING_CONVENTIONS {
}
const _VALUE_SET_SEPARATOR = "\f" # ASCII Form-feed character (AKA page break)
static func setup() -> void:
create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Enables/Disables the update notification checked startup.")
create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Sets the server connection timeout in minutes.")
create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Sets the test case runtime timeout in seconds.")
create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Show notification if new gdUnit4 version is found")
# test settings
create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Server connection timeout in minutes")
create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Test case runtime timeout in seconds")
create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER)
create_property_if_need(TEST_SITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Sets test-suite genrate script name convention.", NAMING_CONVENTIONS.keys())
create_property_if_need(TEST_DISCOVER_ENABLED, false, "Enables/Disables the automatic detection of tests by finding tests in test lookup folders at runtime.")
create_property_if_need(REPORT_PUSH_ERRORS, false, "Enables/Disables report of push_error() as failure!")
create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Enables/Disables report of script errors as failure!")
create_property_if_need(REPORT_ORPHANS, true, "Enables/Disables orphan reporting.")
create_property_if_need(REPORT_ASSERT_ERRORS, true, "Enables/Disables error reporting checked asserts.")
create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Enables/Disables warning reporting checked asserts")
create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Enabled/disabled number values will be compared strictly by type. (real vs int)")
create_property_if_need(TEST_SUITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention to use when generating testsuites", NAMING_CONVENTIONS.keys())
create_property_if_need(TEST_DISCOVER_ENABLED, false, "Automatically detect new tests in test lookup folders at runtime")
create_property_if_need(TEST_FLAKY_CHECK, false, "Rerun tests on failure and mark them as FLAKY")
create_property_if_need(TEST_FLAKY_MAX_RETRIES, 3, "Sets the number of retries for rerunning a flaky test")
# report settings
create_property_if_need(REPORT_PUSH_ERRORS, false, "Report push_error() as failure")
create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Report script errors as failure")
create_property_if_need(REPORT_ORPHANS, true, "Report orphaned nodes after tests finish")
create_property_if_need(REPORT_ASSERT_ERRORS, true, "Report assertion failures as errors")
create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Report assertion failures as warnings")
create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Compare number values strictly by type (real vs int)")
# inspector
create_property_if_need(INSPECTOR_NODE_COLLAPSE, true,
"Enables/Disables that the testsuite node is closed after a successful test run.")
"Close testsuite node after a successful test run.")
create_property_if_need(INSPECTOR_TREE_VIEW_MODE, GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE,
"Sets the inspector panel presentation.", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys())
"Inspector panel presentation mode", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys())
create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED,
"Sets the inspector panel presentation.", GdUnitInspectorTreeConstants.SORT_MODE.keys())
"Inspector panel sorting mode", GdUnitInspectorTreeConstants.SORT_MODE.keys())
create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false,
"Shows/Hides the 'Run overall Tests' button in the inspector toolbar.")
create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Defines the test suite template")
"Show 'Run overall Tests' button in the inspector toolbar")
create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use")
create_shortcut_properties_if_need()
migrate_properties()
@ -129,17 +138,17 @@ static func migrate_properties() -> void:
static func create_shortcut_properties_if_need() -> void:
# inspector
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun of the last tests performed.")
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun of the last tests performed (Debug).")
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug).")
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stops the current test execution.")
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun the most recently executed tests")
create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun the most recently executed tests (Debug mode)")
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug mode)")
create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stop the current test execution")
# script editor
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Runs the currently selected test.")
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Runs the currently selected test (Debug).")
create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Creates a new test case for the currently selected function.")
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Run the currently selected test")
create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Run the currently selected test (Debug mode).")
create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Create a new test case for the currently selected function")
# filesystem
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file.")
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file (Debug).")
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file")
create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file (Debug)")
static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void:
@ -148,7 +157,7 @@ static func create_property_if_need(name :String, default :Variant, help :="", v
ProjectSettings.set_setting(name, default)
ProjectSettings.set_initial_value(name, default)
help += "" if value_set.is_empty() else " %s" % value_set
help = help if value_set.is_empty() else "%s%s%s" % [help, _VALUE_SET_SEPARATOR, value_set]
set_help(name, default, help)
@ -175,6 +184,7 @@ static func is_update_notification_enabled() -> bool:
static func set_update_notification(enable :bool) -> void:
ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable)
@warning_ignore("return_value_discarded")
ProjectSettings.save()
@ -185,6 +195,7 @@ static func get_log_path() -> String:
static func set_log_path(path :String) -> void:
ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true)
ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path)
@warning_ignore("return_value_discarded")
ProjectSettings.save()
@ -261,6 +272,14 @@ static func is_test_discover_enabled() -> bool:
return get_setting(TEST_DISCOVER_ENABLED, false)
static func is_test_flaky_check_enabled() -> bool:
return get_setting(TEST_FLAKY_CHECK, false)
static func get_flaky_max_retries() -> int:
return get_setting(TEST_FLAKY_MAX_RETRIES, 3)
static func set_test_discover_enabled(enable :bool) -> void:
var property := get_property(TEST_DISCOVER_ENABLED)
property.set_value(enable)
@ -271,29 +290,34 @@ static func is_log_enabled() -> bool:
return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE)
static func list_settings(category :String) -> Array[GdUnitProperty]:
var settings :Array[GdUnitProperty] = []
static func list_settings(category: String) -> Array[GdUnitProperty]:
var settings: Array[GdUnitProperty] = []
for property in ProjectSettings.get_property_list():
var property_name :String = property["name"]
if property_name.begins_with(category):
var value :Variant = ProjectSettings.get_setting(property_name)
var default :Variant = ProjectSettings.property_get_revert(property_name)
var help :String = property["hint_string"]
var value_set := extract_value_set_from_help(help)
settings.append(GdUnitProperty.new(property_name, property["type"], value, default, help, value_set))
settings.append(build_property(property_name, property))
return settings
static func extract_value_set_from_help(value :String) -> PackedStringArray:
var split_value := value.split(_VALUE_SET_SEPARATOR)
if not split_value.size() > 1:
return PackedStringArray()
var regex := RegEx.new()
@warning_ignore("return_value_discarded")
regex.compile("\\[(.+)\\]")
var matches := regex.search_all(value)
var matches := regex.search_all(split_value[1])
if matches.is_empty():
return PackedStringArray()
var values :String = matches[0].get_string(1)
var values: String = matches[0].get_string(1)
return values.replacen(" ", "").replacen("\"", "").split(",", false)
static func extract_help_text(value :String) -> String:
return value.split(_VALUE_SET_SEPARATOR)[0]
static func update_property(property :GdUnitProperty) -> Variant:
var current_value :Variant = ProjectSettings.get_setting(property.name())
if current_value != property.value():
@ -315,7 +339,7 @@ static func reset_property(property :GdUnitProperty) -> void:
static func validate_property_value(property :GdUnitProperty) -> Variant:
match property.name():
TEST_LOOKUP_FOLDER:
return validate_lookup_folder(property.value())
return validate_lookup_folder(property.value_as_string())
_: return null
@ -349,14 +373,19 @@ static func get_property(name :String) -> GdUnitProperty:
for property in ProjectSettings.get_property_list():
var property_name :String = property["name"]
if property_name == name:
var value :Variant = ProjectSettings.get_setting(property_name)
var default :Variant = ProjectSettings.property_get_revert(property_name)
var help :String = property["hint_string"]
var value_set := extract_value_set_from_help(help)
return GdUnitProperty.new(property_name, property["type"], value, default, help, value_set)
return build_property(name, property)
return null
static func build_property(property_name: String, property: Dictionary) -> GdUnitProperty:
var value: Variant = ProjectSettings.get_setting(property_name)
var value_type: int = property["type"]
var default: Variant = ProjectSettings.property_get_revert(property_name)
var help: String = property["hint_string"]
var value_set := extract_value_set_from_help(help)
return GdUnitProperty.new(property_name, value_type, value, default, extract_help_text(help), value_set)
static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void:
var property := get_property(old_property)
if property == null:
@ -371,8 +400,10 @@ static func migrate_property(old_property :String, new_property :String, default
static func dump_to_tmp() -> void:
@warning_ignore("return_value_discarded")
ProjectSettings.save_custom("user://project_settings.godot")
static func restore_dump_from_tmp() -> void:
@warning_ignore("return_value_discarded")
DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot")

View File

@ -41,12 +41,15 @@ func elapsed_time() -> float:
func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant:
# register checked signal to wait for
@warning_ignore("return_value_discarded")
source.connect(signal_name, _on_signal_emmited)
# install timeout timer
var scene_tree := Engine.get_main_loop() as SceneTree
var timer := Timer.new()
Engine.get_main_loop().root.add_child(timer)
scene_tree.root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
@warning_ignore("return_value_discarded")
timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED)
timer.start(_timeout_millis * 0.001 * Engine.get_time_scale())
@ -61,12 +64,13 @@ func on_signal(source :Object, signal_name :String, expected_signal_args :Array)
value = [value]
if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args):
break
await Engine.get_main_loop().process_frame
await scene_tree.process_frame
source.disconnect(signal_name, _on_signal_emmited)
_time_left = timer.time_left
await Engine.get_main_loop().process_frame
if value is Array and value.size() == 1:
await scene_tree.process_frame
@warning_ignore("unsafe_cast")
if value is Array and (value as Array).size() == 1:
return value[0]
return value

View File

@ -30,8 +30,8 @@ func register_emitter(emitter :Object) -> void:
return
_collected_signals[emitter] = Dictionary()
# connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections.
if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter):
emitter.tree_exiting.connect(unregister_emitter.bind(emitter))
if emitter is Node and !(emitter as Node).tree_exiting.is_connected(unregister_emitter):
(emitter as Node).tree_exiting.connect(unregister_emitter.bind(emitter))
# connect to all signals of the emitter we want to collect
for signal_def in emitter.get_signal_list():
var signal_name :String = signal_def["name"]
@ -54,6 +54,7 @@ func unregister_emitter(emitter :Object) -> void:
var signal_name :String = signal_def["name"]
if emitter.is_connected(signal_name, _on_signal_emmited):
emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name))
@warning_ignore("return_value_discarded")
_collected_signals.erase(emitter)
@ -77,7 +78,8 @@ func _on_signal_emmited(
var emitter :Object = signal_args.pop_back()
#prints("_on_signal_emmited:", emitter, signal_name, signal_args)
if is_signal_collecting(emitter, signal_name):
_collected_signals[emitter][signal_name].append(signal_args)
@warning_ignore("unsafe_cast")
(_collected_signals[emitter][signal_name] as Array).append(signal_args)
func reset_received_signals(emitter :Object, signal_name: String, signal_args :Array) -> void:
@ -85,12 +87,14 @@ func reset_received_signals(emitter :Object, signal_name: String, signal_args :A
if _collected_signals.has(emitter):
var signals_by_emitter :Dictionary = _collected_signals[emitter]
if signals_by_emitter.has(signal_name):
_collected_signals[emitter][signal_name].erase(signal_args)
@warning_ignore("unsafe_cast")
(_collected_signals[emitter][signal_name] as Array).erase(signal_args)
#_debug_signal_list("after claer");
func is_signal_collecting(emitter :Object, signal_name :String) -> bool:
return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name)
@warning_ignore("unsafe_cast")
return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name)
func match(emitter :Object, signal_name :String, args :Array) -> bool:

View File

@ -1,17 +1,24 @@
class_name GdUnitSignals
extends RefCounted
@warning_ignore("unused_signal")
signal gdunit_client_connected(client_id :int)
@warning_ignore("unused_signal")
signal gdunit_client_disconnected(client_id :int)
@warning_ignore("unused_signal")
signal gdunit_client_terminated()
@warning_ignore("unused_signal")
signal gdunit_event(event :GdUnitEvent)
@warning_ignore("unused_signal")
signal gdunit_event_debug(event :GdUnitEvent)
@warning_ignore("unused_signal")
signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto)
@warning_ignore("unused_signal")
signal gdunit_message(message :String)
signal gdunit_report(execution_context_id :int, report :GdUnitReport)
@warning_ignore("unused_signal")
signal gdunit_set_test_failed(is_failed :bool)
@warning_ignore("unused_signal")
signal gdunit_settings_changed(property :GdUnitProperty)
const META_KEY := "GdUnitSignals"
@ -29,8 +36,10 @@ static func dispose() -> void:
var signals := instance()
# cleanup connected signals
for signal_ in signals.get_signal_list():
for connection in signals.get_signal_connection_list(signal_["name"]):
connection["signal"].disconnect(connection["callable"])
@warning_ignore("unsafe_cast")
for connection in signals.get_signal_connection_list(signal_["name"] as StringName):
var _signal: Signal = connection["signal"]
var _callable: Callable = connection["callable"]
_signal.disconnect(_callable)
signals = null
Engine.remove_meta(META_KEY)
while signals.get_reference_count() > 0:
signals.unreference()

View File

@ -17,18 +17,20 @@ static func instance(name :String, clazz :Callable) -> Variant:
return Engine.get_meta(name)
var singleton :Variant = clazz.call()
if is_instance_of(singleton, RefCounted):
push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, singleton.get_class()])
@warning_ignore("unsafe_cast")
push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()])
return
Engine.set_meta(name, singleton)
GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton])
var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
@warning_ignore("return_value_discarded")
singletons.append(name)
Engine.set_meta(MEATA_KEY, singletons)
return singleton
static func unregister(p_singleton :String) -> void:
static func unregister(p_singleton :String, use_call_deferred :bool = false) -> void:
var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
if singletons.has(p_singleton):
GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton);
@ -36,18 +38,19 @@ static func unregister(p_singleton :String) -> void:
singletons.remove_at(index)
var instance_ :Object = Engine.get_meta(p_singleton)
GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_])
GdUnitTools.free_instance(instance_)
@warning_ignore("return_value_discarded")
GdUnitTools.free_instance(instance_, use_call_deferred)
Engine.remove_meta(p_singleton)
GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton)
Engine.set_meta(MEATA_KEY, singletons)
static func dispose() -> void:
static func dispose(use_call_deferred :bool = false) -> void:
# use a copy because unregister is modify the singletons array
var singletons := PackedStringArray(Engine.get_meta(MEATA_KEY, PackedStringArray()))
var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
GdUnitTools.prints_verbose("----------------------------------------------------------------")
GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons)
for singleton in singletons:
unregister(singleton)
for singleton in PackedStringArray(singletons):
unregister(singleton, use_call_deferred)
Engine.remove_meta(MEATA_KEY)
GdUnitTools.prints_verbose("----------------------------------------------------------------")

View File

@ -5,7 +5,9 @@ extends RefCounted
static func create(source :Script, line_number :int) -> GdUnitResult:
var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder())
# we need to save and close the testsuite and source if is current opened before modify
@warning_ignore("return_value_discarded")
ScriptEditorControls.save_an_open_script(source.resource_path)
@warning_ignore("return_value_discarded")
ScriptEditorControls.save_an_open_script(test_suite_path, true)
if GdObjects.is_cs_script(source):
return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path)

View File

@ -30,8 +30,10 @@ func prescan_testsuite_classes() -> void:
var base_class :String = script_meta["base"]
var resource_path :String = script_meta["path"]
if base_class == "GdUnitTestSuite":
@warning_ignore("return_value_discarded")
_included_resources.append(resource_path)
elif ClassDB.class_exists(base_class):
@warning_ignore("return_value_discarded")
_excluded_resources.append(resource_path)
@ -54,6 +56,7 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N
if exclude_scan_directories.has(dir.get_current_dir()):
return collected_suites
prints("Scanning for test suites in:", dir.get_current_dir())
@warning_ignore("return_value_discarded")
dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
var file_name := dir.get_next()
while file_name != "":
@ -61,6 +64,7 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N
if dir.current_is_dir():
var sub_dir := DirAccess.open(resource_path)
if sub_dir != null:
@warning_ignore("return_value_discarded")
_scan_test_suites(sub_dir, collected_suites)
else:
var time := LocalTime.now()
@ -91,7 +95,7 @@ func _parse_is_test_suite(resource_path :String) -> Node:
return null
# Check in the global class cache whether the GdUnitTestSuite class has been extended.
if _included_resources.has(resource_path):
return _parse_test_suite(ResourceLoader.load(resource_path))
return _parse_test_suite(GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path))
# Otherwise we need to scan manual, we need to exclude classes where direct extends form Godot classes
# the resource loader can fail to load e.g. plugin classes with do preload other scripts
@ -100,10 +104,25 @@ func _parse_is_test_suite(resource_path :String) -> Node:
if extends_from.is_empty() or ClassDB.class_exists(extends_from):
return null
# Finally, we need to load the class to determine it is a test suite
var script := ResourceLoader.load(resource_path)
var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path)
if not GdObjects.is_test_suite(script):
return null
return _parse_test_suite(ResourceLoader.load(resource_path))
return _parse_test_suite(script)
# We load the test suites with disabled unsafe_method_access to avoid spamming loading errors
# `unsafe_method_access` will happen when using `assert_that`
static func load_with_disabled_warnings(resource_path: String) -> GDScript:
# grap current level
var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access")
# disable and load the script
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0)
var script: GDScript = ResourceLoader.load(resource_path)
# restore
ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access)
return script
static func _is_script_format_supported(resource_path :String) -> bool:
@ -113,65 +132,58 @@ static func _is_script_format_supported(resource_path :String) -> bool:
return GdUnit4CSharpApiLoader.is_csharp_file(resource_path)
func _parse_test_suite(script :GDScript) -> GdUnitTestSuite:
func _parse_test_suite(script: Script) -> GdUnitTestSuite:
if not GdObjects.is_test_suite(script):
return null
var test_suite :GdUnitTestSuite = script.new()
# If test suite a C# script
if GdUnit4CSharpApiLoader.is_test_suite(script.resource_path):
return GdUnit4CSharpApiLoader.parse_test_suite(script.resource_path)
# Do pares as GDScript
var test_suite: GdUnitTestSuite = (script as GDScript).new()
test_suite.set_name(GdUnitTestSuiteScanner.parse_test_suite_name(script))
# add test cases to test suite and parse test case line nummber
var test_case_names := _extract_test_case_names(script)
_parse_and_add_test_cases(test_suite, script, test_case_names)
# not all test case parsed?
# we have to scan the base class to
if not test_case_names.is_empty():
var base_script :GDScript = test_suite.get_script().get_base_script()
while base_script is GDScript:
# do not parse testsuite itself
if base_script.resource_path.find("GdUnitTestSuite") == -1:
_parse_and_add_test_cases(test_suite, base_script, test_case_names)
base_script = base_script.get_base_script()
var test_case_names := _extract_test_case_names(script as GDScript)
_parse_and_add_test_cases(test_suite, script as GDScript, test_case_names)
return test_suite
func _extract_test_case_names(script :GDScript) -> PackedStringArray:
var names := PackedStringArray()
for method in script.get_script_method_list():
var funcName :String = method["name"]
if funcName.begins_with("test"):
names.append(funcName)
return names
return script.get_script_method_list()\
.map(func(descriptor: Dictionary) -> String: return descriptor["name"])\
.filter(func(func_name: String) -> bool: return func_name.begins_with("test"))
static func parse_test_suite_name(script :Script) -> String:
return script.resource_path.get_file().replace(".gd", "")
func _handle_test_suite_arguments(test_suite :Node, script :GDScript, fd :GdFunctionDescriptor) -> void:
func _handle_test_suite_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void:
for arg in fd.args():
match arg.name():
_TestCase.ARGUMENT_SKIP:
var result :Variant = _expression_runner.execute(script, arg.value_as_string())
var result: Variant = _expression_runner.execute(script, arg.plain_value())
if result is bool:
test_suite.__is_skipped = result
else:
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string())
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value())
_TestCase.ARGUMENT_SKIP_REASON:
test_suite.__skip_reason = arg.value_as_string()
test_suite.__skip_reason = arg.plain_value()
_:
push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path])
func _handle_test_case_arguments(test_suite :Node, script :GDScript, fd :GdFunctionDescriptor) -> void:
func _handle_test_case_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void:
var timeout := _TestCase.DEFAULT_TIMEOUT
var iterations := Fuzzer.ITERATION_DEFAULT_COUNT
var seed_value := -1
var is_skipped := false
var skip_reason := "Unknown."
var fuzzers :Array[GdFunctionArgument] = []
var fuzzers: Array[GdFunctionArgument] = []
var test := _TestCase.new()
for arg in fd.args():
for arg: GdFunctionArgument in fd.args():
# verify argument is allowed
# is test using fuzzers?
if arg.type() == GdObjects.TYPE_FUZZER:
@ -181,31 +193,33 @@ func _handle_test_case_arguments(test_suite :Node, script :GDScript, fd :GdFunct
_TestCase.ARGUMENT_TIMEOUT:
timeout = arg.default()
_TestCase.ARGUMENT_SKIP:
var result :Variant = _expression_runner.execute(script, arg.value_as_string())
var result :Variant = _expression_runner.execute(script, arg.plain_value())
if result is bool:
is_skipped = result
else:
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string())
push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value())
_TestCase.ARGUMENT_SKIP_REASON:
skip_reason = arg.value_as_string()
skip_reason = arg.plain_value()
Fuzzer.ARGUMENT_ITERATIONS:
iterations = arg.default()
Fuzzer.ARGUMENT_SEED:
seed_value = arg.default()
# create new test
test.configure(fd.name(), fd.line_number(), script.resource_path, timeout, fuzzers, iterations, seed_value)
@warning_ignore("return_value_discarded")
test.configure(fd.name(), fd.line_number(), fd.source_path(), timeout, fuzzers, iterations, seed_value)
test.set_function_descriptor(fd)
test.skip(is_skipped, skip_reason)
_validate_argument(fd, test)
test_suite.add_child(test)
func _parse_and_add_test_cases(test_suite :Node, script :GDScript, test_case_names :PackedStringArray) -> void:
func _parse_and_add_test_cases(test_suite: GdUnitTestSuite, script: GDScript, test_case_names: PackedStringArray) -> void:
var test_cases_to_find := Array(test_case_names)
var functions_to_scan := test_case_names.duplicate()
@warning_ignore("return_value_discarded")
functions_to_scan.append("before")
var source := _script_parser.load_source_code(script, [script.resource_path])
var function_descriptors := _script_parser.parse_functions(source, "", [script.resource_path], functions_to_scan)
var function_descriptors := _script_parser.get_function_descriptors(script, functions_to_scan)
for fd in function_descriptors:
if fd.name() == "before":
_handle_test_suite_arguments(test_suite, script, fd)
@ -226,7 +240,7 @@ func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void:
# converts given file name by configured naming convention
static func _to_naming_convention(file_name :String) -> String:
var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, 0)
var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, 0)
match nc:
GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT:
if GdObjects.is_snake_case(file_name):
@ -291,16 +305,15 @@ static func create_test_suite(test_suite_path :String, source_path :String) -> G
static func get_test_case_line_number(resource_path :String, func_name :String) -> int:
var file := FileAccess.open(resource_path, FileAccess.READ)
if file != null:
var script_parser := GdScriptParser.new()
var line_number := 0
while not file.eof_reached():
var row := GdScriptParser.clean_up_row(file.get_line())
var row := file.get_line()
line_number += 1
# ignore comments and empty lines and not test functions
if row.begins_with("#") || row.length() == 0 || row.find("functest") == -1:
if row.begins_with("#") || row.length() == 0 || row.find("func test_") == -1:
continue
# abort if test case name found
if script_parser.parse_func_name(row) == "test_" + func_name:
if row.find("func") != -1 and row.find("test_" + func_name) != -1:
return line_number
return -1
@ -323,7 +336,7 @@ func get_extends_classname(resource_path :String) -> String:
static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult:
var script := load(resource_path) as GDScript
var script := load_with_disabled_warnings(resource_path)
# count all exiting lines and add two as space to add new test case
var line_number := count_lines(script) + 2
var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name)
@ -350,7 +363,7 @@ static func test_suite_exists(test_suite_path :String) -> bool:
static func test_case_exists(test_suite_path :String, func_name :String) -> bool:
if not test_suite_exists(test_suite_path):
return false
var script := ResourceLoader.load(test_suite_path) as GDScript
var script := load_with_disabled_warnings(test_suite_path)
for f in script.get_script_method_list():
if f["name"] == "test_" + func_name:
return true

View File

@ -27,11 +27,13 @@ static func prints_verbose(message :String) -> void:
prints(message)
static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool:
@warning_ignore("unsafe_cast")
static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool:
if instance is Array:
for element :Variant in instance:
@warning_ignore("return_value_discarded")
free_instance(element)
instance.clear()
(instance as Array).clear()
return true
# do not free an already freed instance
if not is_instance_valid(instance):
@ -41,26 +43,39 @@ static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool:
return false
if is_stdout_verbose:
print_verbose("GdUnit4:gc():free instance ", instance)
release_double(instance)
release_double(instance as Object)
if instance is RefCounted:
instance.notification(Object.NOTIFICATION_PREDELETE)
(instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE)
# If scene runner freed we explicit await all inputs are processed
if instance is GdUnitSceneRunnerImpl:
await (instance as GdUnitSceneRunnerImpl).await_input_processed()
return true
else:
# is instance already freed?
#if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"):
# return false
#release_connections(instance)
if instance is Timer:
instance.stop()
instance.call_deferred("free")
await Engine.get_main_loop().process_frame
var timer := instance as Timer
timer.stop()
if use_call_deferred:
timer.call_deferred("free")
else:
timer.free()
await (Engine.get_main_loop() as SceneTree).process_frame
return true
if instance is Node and instance.get_parent() != null:
if instance is Node and (instance as Node).get_parent() != null:
var node := instance as Node
if is_stdout_verbose:
print_verbose("GdUnit4:gc():remove node from parent ", instance.get_parent(), instance)
instance.get_parent().remove_child(instance)
instance.set_owner(null)
instance.free()
print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node)
if use_call_deferred:
node.get_parent().remove_child.call_deferred(node)
#instance.call_deferred("set_owner", null)
else:
node.get_parent().remove_child(node)
if is_stdout_verbose:
print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance)
if use_call_deferred:
(instance as Object).call_deferred("free")
else:
(instance as Object).free()
return !is_instance_valid(instance)
@ -81,21 +96,22 @@ static func _release_connections(instance :Object) -> void:
static func release_timers() -> void:
# we go the new way to hold all gdunit timers in group 'GdUnitTimers'
if Engine.get_main_loop().root == null:
var scene_tree := Engine.get_main_loop() as SceneTree
if scene_tree.root == null:
return
for node :Node in Engine.get_main_loop().root.get_children():
for node :Node in scene_tree.root.get_children():
if is_instance_valid(node) and node.is_in_group("GdUnitTimers"):
if is_instance_valid(node):
Engine.get_main_loop().root.remove_child(node)
node.stop()
node.free()
scene_tree.root.remove_child.call_deferred(node)
(node as Timer).stop()
node.queue_free()
# the finally cleaup unfreed resources and singletons
static func dispose_all() -> void:
static func dispose_all(use_call_deferred :bool = false) -> void:
release_timers()
GdUnitSingleton.dispose(use_call_deferred)
GdUnitSignals.dispose()
GdUnitSingleton.dispose()
# if instance an mock or spy we need manually freeing the self reference
@ -104,12 +120,6 @@ static func release_double(instance :Object) -> void:
instance.call("__release_double")
static func clear_push_errors() -> void:
var runner :Node = Engine.get_meta("GdUnitRunner")
if runner != null:
runner.clear_push_errors()
static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void:
var test_case :Node = test_suite.find_child(test_case_name, false, false)
var test_case: _TestCase = test_suite.find_child(test_case_name, false, false)
test_case.expect_to_interupt()

View File

@ -19,11 +19,3 @@ static func set_event_global_position(event: InputEventMouseMotion, global_posit
event.global_position = event.position
else:
event.global_position = global_position
# we crash on macOS when using free() inside the plugin _exit_tree
static func free_fix(instance: Object) -> void:
if OS.get_distribution_name().contains("mac"):
instance.queue_free()
else:
instance.free()

View File

@ -3,6 +3,7 @@ class_name LocalTime
extends Resource
enum TimeUnit {
DEFAULT = 0,
MILLIS = 1,
SECOND = 2,
MINUTE = 3,
@ -60,6 +61,7 @@ func plus(time_unit :TimeUnit, value :int) -> LocalTime:
addValue = value * MILLIS_PER_MINUTE
TimeUnit.HOUR:
addValue = value * MILLIS_PER_HOUR
@warning_ignore("return_value_discarded")
_init(_time + addValue)
return self

View File

@ -22,7 +22,6 @@ var _expect_to_interupt := false
var _timer: Timer
var _interupted: bool = false
var _failed := false
var _report: GdUnitReport = null
var _parameter_set_resolver: GdUnitTestParameterSetResolver
var _is_disposed := false
@ -80,14 +79,13 @@ func dispose() -> void:
stop_timer()
_remove_failure_handler()
_fuzzers.clear()
_report = null
@warning_ignore("shadowed_variable_base_class", "redundant_await")
func _execute_test_case(name: String, test_parameter: Array) -> void:
# needs at least on await otherwise it breaks the awaiting chain
await get_parent().callv(name, test_parameter)
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
completed.emit()
@ -104,22 +102,30 @@ func set_timeout() -> void:
_timer = Timer.new()
add_child(_timer)
_timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id())
_timer.timeout.connect(func do_interrupt() -> void:
if is_fuzzed():
_report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout"))
else:
_report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout))
_interupted = true
completed.emit()
, CONNECT_DEFERRED)
@warning_ignore("return_value_discarded")
_timer.timeout.connect(do_interrupt, CONNECT_DEFERRED)
_timer.set_one_shot(true)
_timer.set_wait_time(time)
_timer.set_autostart(false)
_timer.start()
func do_interrupt() -> void:
_interupted = true
if not is_expect_interupted():
var execution_context:= GdUnitThreadManager.get_current_context().get_execution_context()
if is_fuzzed():
execution_context.add_report(GdUnitReport.new()\
.create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout")))
else:
execution_context.add_report(GdUnitReport.new()\
.create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout)))
completed.emit()
func _set_failure_handler() -> void:
if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received):
@warning_ignore("return_value_discarded")
GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received)
@ -164,10 +170,6 @@ func is_skipped() -> bool:
return _skipped
func report() -> GdUnitReport:
return _report
func skip_info() -> String:
return _skip_reason

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://btx5kcrsngasl"
path="res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdUnit4/src/core/assets/touch-button.png"
dest_files=["res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -50,6 +50,7 @@ static func instance() -> GdUnitCommandHandler:
return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new())
@warning_ignore("return_value_discarded")
func _init() -> void:
assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING)
@ -58,6 +59,7 @@ func _init() -> void:
GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected)
GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
# preload previous test execution
@warning_ignore("return_value_discarded")
_runner_config.load_config()
init_shortcuts()
@ -73,13 +75,10 @@ func _init() -> void:
register_command(GdUnitCommand.new(CMD_CREATE_TESTCASE, is_not_running, cmd_create_test, GdUnitShortcut.ShortCut.CREATE_TEST))
register_command(GdUnitCommand.new(CMD_STOP_TEST_RUN, is_running, cmd_stop.bind(_client_id), GdUnitShortcut.ShortCut.STOP_TEST_RUN))
# do not reschedule inside of test run (called on GdUnitCommandHandlerTest)
if Engine.has_meta("GdUnitRunner"):
return
# schedule discover tests if enabled
if GdUnitSettings.is_test_discover_enabled():
var timer :SceneTreeTimer = Engine.get_main_loop().create_timer(5)
# schedule discover tests if enabled and running inside the editor
if Engine.is_editor_hint() and GdUnitSettings.is_test_discover_enabled():
var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(5)
@warning_ignore("return_value_discarded")
timer.timeout.connect(cmd_discover_tests)
@ -123,7 +122,7 @@ func init_shortcuts() -> void:
register_shortcut(shortcut, inputEvent)
func create_shortcut_input_even(key_codes : PackedInt32Array) -> InputEventKey:
func create_shortcut_input_even(key_codes: PackedInt32Array) -> InputEventKey:
var inputEvent :InputEventKey = InputEventKey.new()
inputEvent.pressed = true
for key_code in key_codes:
@ -208,7 +207,8 @@ func cmd_run(debug :bool) -> void:
if _is_running:
return
# save current selected excution config
var result := _runner_config.set_server_port(Engine.get_meta("gdunit_server_port")).save_config()
var server_port: int = Engine.get_meta("gdunit_server_port")
var result := _runner_config.set_server_port(server_port).save_config()
if result.is_error():
push_error(result.error_message())
return
@ -232,9 +232,10 @@ func cmd_stop(client_id :int) -> void:
if _running_debug_mode:
EditorInterface.stop_playing_scene()
else: if _current_runner_process_id > 0:
var result := OS.kill(_current_runner_process_id)
if result != OK:
push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result)
if OS.is_process_running(_current_runner_process_id):
var result := OS.kill(_current_runner_process_id)
if result != OK:
push_error("ERROR checked stopping GdUnit Test Runner. error code: %s" % result)
_current_runner_process_id = -1
@ -242,6 +243,7 @@ func cmd_editor_run_test(debug :bool) -> void:
var cursor_line := active_base_editor().get_caret_line()
#run test case?
var regex := RegEx.new()
@warning_ignore("return_value_discarded")
regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)")
var result := regex.search(active_base_editor().get_line(cursor_line))
if result:
@ -262,8 +264,10 @@ func cmd_create_test() -> void:
# show error dialog
push_error("Failed to create test case: %s" % result.error_message())
return
var info := result.value() as Dictionary
ScriptEditorControls.edit_script(info.get("path"), info.get("line"))
var info: Dictionary = result.value()
var script_path: String = info.get("path")
var script_line: int = info.get("line")
ScriptEditorControls.edit_script(script_path, script_line)
func cmd_discover_tests() -> void:
@ -279,8 +283,10 @@ static func scan_test_directorys(base_directory :String, test_directory: String,
if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory):
continue
if match_test_directory(directory, test_directory):
@warning_ignore("return_value_discarded")
test_suite_paths.append(current_directory)
else:
@warning_ignore("return_value_discarded")
scan_test_directorys(current_directory, test_directory, test_suite_paths)
return test_suite_paths
@ -342,11 +348,13 @@ func _on_run_overall_pressed(_debug := false) -> void:
func _on_settings_changed(property :GdUnitProperty) -> void:
if SETTINGS_SHORTCUT_MAPPING.has(property.name()):
var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name())
var input_event := create_shortcut_input_even(property.value())
var value: PackedInt32Array = property.value()
var input_event := create_shortcut_input_even(value)
prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()])
register_shortcut(shortcut, input_event)
if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED:
var timer :SceneTreeTimer = Engine.get_main_loop().create_timer(3)
var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3)
@warning_ignore("return_value_discarded")
timer.timeout.connect(cmd_discover_tests)

View File

@ -1,60 +1,105 @@
extends RefCounted
# Caches all test indices for parameterized tests
class TestCaseIndicesCache:
var _cache := {}
func _key(resource_path: String, test_name: String) -> StringName:
return &"%s_%s" % [resource_path, test_name]
func contains_test_case(resource_path: String, test_name: String) -> bool:
return _cache.has(_key(resource_path, test_name))
func validate(resource_path: String, test_name: String, indices: PackedStringArray) -> bool:
var cached_indicies: PackedStringArray = _cache[_key(resource_path, test_name)]
return GdArrayTools.has_same_content(cached_indicies, indices)
func sync(resource_path: String, test_name: String, indices: PackedStringArray) -> void:
if indices.is_empty():
_cache[_key(resource_path, test_name)] = []
else:
_cache[_key(resource_path, test_name)] = indices
# contains all tracked test suites where discovered since editor start
# key : test suite resource_path
# value: the list of discovered test case names
var _discover_cache := {}
var discovered_test_case_indices_cache := TestCaseIndicesCache.new()
func _init() -> void:
# Register for discovery events to sync the cache
@warning_ignore("return_value_discarded")
GdUnitSignals.instance().gdunit_add_test_suite.connect(sync_cache)
func sync_cache(dto :GdUnitTestSuiteDto) -> void:
var resource_path := dto.path()
var discovered_test_cases :Array[String] = []
func sync_cache(dto: GdUnitTestSuiteDto) -> void:
var resource_path := ProjectSettings.localize_path(dto.path())
var discovered_test_cases: Array[String] = []
for test_case in dto.test_cases():
discovered_test_cases.append(test_case.name())
discovered_test_case_indices_cache.sync(resource_path, test_case.name(), test_case.test_case_names())
_discover_cache[resource_path] = discovered_test_cases
func discover(script: Script) -> void:
# for cs scripts we need to recomplie before discover new tests
if GdObjects.is_cs_script(script):
await rebuild_project(script)
if GdObjects.is_test_suite(script):
# a new test suite is discovered
if not _discover_cache.has(script.resource_path):
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var script_path := ProjectSettings.localize_path(script.resource_path)
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var suite_name := test_suite.get_name()
if not _discover_cache.has(script_path):
var dto :GdUnitTestSuiteDto = GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script.resource_path, test_suite.get_name(), dto))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script_path, suite_name, dto))
sync_cache(dto)
test_suite.queue_free()
return
var tests_added :Array[String] = []
var tests_removed := PackedStringArray()
var script_test_cases := extract_test_functions(script)
var discovered_test_cases :Array[String] = _discover_cache.get(script.resource_path, [] as Array[String])
var discovered_test_cases :Array[String] = _discover_cache.get(script_path, [] as Array[String])
var script_test_cases := extract_test_functions(test_suite)
# first detect removed/renamed tests
var tests_removed := PackedStringArray()
for test_case in discovered_test_cases:
if not script_test_cases.has(test_case):
@warning_ignore("return_value_discarded")
tests_removed.append(test_case)
# second detect new added tests
var tests_added :Array[String] = []
for test_case in script_test_cases:
if not discovered_test_cases.has(test_case):
tests_added.append(test_case)
# We need to scan for parameterized test because of possible test data changes
# For more details look at https://github.com/MikeSchulze/gdUnit4/issues/592
for test_case_name in script_test_cases:
if discovered_test_case_indices_cache.contains_test_case(script_path, test_case_name):
var test_case: _TestCase = test_suite.find_child(test_case_name, false, false)
var test_indices := test_case.test_case_names()
if not discovered_test_case_indices_cache.validate(script_path, test_case_name, test_indices):
if !tests_removed.has(test_case_name):
tests_removed.append(test_case_name)
if !tests_added.has(test_case_name):
tests_added.append(test_case_name)
discovered_test_case_indices_cache.sync(script_path, test_case_name, test_indices)
# finally notify changes to the inspector
if not tests_removed.is_empty() or not tests_added.is_empty():
var scanner := GdUnitTestSuiteScanner.new()
var test_suite := scanner._parse_test_suite(script)
var suite_name := test_suite.get_name()
# emit deleted tests
for test_name in tests_removed:
discovered_test_cases.erase(test_name)
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestRemoved.new(script.resource_path, suite_name, test_name))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestRemoved.new(script_path, suite_name, test_name))
# emit new discovered tests
for test_name in tests_added:
@ -62,25 +107,46 @@ func discover(script: Script) -> void:
var test_case := test_suite.find_child(test_name, false, false)
var dto := GdUnitTestCaseDto.new()
dto = dto.deserialize(dto.serialize(test_case))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestAdded.new(script.resource_path, suite_name, dto))
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestAdded.new(script_path, suite_name, dto))
# if the parameterized test fresh added we need to sync the cache
if not discovered_test_case_indices_cache.contains_test_case(script_path, test_name):
discovered_test_case_indices_cache.sync(script_path, test_name, dto.test_case_names())
# update the cache
_discover_cache[script.resource_path] = discovered_test_cases
_discover_cache[script_path] = discovered_test_cases
test_suite.queue_free()
func extract_test_functions(script :Script) -> PackedStringArray:
return script.get_script_method_list()\
.map(map_func_names)\
.filter(filter_test_cases)
func extract_test_functions(test_suite :Node) -> PackedStringArray:
return test_suite.get_children()\
.filter(func(child: Node) -> bool: return is_instance_of(child, _TestCase))\
.map(func (child: Node) -> String: return child.get_name())
func map_func_names(method_info :Dictionary) -> String:
return method_info["name"]
func is_paramaterized_test(test_suite :Node, test_case_name: String) -> bool:
return test_suite.get_children()\
.filter(func(child: Node) -> bool: return child.name == test_case_name)\
.any(func (test: _TestCase) -> bool: return test.is_parameterized())
func filter_test_cases(value :String) -> bool:
return value.begins_with("test_")
# do rebuild the entire project, there is actual no way to enforce the Godot engine itself to do this
func rebuild_project(script: Script) -> void:
var class_path := ProjectSettings.globalize_path(script.resource_path)
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard: CSharpScript change detected on: '%s' [/color]" % class_path)
var scene_tree := Engine.get_main_loop() as SceneTree
await scene_tree.process_frame
var output := []
var exit_code := OS.execute("dotnet", ["--version"], output)
if exit_code == -1:
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Rebuild the project failed.[/color]")
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Can't find installed `dotnet`! Please check your environment is setup correctly.[/color]")
return
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % output[0].strip_edges())
output.clear()
func filter_by_test_cases(method_info :Dictionary, value :String) -> bool:
return method_info["name"] == value
exit_code = OS.execute("dotnet", ["build"], output)
print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]")
for out:Variant in output:
print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges())
await scene_tree.process_frame

View File

@ -5,21 +5,34 @@ extends RefCounted
static func run() -> void:
prints("Running test discovery ..")
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new())
await Engine.get_main_loop().create_timer(.5).timeout
await (Engine.get_main_loop() as SceneTree).create_timer(.5).timeout
var test_suite_directories :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), [])
var scanner := GdUnitTestSuiteScanner.new()
var _test_suites_to_process :Array[Node] = []
for test_suite_dir in test_suite_directories:
_test_suites_to_process.append_array(scanner.scan(test_suite_dir))
# We run the test discovery in an extra thread so that the main thread is not blocked
var t:= Thread.new()
@warning_ignore("return_value_discarded")
t.start(func () -> void:
var test_suite_directories :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), [])
var scanner := GdUnitTestSuiteScanner.new()
var _test_suites_to_process :Array[Node] = []
var test_case_count :int = _test_suites_to_process.reduce(func (accum :int, test_suite :Node) -> int:
return accum + test_suite.get_child_count(), 0)
for test_suite_dir in test_suite_directories:
_test_suites_to_process.append_array(scanner.scan(test_suite_dir))
for test_suite in _test_suites_to_process:
var ts_dto := GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_add_test_suite.emit(ts_dto)
# Do sync the main thread before emit the discovered test suites to the inspector
await (Engine.get_main_loop() as SceneTree).process_frame
var test_case_count :int = 0
for test_suite in _test_suites_to_process:
test_case_count += test_suite.get_child_count()
var ts_dto := GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_add_test_suite.emit(ts_dto)
test_suite.free()
prints("%d test suites discovered." % _test_suites_to_process.size())
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(_test_suites_to_process.size(), test_case_count))
await Engine.get_main_loop().process_frame
prints("%d test suites discovered." % _test_suites_to_process.size())
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(_test_suites_to_process.size(), test_case_count))
_test_suites_to_process.clear()
)
# wait unblocked to the tread is finished
while t.is_alive():
await (Engine.get_main_loop() as SceneTree).process_frame
# needs finally to wait for finish
await t.wait_to_finish()

View File

@ -3,6 +3,7 @@ extends Resource
const WARNINGS = "warnings"
const FAILED = "failed"
const FLAKY = "flaky"
const ERRORS = "errors"
const SKIPPED = "skipped"
const ELAPSED_TIME = "elapsed_time"
@ -10,6 +11,7 @@ const ORPHAN_NODES = "orphan_nodes"
const ERROR_COUNT = "error_count"
const FAILED_COUNT = "failed_count"
const SKIPPED_COUNT = "skipped_count"
const RETRY_COUNT = "retry_count"
enum {
INIT,
@ -18,6 +20,7 @@ enum {
TESTSUITE_AFTER,
TESTCASE_BEFORE,
TESTCASE_AFTER,
TESTCASE_STATISTICS,
DISCOVER_START,
DISCOVER_END,
DISCOVER_SUITE_ADDED,
@ -71,6 +74,15 @@ func test_after(p_resource_path :String, p_suite_name :String, p_test_name :Stri
return self
func test_statistics(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}) -> GdUnitEvent:
_event_type = TESTCASE_STATISTICS
_resource_path = p_resource_path
_suite_name = p_suite_name
_test_name = p_test_name
_statistics = p_statistics
return self
func type() -> int:
return _event_type
@ -135,6 +147,10 @@ func is_error() -> bool:
return _statistics.get(ERRORS, false)
func is_flaky() -> bool:
return _statistics.get(FLAKY, false)
func is_skipped() -> bool:
return _statistics.get(SKIPPED, false)
@ -170,7 +186,8 @@ func deserialize(serialized :Dictionary) -> GdUnitEvent:
if serialized.has("reports"):
# needs this workaround to copy typed values in the array
var reports_to_deserializ :Array[Dictionary] = []
reports_to_deserializ.append_array(serialized.get("reports"))
@warning_ignore("unsafe_cast")
reports_to_deserializ.append_array(serialized.get("reports") as Array)
_reports = _deserialize_reports(reports_to_deserializ)
return self

View File

@ -2,50 +2,65 @@
## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor
class_name GdUnitExecutionContext
var _parent_context :GdUnitExecutionContext
var _sub_context :Array[GdUnitExecutionContext] = []
var _orphan_monitor :GdUnitOrphanNodesMonitor
var _memory_observer :GdUnitMemoryObserver
var _report_collector :GdUnitTestReportCollector
var _timer :LocalTime
var _parent_context: GdUnitExecutionContext
var _sub_context: Array[GdUnitExecutionContext] = []
var _orphan_monitor: GdUnitOrphanNodesMonitor
var _memory_observer: GdUnitMemoryObserver
var _report_collector: GdUnitTestReportCollector
var _timer: LocalTime
var _test_case_name: StringName
var _name :String
var _test_case_parameter_set: Array
var _name: String
var _test_execution_iteration: int = 0
var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled()
var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries()
var error_monitor : GodotGdErrorMonitor = null:
set (value):
error_monitor = value
# execution states
var _is_calculated := false
var _is_success: bool
var _is_flaky: bool
var _is_skipped: bool
var _has_warnings: bool
var _has_failures: bool
var _has_errors: bool
var _failure_count := 0
var _orphan_count := 0
var _error_count := 0
var _skipped_count := 0
var error_monitor: GodotGdErrorMonitor = null:
get:
if _parent_context != null:
return _parent_context.error_monitor
if error_monitor == null:
error_monitor = GodotGdErrorMonitor.new()
return error_monitor
var test_suite : GdUnitTestSuite = null:
set (value):
test_suite = value
var test_suite: GdUnitTestSuite = null:
get:
if _parent_context != null:
return _parent_context.test_suite
return test_suite
var test_case : _TestCase = null:
var test_case: _TestCase = null:
get:
if _test_case_name.is_empty():
return null
return test_suite.find_child(_test_case_name, false, false)
if test_case == null and _parent_context != null:
return _parent_context.test_case
return test_case
func _init(name :String, parent_context :GdUnitExecutionContext = null) -> void:
func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void:
_name = name
_parent_context = parent_context
_timer = LocalTime.now()
_orphan_monitor = GdUnitOrphanNodesMonitor.new(name)
_orphan_monitor.start()
_memory_observer = GdUnitMemoryObserver.new()
error_monitor = GodotGdErrorMonitor.new()
_report_collector = GdUnitTestReportCollector.new(get_instance_id())
_report_collector = GdUnitTestReportCollector.new()
if parent_context != null:
parent_context._sub_context.append(self)
@ -58,40 +73,55 @@ func dispose() -> void:
_parent_context = null
test_suite = null
test_case = null
dispose_sub_contexts()
func dispose_sub_contexts() -> void:
for context in _sub_context:
context.dispose()
_sub_context.clear()
func set_active() -> void:
test_suite.__execution_context = self
GdUnitThreadManager.get_current_context().set_execution_context(self)
static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext:
assert(test_suite_, "test_suite is null")
var context := GdUnitExecutionContext.new(test_suite_.get_name())
context.test_suite = test_suite_
context.set_active()
return context
static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext:
var context := GdUnitExecutionContext.new(test_case_name, pe)
context._test_case_name = test_case_name
context.set_active()
return context
static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext:
static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext:
var context := GdUnitExecutionContext.new(pe._test_case_name, pe)
context._test_case_name = pe._test_case_name
context.set_active()
context._test_execution_iteration = pe._test_execution_iteration
return context
func test_failed() -> bool:
return has_failures() or has_errors()
static func of_test_suite(p_test_suite: GdUnitTestSuite) -> GdUnitExecutionContext:
assert(p_test_suite, "test_suite is null")
var context := GdUnitExecutionContext.new(p_test_suite.get_name())
context.test_suite = p_test_suite
return context
static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext:
assert(p_test_case, "test_case is null")
var context := GdUnitExecutionContext.new(p_test_case.get_name(), pe)
context.test_case = p_test_case
return context
static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext:
var context := GdUnitExecutionContext.new(test_case_name, pe)
context._test_case_name = test_case_name
context._test_case_parameter_set = test_case_parameter_set
return context
func get_test_suite_path() -> String:
return test_suite.get_script().resource_path
func get_test_suite_name() -> StringName:
return test_suite.get_name()
func get_test_case_name() -> StringName:
if _test_case_name.is_empty():
return test_case.get_name()
return _test_case_name
func error_monitor_start() -> void:
@ -102,7 +132,7 @@ func error_monitor_stop() -> void:
await error_monitor.scan()
for error_report in error_monitor.to_reports():
if error_report.is_error():
_report_collector._reports.append(error_report)
_report_collector.push_back(error_report)
func orphan_monitor_start() -> void:
@ -113,45 +143,164 @@ func orphan_monitor_stop() -> void:
_orphan_monitor.stop()
func add_report(report: GdUnitReport) -> void:
_report_collector.push_back(report)
func reports() -> Array[GdUnitReport]:
return _report_collector.reports()
func build_report_statistics(orphans :int, recursive := true) -> Dictionary:
func collect_reports(recursive: bool) -> Array[GdUnitReport]:
if not recursive:
return reports()
var current_reports := reports()
# we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended`
for sub_context in _sub_context:
current_reports.append_array(sub_context.reports())
# needs finally to clean the test reports to avoid counting twice
sub_context.reports().clear()
return current_reports
func collect_orphans(p_reports: Array[GdUnitReport]) -> int:
var orphans := 0
if not _sub_context.is_empty():
orphans += collect_testcase_orphan_reports(_sub_context[0], p_reports)
orphans += collect_teststage_orphan_reports(p_reports)
return orphans
func collect_testcase_orphan_reports(context: GdUnitExecutionContext, p_reports: Array[GdUnitReport]) -> int:
var orphans := context.count_orphans()
if orphans > 0:
p_reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans)))
return orphans
func collect_teststage_orphan_reports(p_reports: Array[GdUnitReport]) -> int:
var orphans := count_orphans()
if orphans > 0:
p_reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans)))
return orphans
func build_reports(recursive:= true) -> Array[GdUnitReport]:
var collected_reports: Array[GdUnitReport] = collect_reports(recursive)
if recursive:
_orphan_count = collect_orphans(collected_reports)
else:
_orphan_count = count_orphans()
if _orphan_count > 0:
collected_reports.push_front(GdUnitReport.new() \
.create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(_orphan_count)))
_is_skipped = is_skipped()
_skipped_count = count_skipped(recursive)
_is_success = is_success()
_is_flaky = is_flaky()
_has_warnings = has_warnings()
_has_errors = has_errors()
_error_count = count_errors(recursive)
if !_is_success:
_has_failures = has_failures()
_failure_count = count_failures(recursive)
_is_calculated = true
return collected_reports
# Evaluates the actual test case status by validate latest execution state (cold be more based on flaky max retry count)
func evaluate_test_retry_status() -> bool:
# get latest test execution status
var last_test_status :GdUnitExecutionContext = _sub_context.back()
_is_skipped = last_test_status.is_skipped()
_skipped_count = last_test_status.count_skipped(false)
_is_success = last_test_status.is_success()
# if success but it have more than one sub contexts the test was rerurn becouse of failures and will be marked as flaky
_is_flaky = _is_success and _sub_context.size() > 1
_has_warnings = last_test_status.has_warnings()
_has_errors = last_test_status.has_errors()
_error_count = last_test_status.count_errors(false)
_has_failures = last_test_status.has_failures()
_failure_count = last_test_status.count_failures(false)
_orphan_count = last_test_status.collect_orphans(collect_reports(false))
_is_calculated = true
# finally cleanup the retry execution contexts
dispose_sub_contexts()
return _is_success
func get_execution_statistics() -> Dictionary:
return {
GdUnitEvent.ORPHAN_NODES: orphans,
GdUnitEvent.RETRY_COUNT: _test_execution_iteration,
GdUnitEvent.ORPHAN_NODES: _orphan_count,
GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(),
GdUnitEvent.FAILED: has_failures(),
GdUnitEvent.ERRORS: has_errors(),
GdUnitEvent.WARNINGS: has_warnings(),
GdUnitEvent.SKIPPED: has_skipped(),
GdUnitEvent.FAILED_COUNT: count_failures(recursive),
GdUnitEvent.ERROR_COUNT: count_errors(recursive),
GdUnitEvent.SKIPPED_COUNT: count_skipped(recursive)
GdUnitEvent.FAILED: !_is_success,
GdUnitEvent.ERRORS: _has_errors,
GdUnitEvent.WARNINGS: _has_warnings,
GdUnitEvent.FLAKY: _is_flaky,
GdUnitEvent.SKIPPED: _is_skipped,
GdUnitEvent.FAILED_COUNT: _failure_count,
GdUnitEvent.ERROR_COUNT: _error_count,
GdUnitEvent.SKIPPED_COUNT: _skipped_count
}
func has_failures() -> bool:
return _sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c.has_failures()) or _report_collector.has_failures()
return (
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c._has_failures if c._is_calculated else c.has_failures())
or _report_collector.has_failures()
)
func has_errors() -> bool:
return _sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c.has_errors()) or _report_collector.has_errors()
return (
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c._has_errors if c._is_calculated else c.has_errors())
or _report_collector.has_errors()
)
func has_warnings() -> bool:
return _sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c.has_warnings()) or _report_collector.has_warnings()
return (
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c._has_warnings if c._is_calculated else c.has_warnings())
or _report_collector.has_warnings()
)
func has_skipped() -> bool:
return _sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c.has_skipped()) or _report_collector.has_skipped()
func is_flaky() -> bool:
return (
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c._is_flaky if c._is_calculated else c.is_flaky())
or _test_execution_iteration > 1
)
func count_failures(recursive :bool) -> int:
func is_success() -> bool:
if _sub_context.is_empty():
return not has_failures()
var failed_context := _sub_context.filter(func(c :GdUnitExecutionContext) -> bool:
return !(c._is_success if c._is_calculated else c.is_success()))
return failed_context.is_empty() and not has_failures()
func is_skipped() -> bool:
return (
_sub_context.any(func(c :GdUnitExecutionContext) -> bool:
return c._is_skipped if c._is_calculated else c.is_skipped())
or test_case.is_skipped() if test_case != null else false
)
func is_interupted() -> bool:
return false if test_case == null else test_case.is_interupted()
func count_failures(recursive: bool) -> int:
if not recursive:
return _report_collector.count_failures()
return _sub_context\
@ -159,7 +308,7 @@ func count_failures(recursive :bool) -> int:
return c.count_failures(recursive)).reduce(sum, _report_collector.count_failures())
func count_errors(recursive :bool) -> int:
func count_errors(recursive: bool) -> int:
if not recursive:
return _report_collector.count_errors()
return _sub_context\
@ -167,7 +316,7 @@ func count_errors(recursive :bool) -> int:
return c.count_errors(recursive)).reduce(sum, _report_collector.count_errors())
func count_skipped(recursive :bool) -> int:
func count_skipped(recursive: bool) -> int:
if not recursive:
return _report_collector.count_skipped()
return _sub_context\
@ -182,15 +331,24 @@ func count_orphans() -> int:
return _orphan_monitor.orphan_nodes() - orphans
func sum(accum :int, number :int) -> int:
func sum(accum: int, number: int) -> int:
return accum + number
func register_auto_free(obj :Variant) -> Variant:
func retry_execution() -> bool:
var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries
if retry:
_test_execution_iteration += 1
return retry
func register_auto_free(obj: Variant) -> Variant:
return _memory_observer.register_auto_free(obj)
## Runs the gdunit garbage collector to free registered object
func gc() -> void:
# unreference last used assert form the test to prevent memory leaks
GdUnitThreadManager.get_current_context().clear_assert()
await _memory_observer.gc()
orphan_monitor_stop()

View File

@ -18,6 +18,7 @@ func register_auto_free(obj :Variant) -> Variant:
if not is_instance_valid(obj):
return obj
# do not register on GDScriptNativeClass
@warning_ignore("unsafe_cast")
if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") :
return obj
#if obj is GDScript or obj is ScriptExtension:
@ -41,6 +42,7 @@ static func _is_instance_guard_enabled() -> bool:
return false
@warning_ignore("unsafe_method_access")
static func debug_observe(name :String, obj :Object, indent :int = 0) -> void:
if not _show_debug:
return
@ -54,7 +56,7 @@ static func debug_observe(name :String, obj :Object, indent :int = 0) -> void:
prints(name, obj, obj.get_class(), obj.get_name())
static func guard_instance(obj :Object) -> Object:
static func guard_instance(obj :Object) -> void:
if not _is_instance_guard_enabled():
return
var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
@ -62,7 +64,6 @@ static func guard_instance(obj :Object) -> Object:
return
debug_observe("Gard on instance", obj)
Engine.set_meta(tag, obj)
return obj
static func unguard_instance(obj :Object, verbose := true) -> void:
@ -78,7 +79,7 @@ static func unguard_instance(obj :Object, verbose := true) -> void:
static func gc_guarded_instance(name :String, instance :Object) -> void:
if not _is_instance_guard_enabled():
return
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
unguard_instance(instance, false)
if is_instance_valid(instance) and instance is RefCounted:
# finally do this very hacky stuff
@ -90,8 +91,8 @@ static func gc_guarded_instance(name :String, instance :Object) -> void:
# if base_script:
# base_script.unreference()
debug_observe(name, instance)
instance.unreference()
await Engine.get_main_loop().process_frame
(instance as RefCounted).unreference()
await (Engine.get_main_loop() as SceneTree).process_frame
static func gc_on_guarded_instances() -> void:
@ -106,7 +107,7 @@ static func gc_on_guarded_instances() -> void:
# store the object into global store aswell to be verified by 'is_marked_auto_free'
func _tag_object(obj :Variant) -> void:
var tagged_object := Engine.get_meta(TAG_AUTO_FREE, []) as Array
var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, [])
tagged_object.append(obj)
Engine.set_meta(TAG_AUTO_FREE, tagged_object)
@ -116,16 +117,18 @@ func gc() -> void:
if _store.is_empty():
return
# give engine time to free objects to process objects marked by queue_free()
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
if _is_stdout_verbose:
print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size())
var tagged_objects := Engine.get_meta(TAG_AUTO_FREE, []) as Array
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
while not _store.is_empty():
var value :Variant = _store.pop_front()
tagged_objects.erase(value)
await GdUnitTools.free_instance(value, _is_stdout_verbose)
assert(_store.is_empty(), "The memory observer has still entries in the store!")
## Checks whether the specified object is registered for automatic release
static func is_marked_auto_free(obj :Object) -> bool:
return Engine.get_meta(TAG_AUTO_FREE, []).has(obj)
static func is_marked_auto_free(obj: Variant) -> bool:
var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
return tagged_objects.has(obj)

View File

@ -3,7 +3,6 @@ class_name GdUnitTestReportCollector
extends RefCounted
var _execution_context_id :int
var _reports :Array[GdUnitReport] = []
@ -23,11 +22,6 @@ static func __filter_is_skipped(report :GdUnitReport) -> bool:
return report.is_skipped()
func _init(execution_context_id :int) -> void:
_execution_context_id = execution_context_id
GdUnitSignals.instance().gdunit_report.connect(on_reports)
func count_failures() -> int:
return _reports.filter(__filter_is_failure).size()
@ -64,7 +58,5 @@ func reports() -> Array[GdUnitReport]:
return _reports
# Consumes reports emitted by tests
func on_reports(execution_context_id :int, report :GdUnitReport) -> void:
if execution_context_id == _execution_context_id:
_reports.append(report)
func push_back(report :GdUnitReport) -> void:
_reports.push_back(report)

View File

@ -5,7 +5,7 @@ class_name GdUnitTestSuiteExecutor
# preload all asserts here
@warning_ignore("unused_private_class_variable")
var _assertions := GdUnitAssertions.new()
var _executeStage :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new()
var _executeStage := GdUnitTestSuiteExecutionStage.new()
func _init(debug_mode :bool = false) -> void:
@ -17,8 +17,8 @@ func execute(test_suite :GdUnitTestSuite) -> void:
if not orphan_detection_enabled:
prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.")
Engine.get_main_loop().root.call_deferred("add_child", test_suite)
await Engine.get_main_loop().process_frame
(Engine.get_main_loop() as SceneTree).root.call_deferred("add_child", test_suite)
await (Engine.get_main_loop() as SceneTree).process_frame
await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite))

View File

@ -4,86 +4,38 @@ class_name GdUnitTestCaseAfterStage
extends IGdUnitExecutionStage
var _test_name :StringName = ""
var _call_stage :bool
var _call_stage: bool
func _init(call_stage := true) -> void:
_call_stage = call_stage
func _execute(context :GdUnitExecutionContext) -> void:
func _execute(context: GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
if _call_stage:
@warning_ignore("redundant_await")
await test_suite.after_test()
# unreference last used assert form the test to prevent memory leaks
GdUnitThreadManager.get_current_context().set_assert(null)
await context.gc()
await context.error_monitor_stop()
if context.test_case.is_skipped():
var reports := context.build_reports()
if context.is_skipped():
fire_test_skipped(context)
else:
fire_test_ended(context)
if is_instance_valid(context.test_case):
context.test_case.dispose()
fire_event(GdUnitEvent.new() \
.test_after(context.get_test_suite_path(),
context.get_test_suite_name(),
context.get_test_case_name(),
context.get_execution_statistics(),
reports))
func set_test_name(test_name :StringName) -> void:
_test_name = test_name
func fire_test_ended(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var test_name := context._test_case_name if _test_name.is_empty() else _test_name
var reports := collect_reports(context)
var orphans := collect_orphans(context, reports)
fire_event(GdUnitEvent.new()\
.test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports))
func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := 0
if not context._sub_context.is_empty():
orphans += add_orphan_report_test(context._sub_context[0], reports)
orphans += add_orphan_report_teststage(context, reports)
return orphans
func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]:
var reports := context.reports()
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
var test_case := context.test_case
if test_case.is_interupted() and not test_case.is_expect_interupted() and test_case.report() != null:
reports.push_back(test_case.report())
# we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended`
if not context._sub_context.is_empty():
reports.append_array(context._sub_context[0].reports())
# needs finally to clean the test reports to avoid counting twice
context._sub_context[0].reports().clear()
return reports
func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans)))
return orphans
func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int:
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new()\
.create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans)))
return orphans
func fire_test_skipped(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var test_case := context.test_case
var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name
var statistics := {
GdUnitEvent.ORPHAN_NODES: 0,
GdUnitEvent.ELAPSED_TIME: 0,
@ -95,6 +47,11 @@ func fire_test_skipped(context :GdUnitExecutionContext) -> void:
GdUnitEvent.SKIPPED: true,
GdUnitEvent.SKIPPED_COUNT: 1,
}
var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
fire_event(GdUnitEvent.new()\
.test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, [report]))
var report := GdUnitReport.new() \
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info()))
fire_event(GdUnitEvent.new() \
.test_after(context.get_test_suite_path(),
context.get_test_suite_name(),
context.get_test_case_name(),
statistics,
[report]))

View File

@ -3,8 +3,6 @@
class_name GdUnitTestCaseBeforeStage
extends IGdUnitExecutionStage
var _test_name :StringName = ""
var _call_stage :bool
@ -14,16 +12,10 @@ func _init(call_stage := true) -> void:
func _execute(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name
fire_event(GdUnitEvent.new()\
.test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name))
.test_before(context.get_test_suite_path(), context.get_test_suite_name(), context.get_test_case_name()))
if _call_stage:
@warning_ignore("redundant_await")
await test_suite.before_test()
context.error_monitor_start()
func set_test_name(test_name :StringName) -> void:
_test_name = test_name

View File

@ -16,6 +16,9 @@ var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterize
@warning_ignore("redundant_await")
func _execute(context :GdUnitExecutionContext) -> void:
var test_case := context.test_case
context.error_monitor_start()
if test_case.is_parameterized():
await _stage_parameterized_test.execute(context)
elif test_case.is_fuzzed():
@ -23,6 +26,20 @@ func _execute(context :GdUnitExecutionContext) -> void:
else:
await _stage_single_test.execute(context)
await context.gc()
await context.error_monitor_stop()
# finally fire test statistics report
fire_event(GdUnitEvent.new()\
.test_statistics(context.get_test_suite_path(),
context.get_test_suite_name(),
context.get_test_case_name(),
context.get_execution_statistics()))
# finally free the test instance
if is_instance_valid(context.test_case):
context.test_case.dispose()
func set_debug_mode(debug_mode :bool = false) -> void:
super.set_debug_mode(debug_mode)

View File

@ -12,17 +12,16 @@ func _execute(context :GdUnitExecutionContext) -> void:
@warning_ignore("redundant_await")
await test_suite.after()
# unreference last used assert form the test to prevent memory leaks
GdUnitThreadManager.get_current_context().set_assert(null)
await context.gc()
var reports := context.reports()
var orphans := context.count_orphans()
if orphans > 0:
reports.push_front(GdUnitReport.new() \
.create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans)))
fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports))
var reports := context.build_reports(false)
fire_event(GdUnitEvent.new()\
.suite_after(context.get_test_suite_path(),\
test_suite.get_name(),
context.get_execution_statistics(),
reports))
GdUnitFileAccess.clear_tmp()
# Guard that checks if all doubled (spy/mock) objects are released
GdUnitClassDoubler.check_leaked_instances()
# we hide the scene/main window after runner is finished
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)

View File

@ -8,7 +8,7 @@ func _execute(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
fire_event(GdUnitEvent.new()\
.suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count()))
.suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count()))
@warning_ignore("redundant_await")
await test_suite.before()

View File

@ -19,6 +19,7 @@ func _execute(context :GdUnitExecutionContext) -> void:
if context.test_suite.__is_skipped:
await fire_test_suite_skipped(context)
else:
@warning_ignore("return_value_discarded")
GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter)
await _stage_before.execute(context)
for test_case_index in context.test_suite.get_child_count():
@ -27,9 +28,9 @@ func _execute(context :GdUnitExecutionContext) -> void:
if not is_instance_valid(test_case):
continue
context.test_suite.set_active_test_case(test_case.get_name())
await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name()))
await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case))
# stop on first error or if fail fast is enabled
if _fail_fast and context.test_failed():
if _fail_fast and not context.is_success():
break
if test_case.is_interupted():
# it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out
@ -38,14 +39,14 @@ func _execute(context :GdUnitExecutionContext) -> void:
context.test_suite = await clone_test_suite(context.test_suite)
await _stage_after.execute(context)
GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter)
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
context.test_suite.free()
context.dispose()
# clones a test suite and moves the test cases to new instance
func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite:
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
dispose_timers(test_suite)
await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter)
var parent := test_suite.get_parent()
@ -56,10 +57,11 @@ func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite:
test_suite.remove_child(child)
_test_suite.add_child(child)
parent.add_child(_test_suite)
@warning_ignore("return_value_discarded")
GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter)
# finally free current test suite instance
test_suite.free()
await Engine.get_main_loop().process_frame
await (Engine.get_main_loop() as SceneTree).process_frame
return _test_suite
@ -67,7 +69,7 @@ func dispose_timers(test_suite :GdUnitTestSuite) -> void:
GdUnitTools.release_timers()
for child in test_suite.get_children():
if child is Timer:
child.stop()
(child as Timer).stop()
test_suite.remove_child(child)
child.free()
@ -86,7 +88,20 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void:
var test_suite := context.test_suite
var skip_count := test_suite.get_child_count()
fire_event(GdUnitEvent.new()\
.suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count))
.suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count))
for test_case_index in context.test_suite.get_child_count():
# iterate only over test cases
var test_case := context.test_suite.get_child(test_case_index) as _TestCase
if not is_instance_valid(test_case):
continue
var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case)
fire_event(GdUnitEvent.new()\
.test_before(test_case_context.get_test_suite_path(), test_case_context.get_test_suite_name(), test_case_context.get_test_case_name()))
fire_test_skipped(test_case_context)
var statistics := {
GdUnitEvent.ORPHAN_NODES: 0,
GdUnitEvent.ELAPSED_TIME: 0,
@ -99,8 +114,37 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void:
GdUnitEvent.SKIPPED: true
}
var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count))
fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report]))
await Engine.get_main_loop().process_frame
fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report]))
await (Engine.get_main_loop() as SceneTree).process_frame
func fire_test_skipped(context: GdUnitExecutionContext) -> void:
var test_case := context.test_case
var statistics := {
GdUnitEvent.ORPHAN_NODES: 0,
GdUnitEvent.ELAPSED_TIME: 0,
GdUnitEvent.WARNINGS: false,
GdUnitEvent.ERRORS: false,
GdUnitEvent.ERROR_COUNT: 0,
GdUnitEvent.FAILED: false,
GdUnitEvent.FAILED_COUNT: 0,
GdUnitEvent.SKIPPED: true,
GdUnitEvent.SKIPPED_COUNT: 1,
}
var report := GdUnitReport.new() \
.create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite"))
fire_event(GdUnitEvent.new() \
.test_after(context.get_test_suite_path(),
context.get_test_suite_name(),
context.get_test_case_name(),
statistics,
[report]))
# finally fire test statistics report
fire_event(GdUnitEvent.new()\
.test_statistics(context.get_test_suite_path(),
context.get_test_suite_name(),
context.get_test_case_name(),
statistics))
func set_debug_mode(debug_mode :bool = false) -> void:

View File

@ -14,7 +14,7 @@ var _debug_mode := false
## await MyExecutionStage.new().execute(<GdUnitExecutionContext>)
## [/codeblock][br]
func execute(context :GdUnitExecutionContext) -> void:
context.set_active()
GdUnitThreadManager.get_current_context().set_execution_context(context)
@warning_ignore("redundant_await")
await _execute(context)

View File

@ -8,10 +8,16 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new()
func _execute(context :GdUnitExecutionContext) -> void:
await _stage_before.execute(context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(context))
await _stage_after.execute(context)
while context.retry_execution():
var test_context := GdUnitExecutionContext.of(context)
await _stage_before.execute(test_context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
await _stage_after.execute(test_context)
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
break
@warning_ignore("return_value_discarded")
context.evaluate_test_retry_status()
func set_debug_mode(debug_mode :bool = false) -> void:

View File

@ -15,6 +15,7 @@ func _execute(context :GdUnitExecutionContext) -> void:
# guard on fuzzers
for fuzzer in fuzzers:
@warning_ignore("return_value_discarded")
GdUnitMemoryObserver.guard_instance(fuzzer)
for iteration in test_case.iterations():
@ -46,7 +47,8 @@ func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[
test_case.generate_seed()
var fuzzers :Array[Fuzzer] = []
for fuzzer_arg in test_case.fuzzer_arguments():
var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script(), fuzzer_arg.value_as_string())
@warning_ignore("unsafe_cast")
var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String)
fuzzer._iteration_index = 0
fuzzer._iteration_limit = test_case.iterations()
fuzzers.append(fuzzer)

View File

@ -0,0 +1,10 @@
class_name GdUnitTestCaseParameterSetTestStage
extends IGdUnitExecutionStage
## Executes a parameterized test case 'test_<name>()' by given parameters.[br]
## It executes synchronized following stages[br]
## -> test_case() [br]
func _execute(context: GdUnitExecutionContext) -> void:
await context.test_case.execute_paramaterized(context._test_case_parameter_set)
await context.gc()

View File

@ -4,6 +4,7 @@ extends IGdUnitExecutionStage
var _stage_before: IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new()
var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new()
var _stage_test: IGdUnitExecutionStage = GdUnitTestCaseParameterSetTestStage.new()
## Executes a parameterized test case.[br]
@ -12,9 +13,6 @@ var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new()
func _execute(context: GdUnitExecutionContext) -> void:
var test_case := context.test_case
var test_parameter_index := test_case.test_parameter_index()
var is_fail := false
var is_error := false
var failing_index := 0
var parameter_set_resolver := test_case.parameter_set_resolver()
var test_names := parameter_set_resolver.build_test_case_names(test_case)
@ -28,44 +26,55 @@ func _execute(context: GdUnitExecutionContext) -> void:
if test_parameter_index != -1 and test_parameter_index != parameter_set_index:
continue
var current_test_case_name := test_names[parameter_set_index]
_stage_before.set_test_name(current_test_case_name)
_stage_after.set_test_name(current_test_case_name)
var test_case_parameter_set: Array
if parameter_set_resolver.is_parameter_set_static(parameter_set_index):
test_case_parameter_set = parameter_sets[parameter_set_index]
var test_context := GdUnitExecutionContext.of(context)
await _stage_before.execute(test_context)
var current_parameter_set :Array
if parameter_set_resolver.is_parameter_set_static(parameter_set_index):
current_parameter_set = parameter_sets[parameter_set_index]
else:
current_parameter_set = _load_parameter_set(context, parameter_set_index)
if not test_case.is_interupted():
await test_case.execute_paramaterized(current_parameter_set)
await _stage_after.execute(test_context)
# we need to clean up the reports here so they are not reported twice
is_fail = is_fail or test_context.count_failures(false) > 0
is_error = is_error or test_context.count_errors(false) > 0
failing_index = parameter_set_index - 1
test_context.reports().clear()
test_context._test_case_name = current_test_case_name
var has_errors := false
while test_context.retry_execution():
var retry_test_context := GdUnitExecutionContext.of(test_context)
retry_test_context._test_case_name = current_test_case_name
await _stage_before.execute(retry_test_context)
if not test_case.is_interupted():
# we need to load paramater set at execution level after the before stage to get the actual variables from the current test
if not parameter_set_resolver.is_parameter_set_static(parameter_set_index):
test_case_parameter_set = _load_parameter_set(context, parameter_set_index)
await _stage_test.execute(GdUnitExecutionContext.of_parameterized_test(retry_test_context, current_test_case_name, test_case_parameter_set))
await _stage_after.execute(retry_test_context)
has_errors = retry_test_context.has_errors()
if retry_test_context.is_success() or retry_test_context.is_skipped() or retry_test_context.is_interupted():
break
var is_success := test_context.evaluate_test_retry_status()
report_test_failure(context, !is_success, has_errors, parameter_set_index)
if test_case.is_interupted():
break
# add report to parent execution context if failed or an error is found
if is_fail:
context.reports().append(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % failing_index))
if is_error:
context.reports().append(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % failing_index))
await context.gc()
func report_test_failure(test_context: GdUnitExecutionContext, is_failed: bool, has_errors: bool, parameter_set_index: int) -> void:
var test_case := test_context.test_case
if is_failed:
test_context.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % parameter_set_index))
if has_errors:
test_context.add_report(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % parameter_set_index))
func _load_parameter_set(context: GdUnitExecutionContext, parameter_set_index: int) -> Array:
var test_case := context.test_case
var test_suite := context.test_suite
# we need to exchange temporary for parameter resolving the execution context
# this is necessary because of possible usage of `auto_free` and needs to run in the parent execution context
var save_execution_context: GdUnitExecutionContext = test_suite.__execution_context
context.set_active()
var thread_context := GdUnitThreadManager.get_current_context()
var save_execution_context := thread_context.get_execution_context()
thread_context.set_execution_context(context)
var parameters := test_case.load_parameter_sets()
# restore the original execution context and restart the orphan monitor to get new instances into account
save_execution_context.set_active()
thread_context.set_execution_context(save_execution_context)
save_execution_context.orphan_monitor_start()
return parameters[parameter_set_index]
@ -74,3 +83,4 @@ func set_debug_mode(debug_mode: bool=false) -> void:
super.set_debug_mode(debug_mode)
_stage_before.set_debug_mode(debug_mode)
_stage_after.set_debug_mode(debug_mode)
_stage_test.set_debug_mode(debug_mode)

View File

@ -9,10 +9,16 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new()
func _execute(context :GdUnitExecutionContext) -> void:
await _stage_before.execute(context)
if not context.test_case.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(context))
await _stage_after.execute(context)
while context.retry_execution():
var test_context := GdUnitExecutionContext.of(context)
await _stage_before.execute(test_context)
if not test_context.is_skipped():
await _stage_test.execute(GdUnitExecutionContext.of(test_context))
await _stage_after.execute(test_context)
if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted():
break
@warning_ignore("return_value_discarded")
context.evaluate_test_retry_status()
func set_debug_mode(debug_mode :bool = false) -> void:

View File

@ -3,7 +3,6 @@ extends RefCounted
var _name :String
var _parent :GdClassDescriptor = null
var _is_inner_class :bool
var _functions :Array[GdFunctionDescriptor]
@ -14,18 +13,10 @@ func _init(p_name :String, p_is_inner_class :bool, p_functions :Array[GdFunction
_functions = p_functions
func set_parent_clazz(p_parent :GdClassDescriptor) -> void:
_parent = p_parent
func name() -> String:
return _name
func parent() -> GdClassDescriptor:
return _parent
func is_inner_class() -> bool:
return _is_inner_class

View File

@ -21,6 +21,7 @@ var _decoders := {
TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY),
TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY),
TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY),
GdObjects.TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(GdObjects.TYPE_PACKED_VECTOR4_ARRAY),
TYPE_DICTIONARY: _on_type_Dictionary,
TYPE_RID: _on_type_RID,
TYPE_NODE_PATH: _on_type_NodePath,
@ -63,11 +64,11 @@ func _on_type_StringName(value :StringName) -> String:
return 'StringName("%s")' % value
func _on_type_Object(value :Object, type :int) -> String:
func _on_type_Object(value: Variant, _type: int) -> String:
return str(value)
func _on_type_Color(color :Color) -> String:
func _on_type_Color(color: Color) -> String:
if color == Color.BLACK:
return "Color()"
return "Color%s" % color
@ -79,11 +80,11 @@ func _on_type_NodePath(path :NodePath) -> String:
return 'NodePath("%s")' % path
func _on_type_Callable(cb :Callable) -> String:
func _on_type_Callable(_cb :Callable) -> String:
return 'Callable()'
func _on_type_Signal(s :Signal) -> String:
func _on_type_Signal(_s :Signal) -> String:
return 'Signal()'
@ -100,7 +101,8 @@ func _on_type_Array(value :Variant, type :int) -> String:
TYPE_PACKED_COLOR_ARRAY:
var colors := PackedStringArray()
for color in value as PackedColorArray:
for color: Color in value:
@warning_ignore("return_value_discarded")
colors.append(_on_type_Color(color))
if colors.is_empty():
return "PackedColorArray()"
@ -108,7 +110,8 @@ func _on_type_Array(value :Variant, type :int) -> String:
TYPE_PACKED_VECTOR2_ARRAY:
var vectors := PackedStringArray()
for vector in value as PackedVector2Array:
for vector: Vector2 in value:
@warning_ignore("return_value_discarded")
vectors.append(_on_type_Vector(vector, TYPE_VECTOR2))
if vectors.is_empty():
return "PackedVector2Array()"
@ -116,15 +119,26 @@ func _on_type_Array(value :Variant, type :int) -> String:
TYPE_PACKED_VECTOR3_ARRAY:
var vectors := PackedStringArray()
for vector in value as PackedVector3Array:
for vector: Vector3 in value:
@warning_ignore("return_value_discarded")
vectors.append(_on_type_Vector(vector, TYPE_VECTOR3))
if vectors.is_empty():
return "PackedVector3Array()"
return "PackedVector3Array([%s])" % ", ".join(vectors)
GdObjects.TYPE_PACKED_VECTOR4_ARRAY:
var vectors := PackedStringArray()
for vector: Vector4 in value:
@warning_ignore("return_value_discarded")
vectors.append(_on_type_Vector(vector, TYPE_VECTOR4))
if vectors.is_empty():
return "PackedVector4Array()"
return "PackedVector4Array([%s])" % ", ".join(vectors)
TYPE_PACKED_STRING_ARRAY:
var values := PackedStringArray()
for v in value as PackedStringArray:
for v: String in value:
@warning_ignore("return_value_discarded")
values.append('"%s"' % v)
if values.is_empty():
return "PackedStringArray()"
@ -136,7 +150,8 @@ func _on_type_Array(value :Variant, type :int) -> String:
TYPE_PACKED_INT32_ARRAY,\
TYPE_PACKED_INT64_ARRAY:
var vectors := PackedStringArray()
for vector :Variant in value as Array:
for vector :Variant in value:
@warning_ignore("return_value_discarded")
vectors.append(str(vector))
if vectors.is_empty():
return GdObjects.type_as_string(type) + "()"
@ -230,11 +245,16 @@ func _on_type_Basis(basis :Basis) -> String:
return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z]
@warning_ignore("unsafe_cast")
static func decode(value :Variant) -> String:
var type := typeof(value)
if GdArrayTools.is_type_array(type) and value.is_empty():
if GdArrayTools.is_type_array(type) and (value as Array).is_empty():
return "<empty>"
var decoder :Callable = instance("GdUnitDefaultValueDecoders", func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()).get_decoder(type)
var decoder :Callable = (
instance("GdUnitDefaultValueDecoders",
func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()
) as GdDefaultValueDecoder
).get_decoder(type)
if decoder == null:
push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type)
return "null"
@ -243,10 +263,15 @@ static func decode(value :Variant) -> String:
return decoder.call(value)
@warning_ignore("unsafe_cast")
static func decode_typed(type :int, value :Variant) -> String:
if value == null:
return "null"
var decoder :Callable = instance("GdUnitDefaultValueDecoders", func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()).get_decoder(type)
var decoder: Callable = (
instance("GdUnitDefaultValueDecoders",
func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()
) as GdDefaultValueDecoder
).get_decoder(type)
if decoder == null:
push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type)
return "null"

View File

@ -2,23 +2,39 @@ class_name GdFunctionArgument
extends RefCounted
var _cleanup_leading_spaces := RegEx.create_from_string("(?m)^[ \t]+")
var _fix_comma_space := RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""")
var _name: String
var _type: int
var _default_value :Variant
var _parameter_sets :PackedStringArray = []
const UNDEFINED :Variant = "<-NO_ARG->"
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const UNDEFINED: String = "<-NO_ARG->"
const ARG_PARAMETERIZED_TEST := "test_parameters"
static var _fuzzer_regex: RegEx
static var _cleanup_leading_spaces: RegEx
static var _fix_comma_space: RegEx
func _init(p_name :String, p_type :int = TYPE_MAX, value :Variant = UNDEFINED) -> void:
var _name: String
var _type: int
var _type_hint: int
var _default_value: Variant
var _parameter_sets: PackedStringArray = []
func _init(p_name: String, p_type: int, value: Variant = UNDEFINED, p_type_hint: int = TYPE_NIL) -> void:
_init_static_variables()
_name = p_name
_type = p_type
if p_name == ARG_PARAMETERIZED_TEST:
_parameter_sets = _parse_parameter_set(value)
_type_hint = p_type_hint
if value != null and p_name == ARG_PARAMETERIZED_TEST:
_parameter_sets = _parse_parameter_set(str(value))
_default_value = value
# is argument a fuzzer?
if _type == TYPE_OBJECT and _fuzzer_regex.search(_name):
_type = GdObjects.TYPE_FUZZER
func _init_static_variables() -> void:
if _fuzzer_regex == null:
_fuzzer_regex = GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")
_cleanup_leading_spaces = RegEx.create_from_string("(?m)^[ \t]+")
_fix_comma_space = RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""")
func name() -> String:
@ -29,20 +45,58 @@ func default() -> Variant:
return GodotVersionFixures.convert(_default_value, _type)
func set_value(value: String) -> void:
# we onle need to apply default values for Objects, all others are provided by the method descriptor
if _type == GdObjects.TYPE_FUZZER:
_default_value = value
return
if _name == ARG_PARAMETERIZED_TEST:
_parameter_sets = _parse_parameter_set(value)
_default_value = value
return
if _type == TYPE_NIL or _type == GdObjects.TYPE_VARIANT:
_type = _extract_value_type(value)
_default_value = value
if _default_value == null:
_default_value = value
func _extract_value_type(value: String) -> int:
if value != UNDEFINED:
if _fuzzer_regex.search(_name):
return GdObjects.TYPE_FUZZER
if value.rfind(")") == value.length()-1:
return GdObjects.TYPE_FUNC
return _type
func value_as_string() -> String:
if has_default():
return str(_default_value)
return GdDefaultValueDecoder.decode_typed(_type, _default_value)
return ""
func plain_value() -> Variant:
return _default_value
func type() -> int:
return _type
func type_hint() -> int:
return _type_hint
func has_default() -> bool:
return not is_same(_default_value, UNDEFINED)
func is_typed_array() -> bool:
return _type == TYPE_ARRAY and _type_hint != TYPE_NIL
func is_parameter_set() -> bool:
return _name == ARG_PARAMETERIZED_TEST
@ -60,10 +114,12 @@ static func get_parameter_set(parameters :Array[GdFunctionArgument]) -> GdFuncti
func _to_string() -> String:
var s := _name
if _type != TYPE_MAX:
if _type != TYPE_NIL:
s += ":" + GdObjects.type_as_string(_type)
if _default_value != UNDEFINED:
s += "=" + str(_default_value)
if _type_hint != TYPE_NIL:
s += "[%s]" % GdObjects.type_as_string(_type_hint)
if typeof(_default_value) != TYPE_STRING:
s += "=" + value_as_string()
return s
@ -85,6 +141,7 @@ func _parse_parameter_set(input :String) -> PackedStringArray:
for c in buf:
current_index += 1
matched = current_index == buf.size()
@warning_ignore("return_value_discarded")
collected_characters.push_back(c)
match c:
@ -108,6 +165,7 @@ func _parse_parameter_set(input :String) -> PackedStringArray:
if matched:
var parameters := _fix_comma_space.sub(collected_characters.get_string_from_utf8(), ", ", true)
if not parameters.is_empty():
@warning_ignore("return_value_discarded")
output.append(parameters)
collected_characters.clear()
matched = false

View File

@ -6,6 +6,7 @@ var _is_static :bool
var _is_engine :bool
var _is_coroutine :bool
var _name :String
var _source_path: String
var _line_number :int
var _return_type :int
var _return_class :String
@ -13,6 +14,18 @@ var _args : Array[GdFunctionArgument]
var _varargs :Array[GdFunctionArgument]
static func create(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor:
var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, false, false, p_return_type, "", p_args)
fd.enrich_file_info(p_source_path, p_source_line)
return fd
static func create_static(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor:
var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, true, false, p_return_type, "", p_args)
fd.enrich_file_info(p_source_path, p_source_line)
return fd
func _init(p_name :String,
p_line_number :int,
p_is_virtual :bool,
@ -34,10 +47,19 @@ func _init(p_name :String,
_varargs = p_varargs
func with_return_class(clazz_name: String) -> GdFunctionDescriptor:
_return_class = clazz_name
return self
func name() -> String:
return _name
func source_path() -> String:
return _source_path
func line_number() -> int:
return _line_number
@ -74,7 +96,7 @@ func is_private() -> bool:
return name().begins_with("_") and not is_virtual()
func return_type() -> Variant:
func return_type() -> int:
return _return_type
@ -84,6 +106,19 @@ func return_type_as_string() -> String:
return GdObjects.type_as_string(return_type())
@warning_ignore("unsafe_cast")
func set_argument_value(arg_name: String, value: String) -> void:
(
_args.filter(func(arg: GdFunctionArgument) -> bool: return arg.name() == arg_name)\
.front() as GdFunctionArgument
).set_value(value)
func enrich_file_info(p_source_path: String, p_line_number: int) -> void:
_source_path = p_source_path
_line_number = p_line_number
func args() -> Array[GdFunctionArgument]:
return _args
@ -92,34 +127,13 @@ func varargs() -> Array[GdFunctionArgument]:
return _varargs
func typeless() -> String:
var func_signature := ""
if _return_type == TYPE_NIL:
func_signature = "func %s(%s) -> void:" % [name(), typeless_args()]
elif _return_type == GdObjects.TYPE_VARIANT:
func_signature = "func %s(%s) -> Variant:" % [name(), typeless_args()]
else:
func_signature = "func %s(%s) -> %s:" % [name(), typeless_args(), return_type_as_string()]
return "static " + func_signature if is_static() else func_signature
func typeless_args() -> String:
var collect := PackedStringArray()
for arg in args():
if arg.has_default():
collect.push_back( arg.name() + "=" + arg.value_as_string())
else:
collect.push_back(arg.name())
for arg in varargs():
collect.push_back(arg.name() + "=" + arg.value_as_string())
return ", ".join(collect)
func typed_args() -> String:
var collect := PackedStringArray()
for arg in args():
@warning_ignore("return_value_discarded")
collect.push_back(arg._to_string())
for arg in varargs():
@warning_ignore("return_value_discarded")
collect.push_back(arg._to_string())
return ", ".join(collect)
@ -135,28 +149,23 @@ func _to_string() -> String:
# extract function description given by Object.get_method_list()
static func extract_from(descriptor :Dictionary) -> GdFunctionDescriptor:
var function_flags :int = descriptor["flags"]
var is_virtual_ :bool = function_flags & METHOD_FLAG_VIRTUAL
var is_static_ :bool = function_flags & METHOD_FLAG_STATIC
var is_vararg_ :bool = function_flags & METHOD_FLAG_VARARG
#var is_const :bool = function_flags & METHOD_FLAG_CONST
#var is_core :bool = function_flags & METHOD_FLAG_OBJECT_CORE
#var is_default :bool = function_flags & METHOD_FLAGS_DEFAULT
#prints("is_virtual: ", is_virtual)
#prints("is_static: ", is_static)
#prints("is_const: ", is_const)
#prints("is_core: ", is_core)
#prints("is_default: ", is_default)
#prints("is_vararg: ", is_vararg)
static func extract_from(descriptor :Dictionary, is_engine_ := true) -> GdFunctionDescriptor:
var func_name: String = descriptor["name"]
var function_flags: int = descriptor["flags"]
var return_descriptor: Dictionary = descriptor["return"]
var clazz_name: String = return_descriptor["class_name"]
var is_virtual_: bool = function_flags & METHOD_FLAG_VIRTUAL
var is_static_: bool = function_flags & METHOD_FLAG_STATIC
var is_vararg_: bool = function_flags & METHOD_FLAG_VARARG
return GdFunctionDescriptor.new(
descriptor["name"],
func_name,
-1,
is_virtual_,
is_static_,
true,
_extract_return_type(descriptor["return"]),
descriptor["return"]["class_name"],
is_engine_,
_extract_return_type(return_descriptor),
clazz_name,
_extract_args(descriptor),
_build_varargs(is_vararg_)
)
@ -185,13 +194,15 @@ const enum_fix := [
"Control.LayoutMode"]
static func _extract_return_type(return_info :Dictionary) -> Variant:
static func _extract_return_type(return_info :Dictionary) -> int:
var type :int = return_info["type"]
var usage :int = return_info["usage"]
if type == TYPE_INT and usage & PROPERTY_USAGE_CLASS_IS_ENUM:
return GdObjects.TYPE_ENUM
if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT:
return GdObjects.TYPE_VARIANT
if type == TYPE_NIL and usage == 6:
return GdObjects.TYPE_VOID
return type
@ -204,11 +215,10 @@ static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]:
var arg :Dictionary = arguments.pop_back()
var arg_name := _argument_name(arg)
var arg_type := _argument_type(arg)
var arg_default :Variant = GdFunctionArgument.UNDEFINED
if not defaults.is_empty():
var default_value :Variant = defaults.pop_back()
arg_default = GdDefaultValueDecoder.decode_typed(arg_type, default_value)
args_.push_front(GdFunctionArgument.new(arg_name, arg_type, arg_default))
var arg_type_hint := _argument_hint(arg)
#var arg_class: StringName = arg["class_name"]
var default_value: Variant = GdFunctionArgument.UNDEFINED if defaults.is_empty() else defaults.pop_back()
args_.push_front(GdFunctionArgument.new(arg_name, arg_type, default_value, arg_type_hint))
return args_
@ -219,23 +229,41 @@ static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]:
# if function has vararg we need to handle this manually by adding 10 default arguments
var type := GdObjects.TYPE_VARARG
for index in 10:
varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE))
varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, '"%s"' % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE))
return varargs_
static func _argument_name(arg :Dictionary) -> String:
# add suffix to the name to prevent clash with reserved names
return (arg["name"] + "_") as String
return arg["name"]
static func _argument_type(arg :Dictionary) -> int:
var type :int = arg["type"]
var usage :int = arg["usage"]
if type == TYPE_OBJECT:
if arg["class_name"] == "Node":
return GdObjects.TYPE_NODE
if arg["class_name"] == "Fuzzer":
return GdObjects.TYPE_FUZZER
# if the argument untyped we need to scan the assignef value type
if type == TYPE_NIL and usage == PROPERTY_USAGE_NIL_IS_VARIANT:
return GdObjects.TYPE_VARIANT
return type
static func _argument_hint(arg :Dictionary) -> int:
var hint :int = arg["hint"]
var hint_string :String = arg["hint_string"]
match hint:
PROPERTY_HINT_ARRAY_TYPE:
return GdObjects.string_to_type(hint_string)
_:
return 0
static func _argument_type_as_string(arg :Dictionary) -> String:
var type := _argument_type(arg)
match type:

View File

@ -14,7 +14,7 @@ var TOKEN_CLASS_NAME := Token.new("class_name")
var TOKEN_INNER_CLASS := Token.new("class")
var TOKEN_EXTENDS := Token.new("extends")
var TOKEN_ENUM := Token.new("enum")
var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("staticfunc")
var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("static func")
var TOKEN_FUNCTION_DECLARATION := Token.new("func")
var TOKEN_FUNCTION := Token.new(".")
var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->")
@ -64,17 +64,12 @@ var TOKENS :Array[Token] = [
OPERATOR_REMAINDER,
]
var _regex_clazz_name :RegEx
var _regex_clazz_name := GdUnitTools.to_regex("(class) ([a-zA-Z0-9_]+) (extends[a-zA-Z]+:)|(class) ([a-zA-Z0-9_]+)")
var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*")
var _base_clazz :String
var _scanned_inner_classes := PackedStringArray()
var _script_constants := {}
static func clean_up_row(row :String) -> String:
return to_unix_format(row.replace(" ", "").replace("\t", ""))
static func to_unix_format(input :String) -> String:
return input.replace("\r\n", "\n")
@ -263,6 +258,7 @@ class TokenInnerClass extends Token:
func parse(source_rows :PackedStringArray, offset :int) -> void:
# add class signature
@warning_ignore("return_value_discarded")
_content.append(source_rows[offset])
# parse class content
for row_index in range(offset+1, source_rows.size()):
@ -275,8 +271,10 @@ class TokenInnerClass extends Token:
source_row = source_row.trim_prefix("\t")
# refomat invalid empty lines
if source_row.dedent().is_empty():
@warning_ignore("return_value_discarded")
_content.append("")
else:
@warning_ignore("return_value_discarded")
_content.append(source_row)
continue
break
@ -287,9 +285,6 @@ class TokenInnerClass extends Token:
return "TokenInnerClass{%s}" % [_clazz_name]
func _init() -> void:
_regex_clazz_name = GdUnitTools.to_regex("(class)([a-zA-Z0-9]+)(extends[a-zA-Z]+:)|(class)([a-zA-Z0-9]+)(:)")
func get_token(input :String, current_index :int) -> Token:
for t in TOKENS:
@ -352,38 +347,7 @@ func tokenize_inner_class(source_code: String, current: int, token: Token) -> To
return TokenInnerClass.new(clazz_name)
@warning_ignore("assert_always_false")
func _process_values(left: Token, token_stack: Array, operator: Token) -> Token:
# precheck
if left.is_variable() and operator.is_operator():
var lvalue :Variant = left.value()
var value :Variant = null
var next_token_ := token_stack.pop_front() as Token
match operator:
OPERATOR_ADD:
value = lvalue + next_token_.value()
OPERATOR_SUB:
value = lvalue - next_token_.value()
OPERATOR_MUL:
value = lvalue * next_token_.value()
OPERATOR_DIV:
value = lvalue / next_token_.value()
OPERATOR_REMAINDER:
value = lvalue & next_token_.value()
_:
assert(false, "Unsupported operator %s" % operator)
return Variable.new( str(value))
return operator
func parse_func_return_type(row: String) -> int:
var token := parse_return_token(row)
if token == TOKEN_NOT_MATCH:
return TYPE_NIL
return token.type()
func parse_return_token(input: String) -> Token:
func parse_return_token(input: String) -> Variable:
var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token)
if index == -1:
return TOKEN_NOT_MATCH
@ -397,10 +361,30 @@ func parse_return_token(input: String) -> Token:
return token
# Parses the argument into a argument signature
# e.g. func foo(arg1 :int, arg2 = 20) -> [arg1, arg2]
func parse_arguments(input: String) -> Array[GdFunctionArgument]:
var args :Array[GdFunctionArgument] = []
func get_function_descriptors(script: GDScript, included_functions: PackedStringArray = []) -> Array[GdFunctionDescriptor]:
var fds: Array[GdFunctionDescriptor] = []
for method_descriptor in script.get_script_method_list():
var func_name: String = method_descriptor["name"]
if included_functions.is_empty() or func_name in included_functions:
# exclude type set/geters
if is_getter_or_setter(func_name):
continue
if not fds.any(func(fd: GdFunctionDescriptor) -> bool: return fd.name() == func_name):
fds.append(GdFunctionDescriptor.extract_from(method_descriptor, false))
# we need to enrich it by default arguments and line number by parsing the script
# the engine core functions has no valid methods to get this info
_prescan_script(script)
_enrich_function_descriptor(script, fds)
return fds
func is_getter_or_setter(func_name: String) -> bool:
return func_name.begins_with("@") and (func_name.ends_with("getter") or func_name.ends_with("setter"))
func _parse_function_arguments(input: String) -> Dictionary:
var arguments := {}
var current_index := 0
var token :Token = null
var bracket := 0
@ -431,7 +415,7 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]:
bracket -= 1
# if function end?
if in_function and bracket == 0:
return args
return arguments
# is function
if token == TOKEN_FUNCTION_DECLARATION:
token = next_token(input, current_index)
@ -441,13 +425,13 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]:
if token is FuzzerToken:
var arg_value := _parse_end_function(input.substr(current_index), true)
current_index += arg_value.length()
args.append(GdFunctionArgument.new(token.name(), token.type(), arg_value))
var arg_name :String = (token as FuzzerToken).name()
arguments[arg_name] = arg_value.lstrip(" ")
continue
# is value argument
if in_function and token.is_variable():
var arg_name :String = token.plain_value()
var arg_type :int = TYPE_NIL
var arg_value :Variant = GdFunctionArgument.UNDEFINED
var arg_name: String = (token as Variable).plain_value()
var arg_value: String = GdFunctionArgument.UNDEFINED
# parse type and default value
while current_index < len(input):
token = next_token(input, current_index)
@ -460,10 +444,6 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]:
if token == TOKEN_SPACE:
current_index += token._consumed
token = next_token(input, current_index)
arg_type = GdObjects.string_as_typeof(token._token)
# handle enum detection as argument
if arg_type == GdObjects.TYPE_VARIANT and is_class_enum_type(token._token):
arg_type = GdObjects.TYPE_ENUM
TOKEN_ARGUMENT_TYPE_ASIGNMENT:
arg_value = _parse_end_function(input.substr(current_index), true)
current_index += arg_value.length()
@ -489,28 +469,8 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]:
TOKEN_ARGUMENT_SEPARATOR:
if bracket <= 1:
break
arg_value = arg_value.lstrip(" ")
if arg_type == TYPE_NIL and arg_value != GdFunctionArgument.UNDEFINED:
if arg_value.begins_with("Color."):
arg_type = TYPE_COLOR
elif arg_value.begins_with("Vector2."):
arg_type = TYPE_VECTOR2
elif arg_value.begins_with("Vector3."):
arg_type = TYPE_VECTOR3
elif arg_value.begins_with("AABB("):
arg_type = TYPE_AABB
elif arg_value.begins_with("["):
arg_type = TYPE_ARRAY
elif arg_value.begins_with("{"):
arg_type = TYPE_DICTIONARY
else:
arg_type = typeof(str_to_var(arg_value))
if arg_value.rfind(")") == arg_value.length()-1:
arg_type = GdObjects.TYPE_FUNC
elif arg_type == TYPE_NIL:
arg_type = TYPE_STRING
args.append(GdFunctionArgument.new(arg_name, arg_type, arg_value))
return args
arguments[arg_name] = arg_value.lstrip(" ")
return arguments
func _parse_end_function(input: String, remove_trailing_char := false) -> String:
@ -563,9 +523,10 @@ func _parse_end_function(input: String, remove_trailing_char := false) -> String
return input.substr(0, current_index)
@warning_ignore("unsafe_method_access")
func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray:
for row_index in source_rows.size():
var input := GdScriptParser.clean_up_row(source_rows[row_index])
var input := source_rows[row_index]
var token := next_token(input, 0)
if token.is_inner_class():
if token.is_class_name(clazz_name):
@ -574,21 +535,6 @@ func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) ->
return PackedStringArray()
func extract_source_code(script_path :PackedStringArray) -> PackedStringArray:
if script_path.is_empty():
push_error("Invalid script path '%s'" % script_path)
return PackedStringArray()
#load the source code
var resource_path := script_path[0]
var script :GDScript = load(resource_path)
var source_code := load_source_code(script, script_path)
var base_script := script.get_base_script()
if base_script:
_base_clazz = GdObjects.extract_class_name_from_class_path([base_script.resource_path])
source_code += load_source_code(base_script, script_path)
return source_code
func extract_func_signature(rows :PackedStringArray, index :int) -> String:
var signature := ""
@ -604,25 +550,6 @@ func extract_func_signature(rows :PackedStringArray, index :int) -> String:
return ""
func load_source_code(script :GDScript, script_path :PackedStringArray) -> PackedStringArray:
var map := script.get_script_constant_map()
for key :String in map.keys():
var value :Variant = map.get(key)
if value is GDScript:
var class_path := GdObjects.extract_class_path(value)
if class_path.size() > 1:
_scanned_inner_classes.append(class_path[1])
var source_code := GdScriptParser.to_unix_format(script.source_code)
var source_rows := source_code.split("\n")
# extract all inner class names
# want to extract an inner class?
if script_path.size() > 1:
var inner_clazz := script_path[1]
source_rows = extract_inner_class(source_rows, inner_clazz)
return PackedStringArray(source_rows)
func get_class_name(script :GDScript) -> String:
var source_code := GdScriptParser.to_unix_format(script.source_code)
var source_rows := source_code.split("\n")
@ -635,13 +562,12 @@ func get_class_name(script :GDScript) -> String:
token = next_token(input, current_index)
current_index += token._consumed
token = tokenize_value(input, current_index, token)
return token.value()
return (token as Variable).value()
# if no class_name found extract from file name
return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file())
func parse_func_name(row :String) -> String:
var input := GdScriptParser.clean_up_row(row)
func parse_func_name(input :String) -> String:
var current_index := 0
var token := next_token(input, current_index)
current_index += token._consumed
@ -653,100 +579,67 @@ func parse_func_name(row :String) -> String:
return token._token
func parse_functions(rows :PackedStringArray, clazz_name :String, clazz_path :PackedStringArray, included_functions := PackedStringArray()) -> Array[GdFunctionDescriptor]:
var func_descriptors :Array[GdFunctionDescriptor] = []
for rowIndex in rows.size():
var row := rows[rowIndex]
# step over inner class functions
if row.begins_with("\t"):
continue
var input := GdScriptParser.clean_up_row(row)
# skip comments and empty lines
if input.begins_with("#") or input.length() == 0:
continue
var token := next_token(input, 0)
if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION:
if _is_func_included(input, included_functions):
var func_signature := extract_func_signature(rows, rowIndex)
var fd := parse_func_description(func_signature, clazz_name, clazz_path, rowIndex+1)
fd._is_coroutine = is_func_coroutine(rows, rowIndex)
func_descriptors.append(fd)
return func_descriptors
## Enriches the function descriptor by line number and argument default values
## - enrich all function descriptors form current script up to all inherited scrips
func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void:
var enriched_functions := PackedStringArray()
var script_to_scan := script
while script_to_scan != null:
# do not scan the test suite base class itself
if script_to_scan.resource_path == "res://addons/gdUnit4/src/GdUnitTestSuite.gd":
break
var rows := script_to_scan.source_code.split("\n")
for rowIndex in rows.size():
var input := rows[rowIndex]
# step over inner class functions
if input.begins_with("\t"):
continue
# skip comments and empty lines
if input.begins_with("#") or input.length() == 0:
continue
var token := next_token(input, 0)
if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION:
var function_name := parse_func_name(input)
var fd: GdFunctionDescriptor = fds.filter(func(element: GdFunctionDescriptor) -> bool:
# is same function name and not already enriched
return function_name == element.name() and not enriched_functions.has(element.name())
).pop_front()
if fd != null:
# add as enriched function to exclude from next iteration (could be inherited)
@warning_ignore("return_value_discarded")
enriched_functions.append(fd.name())
var func_signature := extract_func_signature(rows, rowIndex)
var func_arguments := _parse_function_arguments(func_signature)
# enrich missing default values
for arg_name: String in func_arguments.keys():
var func_argument: String = func_arguments[arg_name]
fd.set_argument_value(arg_name, func_argument)
fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1)
fd._is_coroutine = is_func_coroutine(rows, rowIndex)
# enrich return class name if not set
if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]:
var var_token := parse_return_token(func_signature)
if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT:
fd._return_class = _patch_inner_class_names(var_token.plain_value(), "")
# if the script ihnerits we need to scan this also
script_to_scan = script_to_scan.get_base_script()
func is_func_coroutine(rows :PackedStringArray, index :int) -> bool:
var is_coroutine := false
for rowIndex in range( index+1, rows.size()):
var row := rows[rowIndex]
is_coroutine = row.contains("await")
for rowIndex in range(index+1, rows.size()):
var input := rows[rowIndex]
is_coroutine = input.contains("await")
if is_coroutine:
return true
var input := GdScriptParser.clean_up_row(row)
var token := next_token(input, 0)
# scan until next function
if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION:
break
return is_coroutine
func _is_func_included(row :String, included_functions :PackedStringArray) -> bool:
if included_functions.is_empty():
return true
for name in included_functions:
if row.find(name) != -1:
return true
return false
func parse_func_description(func_signature :String, clazz_name :String, clazz_path :PackedStringArray, line_number :int) -> GdFunctionDescriptor:
var name := parse_func_name(func_signature)
var return_type :int
var return_clazz := ""
var token := parse_return_token(func_signature)
if token == TOKEN_NOT_MATCH:
return_type = GdObjects.TYPE_VARIANT
else:
return_type = token.type()
if token.type() == TYPE_OBJECT:
return_clazz = _patch_inner_class_names(token.value(), clazz_name)
# is return type an enum?
if is_class_enum_type(return_clazz):
return_type = GdObjects.TYPE_ENUM
return GdFunctionDescriptor.new(
name,
line_number,
is_virtual_func(clazz_name, clazz_path, name),
is_static_func(func_signature),
false,
return_type,
return_clazz,
parse_arguments(func_signature)
)
# caches already parsed classes for virtual functions
# key: <clazz_name> value: a Array of virtual function names
var _virtual_func_cache := Dictionary()
func is_virtual_func(clazz_name :String, clazz_path :PackedStringArray, func_name :String) -> bool:
if _virtual_func_cache.has(clazz_name):
return _virtual_func_cache[clazz_name].has(func_name)
var virtual_functions := Array()
var method_list := GdObjects.extract_class_functions(clazz_name, clazz_path)
for method_descriptor :Dictionary in method_list:
var is_virtual_function :bool = method_descriptor["flags"] & METHOD_FLAG_VIRTUAL
if is_virtual_function:
virtual_functions.append(method_descriptor["name"])
_virtual_func_cache[clazz_name] = virtual_functions
return _virtual_func_cache[clazz_name].has(func_name)
func is_static_func(func_signature :String) -> bool:
var input := GdScriptParser.clean_up_row(func_signature)
var token := next_token(input, 0)
return token == TOKEN_FUNCTION_STATIC_DECLARATION
func is_inner_class(clazz_path :PackedStringArray) -> bool:
return clazz_path.size() > 1
@ -755,39 +648,24 @@ func is_func_end(row :String) -> bool:
return row.strip_edges(false, true).ends_with(":")
func is_class_enum_type(value :String) -> bool:
if value == "Variant":
return false
# first check is given value a enum from the current class
if _script_constants.has(value):
return true
# otherwise we need to determie it by reflection
var script := GDScript.new()
script.source_code = """
extends Resource
static func is_class_enum_type() -> bool:
return typeof(%s) == TYPE_DICTIONARY
""".dedent() % value
script.reload()
return script.call("is_class_enum_type")
func _patch_inner_class_names(clazz :String, clazz_name :String) -> String:
var base_clazz := clazz_name.split(".")[0]
func _patch_inner_class_names(clazz :String, clazz_name :String = "") -> String:
var inner_clazz_name := clazz.split(".")[0]
if _scanned_inner_classes.has(inner_clazz_name):
return base_clazz + "." + clazz
return inner_clazz_name
#var base_clazz := clazz_name.split(".")[0]
#return base_clazz + "." + clazz
if _script_constants.has(clazz):
return clazz_name + "." + clazz
return clazz
func extract_functions(script :GDScript, clazz_name :String, clazz_path :PackedStringArray) -> Array[GdFunctionDescriptor]:
var source_code := load_source_code(script, clazz_path)
func _prescan_script(script: GDScript) -> void:
_script_constants = script.get_script_constant_map()
return parse_functions(source_code, clazz_name, clazz_path)
for key :String in _script_constants.keys():
var value :Variant = _script_constants.get(key)
if value is GDScript:
@warning_ignore("return_value_discarded")
_scanned_inner_classes.append(key)
func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult:
@ -795,13 +673,22 @@ func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult:
return GdUnitResult.error("Invalid script path '%s'" % clazz_path)
var is_inner_class_ := is_inner_class(clazz_path)
var script :GDScript = load(clazz_path[0])
var function_descriptors := extract_functions(script, clazz_name, clazz_path)
_prescan_script(script)
if is_inner_class_:
var inner_class_name := clazz_path[1]
if _scanned_inner_classes.has(inner_class_name):
# do load only on inner class source code and enrich the stored script instance
var source_code := _load_inner_class(script, inner_class_name)
script = _script_constants.get(inner_class_name)
script.source_code = source_code
var function_descriptors := get_function_descriptors(script)
var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors)
# iterate over class dependencies
script = script.get_base_script()
while script != null:
clazz_name = GdObjects.extract_class_name_from_class_path([script.resource_path])
function_descriptors = extract_functions(script, clazz_name, clazz_path)
gd_class.set_parent_clazz(GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors))
script = script.get_base_script()
return GdUnitResult.success(gd_class)
func _load_inner_class(script: GDScript, inner_clazz: String) -> String:
var source_rows := GdScriptParser.to_unix_format(script.source_code).split("\n")
# extract all inner class names
var inner_class_code := extract_inner_class(source_rows, inner_clazz)
return "\n".join(inner_class_code)

View File

@ -9,18 +9,66 @@ func __run_expression() -> Variant:
"""
func execute(src_script :GDScript, expression :String) -> Variant:
var constructor_args_regex := RegEx.create_from_string("new\\((?<args>.*)\\)")
func execute(src_script: GDScript, value: Variant) -> Variant:
if typeof(value) != TYPE_STRING:
return value
var expression: String = value
var parameter_map := src_script.get_script_constant_map()
for key: String in parameter_map.keys():
var parameter_value: Variant = parameter_map[key]
# check we need to construct from inner class
# we need to use the original class instance from the script_constant_map otherwise we run into a runtime error
if expression.begins_with(key + ".new") and parameter_value is GDScript:
var object: GDScript = parameter_value
var args := build_constructor_arguments(parameter_map, expression.substr(expression.find("new")))
if args.is_empty():
return object.new()
return object.callv("new", args)
var script := GDScript.new()
var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path
script.source_code = CLASS_TEMPLATE.dedent()\
.replace("${clazz_path}", resource_path)\
.replace("$expression", expression)
script.reload(false)
var runner :Variant = script.new()
#script.take_over_path(resource_path)
@warning_ignore("return_value_discarded")
script.reload(true)
var runner: Object = script.new()
if runner.has_method("queue_free"):
runner.queue_free()
(runner as Node).queue_free()
@warning_ignore("unsafe_method_access")
return runner.__run_expression()
func to_fuzzer(src_script :GDScript, expression :String) -> Fuzzer:
return execute(src_script, expression) as Fuzzer
func build_constructor_arguments(parameter_map: Dictionary, expression: String) -> Array[Variant]:
var result := constructor_args_regex.search(expression)
var extracted_arguments := result.get_string("args").strip_edges()
if extracted_arguments.is_empty():
return []
var arguments :Array = extracted_arguments.split(",")
return arguments.map(func(argument: String) -> Variant:
var value := argument.strip_edges()
# is argument an constant value
if parameter_map.has(value):
return parameter_map[value]
# is typed named value like Vector3.ONE
for type:int in GdObjects.TYPE_AS_STRING_MAPPINGS:
var type_as_string:String = GdObjects.TYPE_AS_STRING_MAPPINGS[type]
if value.begins_with(type_as_string):
return type_convert(value, type)
# is value a string
if value.begins_with("'") or value.begins_with('"'):
return value.trim_prefix("'").trim_suffix("'").trim_prefix('"').trim_suffix('"')
# fallback to default value converting
return str_to_var(value)
)
func to_fuzzer(src_script: GDScript, expression: String) -> Fuzzer:
@warning_ignore("unsafe_cast")
return execute(src_script, expression) as Fuzzer

View File

@ -45,10 +45,11 @@ func validate(input_value_set: Array) -> String:
for input_values :Variant in input_value_set:
var parameter_set_index := input_value_set.find(input_values)
if input_values is Array:
var current_arg_count :int = input_values.size()
var arr_values: Array = input_values
var current_arg_count := arr_values.size()
if current_arg_count != expected_arg_count:
return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count]
var error := GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values, parameter_set_index)
var error := GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, arr_values, parameter_set_index)
if not error.is_empty():
return error
else:
@ -97,6 +98,7 @@ func build_test_case_names(test_case: _TestCase) -> PackedStringArray:
for parameter_set_index in parameter_sets.size():
var parameter_set := parameter_sets[parameter_set_index]
_static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names)
@warning_ignore("return_value_discarded")
_test_case_names_cache.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, parameter_set, parameter_set_index))
parameter_set_index += 1
return _test_case_names_cache
@ -121,6 +123,7 @@ func _extract_test_names_by_reflection(test_case: _TestCase) -> PackedStringArra
var parameter_sets := load_parameter_sets(test_case)
var test_case_names: PackedStringArray = []
for index in parameter_sets.size():
@warning_ignore("return_value_discarded")
test_case_names.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, str(parameter_sets[index]), index))
return test_case_names
@ -149,10 +152,10 @@ func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array:
if result != OK:
push_error("Extracting test parameters failed! Script loading error: %s" % result)
return []
var instance :Variant = script.new()
var instance :Object = script.new()
GdUnitTestParameterSetResolver.copy_properties(test_case.get_parent(), instance)
instance.queue_free()
var parameter_sets :Variant = instance.call("__extract_test_parameters")
(instance as Node).queue_free()
var parameter_sets: Array = instance.call("__extract_test_parameters")
if not do_validate:
return parameter_sets
# validate the parameter set
@ -169,14 +172,32 @@ func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array:
""".dedent().trim_prefix("\n") % [
GdAssertMessages._error("Internal Error"),
GdAssertMessages._error("Please report this issue as a bug!")]
test_case.get_parent().__execution_context\
.reports()\
.append(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error))
GdUnitThreadManager.get_current_context()\
.get_execution_context()\
.add_report(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error))
test_case.skip(true, error)
test_case._interupted = true
@warning_ignore("return_value_discarded")
fixure_typed_parameters(parameter_sets, _fd.args())
return parameter_sets
func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array:
for parameter_set_index in parameter_sets.size():
var parameter_set: Array = parameter_sets[parameter_set_index]
# run over all function arguments
for parameter_index in parameter_set.size():
var parameter :Variant = parameter_set[parameter_index]
var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index]
if parameter is Array:
var as_array: Array = parameter
# we need to convert the untyped array to the expected typed version
if arg_descriptor.is_typed_array():
parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null)
return parameter_sets
static func copy_properties(source: Object, dest: Object) -> void:
for property in source.get_property_list():
var property_name :String = property["name"]

View File

@ -86,8 +86,10 @@ static func default_CS_template() -> String:
static func build_template(source_path: String) -> String:
var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value() as String)
return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template())\
var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value_as_string())
var template: String = GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template())
return template\
.replace(TAG_TEST_SUITE_CLASS, clazz_name+"Test")\
.replace(TAG_SOURCE_RESOURCE_PATH, source_path)\
.replace(TAG_SOURCE_CLASS_NAME, clazz_name)\

View File

@ -4,9 +4,9 @@ extends RefCounted
var _thread :Thread
var _thread_name :String
var _thread_id :int
var _assert :GdUnitAssert
var _signal_collector :GdUnitSignalCollector
var _execution_context :GdUnitExecutionContext
var _asserts := []
func _init(thread :Thread = null) -> void:
@ -21,7 +21,7 @@ func _init(thread :Thread = null) -> void:
func dispose() -> void:
_assert = null
clear_assert()
if is_instance_valid(_signal_collector):
_signal_collector.clear()
_signal_collector = null
@ -29,13 +29,17 @@ func dispose() -> void:
_thread = null
func set_assert(value :GdUnitAssert) -> GdUnitThreadContext:
_assert = value
return self
func clear_assert() -> void:
_asserts.clear()
func set_assert(value :GdUnitAssert) -> void:
if value != null:
_asserts.append(value)
func get_assert() -> GdUnitAssert:
return _assert
return null if _asserts.is_empty() else _asserts[-1]
func set_execution_context(context :GdUnitExecutionContext) -> void:

View File

@ -36,6 +36,7 @@ func _run(name :String, cb :Callable) -> Variant:
var save_current_thread_id := _current_thread_id
var thread := Thread.new()
thread.set_meta("name", name)
@warning_ignore("return_value_discarded")
thread.start(cb)
_current_thread_id = thread.get_id() as int
_register_thread(thread, _current_thread_id)
@ -52,8 +53,9 @@ func _register_thread(thread :Thread, thread_id :int) -> void:
func _unregister_thread(thread_id :int) -> void:
var context := _thread_context_by_id.get(thread_id) as GdUnitThreadContext
var context: GdUnitThreadContext = _thread_context_by_id.get(thread_id)
if context:
@warning_ignore("return_value_discarded")
_thread_context_by_id.erase(thread_id)
context.dispose()

View File

@ -0,0 +1,210 @@
## The helper class to allow to double Callable
## Is just a wrapper to the original callable with the same function signature.
##
## Due to interface conflicts between 'Callable' and 'Object',
## it is not possible to stub the 'call' and 'call_deferred' methods.
##
## The Callable interface and the Object class have overlapping method signatures,
## which causes conflicts when attempting to stub these methods.
## As a result, you cannot create stubs for 'call' and 'call_deferred' methods.
class_name CallableDoubler
const doubler_script :Script = preload("res://addons/gdUnit4/src/doubler/CallableDoubler.gd")
var _cb: Callable
func _init(cb: Callable) -> void:
assert(cb!=null, "Invalid argument <cb> must not be null")
_cb = cb
## --- helpers -----------------------------------------------------------------------------------------------------------------------------
static func map_func_name(method_info: Dictionary) -> String:
return method_info["name"]
## We do not want to double all functions based on Object for this class
## Is used on SpyBuilder to excluding functions to be doubled for Callable
static func excluded_functions() -> PackedStringArray:
return ClassDB.class_get_method_list("Object")\
.map(CallableDoubler.map_func_name)\
.filter(func (name: String) -> bool:
return !CallableDoubler.callable_functions().has(name))
static func non_callable_functions(name: String) -> bool:
return ![
# we allow "_init", is need to construct it,
"excluded_functions",
"non_callable_functions",
"callable_functions",
"map_func_name"
].has(name)
## Returns the list of supported Callable functions
static func callable_functions() -> PackedStringArray:
var supported_functions :Array = doubler_script.get_script_method_list()\
.map(CallableDoubler.map_func_name)\
.filter(CallableDoubler.non_callable_functions)
# We manually add these functions that we cannot/may not overwrite in this class
supported_functions.append_array(["call_deferred", "callv"])
return supported_functions
## -----------------------------------------------------------------------------------------------------------------------------------------
## Callable functions stubing
## -----------------------------------------------------------------------------------------------------------------------------------------
@warning_ignore("untyped_declaration")
func bind(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Callable:
# save
var bind_values: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)
_cb = _cb.bindv(bind_values)
return _cb
func bindv(caller_args: Array) -> Callable:
_cb = _cb.bindv(caller_args)
return _cb
@warning_ignore("untyped_declaration", "native_method_override", "unused_parameter")
func call(arg0=null,
arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Variant:
# This is a placeholder function signanture without any functionallity!
# It is used by the function doubler to double function signature of Callable:call()
# The doubled function calls direct _cb.callv(<arguments>) see GdUnitSpyFunctionDoubler:TEMPLATE_CALLABLE_CALL template
assert(false)
return null
# Is not supported, see class description
#func call_deferred(a) -> void:
# pass
# Is not supported, see class description
#func callv(a) -> void:
# pass
func get_bound_arguments() -> Array:
return _cb.get_bound_arguments()
func get_bound_arguments_count() -> int:
return _cb.get_bound_arguments_count()
func get_method() -> StringName:
return _cb.get_method()
func get_object() -> Object:
return _cb.get_object()
func get_object_id() -> int:
return _cb.get_object_id()
func hash() -> int:
return _cb.hash()
func is_custom() -> bool:
return _cb.is_custom()
func is_null() -> bool:
return _cb.is_null()
func is_standard() -> bool:
return _cb.is_standard()
func is_valid() -> bool:
return _cb.is_valid()
@warning_ignore("untyped_declaration")
func rpc(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void:
var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)
match args.size():
0: _cb.rpc(0)
1: _cb.rpc(args[0])
2: _cb.rpc(args[0], args[1])
3: _cb.rpc(args[0], args[1], args[2])
4: _cb.rpc(args[0], args[1], args[2], args[3])
5: _cb.rpc(args[0], args[1], args[2], args[3], args[4])
6: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5])
7: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
8: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7])
9: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8])
10: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9])
@warning_ignore("untyped_declaration")
func rpc_id(peer_id: int,
arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE,
arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void:
var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)
match args.size():
0: _cb.rpc_id(peer_id)
1: _cb.rpc_id(peer_id, args[0])
2: _cb.rpc_id(peer_id, args[0], args[1])
3: _cb.rpc_id(peer_id, args[0], args[1], args[2])
4: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3])
5: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4])
6: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5])
7: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6])
8: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7])
9: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8])
10: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9])
func unbind(argcount: int) -> Callable:
_cb = _cb.unbind(argcount)
return _cb

View File

@ -1,4 +1,5 @@
# This class defines a value extractor by given function name and args
class_name GdUnitFuncValueExtractor
extends GdUnitValueExtractor
var _func_names :PackedStringArray
@ -27,13 +28,14 @@ func args() -> Array:
#
# if the value not a Object or not accesible be `func_name` the value is converted to `"n.a."`
# expecing null values
func extract_value(value :Variant) -> Variant:
func extract_value(value: Variant) -> Variant:
if value == null:
return null
for func_name in func_names():
if GdArrayTools.is_array_type(value):
var values := Array()
for element :Variant in Array(value):
@warning_ignore("unsafe_cast")
for element: Variant in (value as Array):
values.append(_call_func(element, func_name))
value = values
else:
@ -50,17 +52,19 @@ func _call_func(value :Variant, func_name :String) -> Variant:
# for array types we need to call explicit by function name, using funcref is only supported for Objects
# TODO extend to all array functions
if GdArrayTools.is_array_type(value) and func_name == "empty":
return value.is_empty()
@warning_ignore("unsafe_cast")
return (value as Array).is_empty()
if is_instance_valid(value):
# extract from function
if value.has_method(func_name):
var extract := Callable(value, func_name)
var obj_value: Object = value
if obj_value.has_method(func_name):
var extract := Callable(obj_value, func_name)
if extract.is_valid():
return value.call(func_name) if args().is_empty() else value.callv(func_name, args())
return obj_value.call(func_name) if args().is_empty() else obj_value.callv(func_name, args())
else:
# if no function exists than try to extract form parmeters
var parameter :Variant = value.get(func_name)
var parameter: Variant = obj_value.get(func_name)
if parameter != null:
return parameter
# nothing found than return 'n.a.'

Some files were not shown because too many files have changed in this diff Show More