Compare commits

...

4 Commits

Author SHA1 Message Date
Martin Felis 43da3e12a5 Simple game name sanitation. 2024-12-01 22:04:43 +01:00
Martin Felis 9a829c30b0 Tests cleanup. 2024-12-01 21:57:39 +01:00
Martin Felis 44083114ba Refactored Game saving and loading, added simple unit test for it. 2024-12-01 21:14:04 +01:00
Martin Felis 437762b2f6 Add GdUnit4 and ignore animation warning concerning invalid tracks. 2024-12-01 21:12:45 +01:00
292 changed files with 24950 additions and 180 deletions

View File

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

21
addons/gdUnit4/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mike Schulze
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,111 @@
#!/usr/bin/env -S godot -s
extends SceneTree
enum {
INIT,
PROCESSING,
EXIT
}
const RETURN_SUCCESS = 0
const RETURN_ERROR = 100
const RETURN_WARNING = 101
var _console := CmdConsole.new()
var _cmd_options: = CmdOptions.new([
CmdOption.new(
"-scp, --src_class_path",
"-scp <source_path>",
"The full class path of the source file.",
TYPE_STRING
),
CmdOption.new(
"-scl, --src_class_line",
"-scl <line_number>",
"The selected line number to generate test case.",
TYPE_INT
)
])
var _status := INIT
var _source_file :String = ""
var _source_line :int = -1
func _init() -> void:
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitBuildTool.gd")
var result := cmd_parser.parse(OS.get_cmdline_args())
if result.is_error():
show_options()
exit(RETURN_ERROR, result.error_message());
return
var cmd_options :Array[CmdCommand] = result.value()
for cmd in cmd_options:
if cmd.name() == '-scp':
_source_file = cmd.arguments()[0]
_source_file = ProjectSettings.localize_path(ProjectSettings.localize_path(_source_file))
if cmd.name() == '-scl':
_source_line = int(cmd.arguments()[0])
# verify required arguments
if _source_file == "":
exit(RETURN_ERROR, "missing required argument -scp <source>")
return
if _source_line == -1:
exit(RETURN_ERROR, "missing required argument -scl <number>")
return
_status = PROCESSING
func _idle(_delta :float) -> void:
if _status == PROCESSING:
var script := ResourceLoader.load(_source_file) as Script
if script == null:
exit(RETURN_ERROR, "Can't load source file %s!" % _source_file)
var result := GdUnitTestSuiteBuilder.create(script, _source_line)
if result.is_error():
print_json_error(result.error_message())
exit(RETURN_ERROR, result.error_message())
return
_console.prints_color("Added testcase: %s" % result.value(), Color.CORNFLOWER_BLUE)
print_json_result(result.value())
exit(RETURN_SUCCESS)
func exit(code :int, message :String = "") -> void:
_status = EXIT
if code == RETURN_ERROR:
if not message.is_empty():
_console.prints_error(message)
_console.prints_error("Abnormal exit with %d" % code)
else:
_console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON)
quit(code)
func print_json_result(result :Dictionary) -> void:
# convert back to system path
var path := ProjectSettings.globalize_path(result["path"]);
var json := 'JSON_RESULT:{"TestCases" : [{"line":%d, "path": "%s"}]}' % [result["line"], path]
prints(json)
func print_json_error(error :String) -> void:
prints('JSON_RESULT:{"Error" : "%s"}' % error)
func show_options() -> void:
_console.prints_color(" Usage:", Color.DARK_SALMON)
_console.prints_color(" build -scp <source_path> -scl <line_number>", Color.DARK_SALMON)
_console.prints_color("-- Options ---------------------------------------------------------------------------------------",
Color.DARK_SALMON).new_line()
for option in _cmd_options.default_options():
descripe_option(option)
func descripe_option(cmd_option :CmdOption) -> void:
_console.print_color(" %-40s" % str(cmd_option.commands()), Color.CORNFLOWER_BLUE)
_console.prints_color(cmd_option.description(), Color.LIGHT_GREEN)
if not cmd_option.help().is_empty():
_console.prints_color("%-4s %s" % ["", cmd_option.help()], Color.DARK_TURQUOISE)
_console.new_line()

View File

@ -0,0 +1,622 @@
#!/usr/bin/env -S godot -s
extends SceneTree
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
#warning-ignore-all:return_value_discarded
class CLIRunner:
extends Node
enum {
READY,
INIT,
RUN,
STOP,
EXIT
}
const DEFAULT_REPORT_COUNT = 20
const RETURN_SUCCESS = 0
const RETURN_ERROR = 100
const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103
const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104
const RETURN_WARNING = 101
var _state := READY
var _test_suites_to_process: Array
var _executor :Variant
var _cs_executor :Variant
var _report: GdUnitHtmlReport
var _report_dir: String
var _report_max: int = DEFAULT_REPORT_COUNT
var _headless_mode_ignore := false
var _runner_config := GdUnitRunnerConfig.new()
var _runner_config_file := ""
var _console := CmdConsole.new()
var _cmd_options := CmdOptions.new([
CmdOption.new(
"-a, --add",
"-a <directory|path of testsuite>",
"Adds the given test suite or directory to the execution pipeline.",
TYPE_STRING
),
CmdOption.new(
"-i, --ignore",
"-i <testsuite_name|testsuite_name:test-name>",
"Adds the given test suite or test case to the ignore list.",
TYPE_STRING
),
CmdOption.new(
"-c, --continue",
"",
"""By default GdUnit will abort checked first test failure to be fail fast,
instead of stop after first failure you can use this option to run the complete test set.""".dedent()
),
CmdOption.new(
"-conf, --config",
"-conf [testconfiguration.cfg]",
"Run all tests by given test configuration. Default is 'GdUnitRunner.cfg'",
TYPE_STRING,
true
),
CmdOption.new(
"-help", "",
"Shows this help message."
),
CmdOption.new("--help-advanced", "",
"Shows advanced options."
)
],
[
# advanced options
CmdOption.new(
"-rd, --report-directory",
"-rd <directory>",
"Specifies the output directory in which the reports are to be written. The default is res://reports/.",
TYPE_STRING,
true
),
CmdOption.new(
"-rc, --report-count",
"-rc <count>",
"Specifies how many reports are saved before they are deleted. The default is %s." % str(DEFAULT_REPORT_COUNT),
TYPE_INT,
true
),
#CmdOption.new("--list-suites", "--list-suites [directory]", "Lists all test suites located in the given directory.", TYPE_STRING),
#CmdOption.new("--describe-suite", "--describe-suite <suite name>", "Shows the description of selected test suite.", TYPE_STRING),
CmdOption.new(
"--info", "",
"Shows the GdUnit version info"
),
CmdOption.new(
"--selftest", "",
"Runs the GdUnit self test"
),
CmdOption.new(
"--ignoreHeadlessMode",
"--ignoreHeadlessMode",
"By default, running GdUnit4 in headless mode is not allowed. You can switch off the headless mode check by set this property."
),
])
func _ready() -> void:
_state = INIT
_report_dir = GdUnitFileAccess.current_dir() + "reports"
_executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new()
# stop checked first test failure to fail fast
_executor.fail_fast(true)
if GdUnit4CSharpApiLoader.is_mono_supported():
prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
_cs_executor = GdUnit4CSharpApiLoader.create_executor(self)
var err := GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
if err != OK:
prints("gdUnitSignals failed")
push_error("Error checked startup, can't connect executor for 'send_event'")
quit(RETURN_ERROR)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
prints("Finallize .. done")
func _process(_delta :float) -> void:
match _state:
INIT:
init_gd_unit()
_state = RUN
RUN:
# all test suites executed
if _test_suites_to_process.is_empty():
_state = STOP
else:
set_process(false)
# process next test suite
var test_suite := _test_suites_to_process.pop_front() as Node
if _cs_executor != null and _cs_executor.IsExecutable(test_suite):
_cs_executor.Execute(test_suite)
await _cs_executor.ExecutionCompleted
else:
await _executor.execute(test_suite)
set_process(true)
STOP:
_state = EXIT
_on_gdunit_event(GdUnitStop.new())
quit(report_exit_code(_report))
func quit(code: int) -> void:
_cs_executor = null
GdUnitTools.dispose_all()
await GdUnitMemoryObserver.gc_on_guarded_instances()
await get_tree().physics_frame
get_tree().quit(code)
func set_report_dir(path: String) -> void:
_report_dir = ProjectSettings.globalize_path(GdUnitFileAccess.make_qualified_path(path))
_console.prints_color(
"Set write reports to %s" % _report_dir,
Color.DEEP_SKY_BLUE
)
func set_report_count(count: String) -> void:
var report_count := count.to_int()
if report_count < 1:
_console.prints_error(
"Invalid report history count '%s' set back to default %d"
% [count, DEFAULT_REPORT_COUNT]
)
_report_max = DEFAULT_REPORT_COUNT
else:
_console.prints_color(
"Set report history count to %s" % count,
Color.DEEP_SKY_BLUE
)
_report_max = report_count
func disable_fail_fast() -> void:
_console.prints_color(
"Disabled fail fast!",
Color.DEEP_SKY_BLUE
)
_executor.fail_fast(false)
func run_self_test() -> void:
_console.prints_color(
"Run GdUnit4 self tests.",
Color.DEEP_SKY_BLUE
)
disable_fail_fast()
_runner_config.self_test()
func show_version() -> void:
_console.prints_color(
"Godot %s" % Engine.get_version_info().get("string"),
Color.DARK_SALMON
)
var config := ConfigFile.new()
config.load("addons/gdUnit4/plugin.cfg")
_console.prints_color(
"GdUnit4 %s" % config.get_value("plugin", "version"),
Color.DARK_SALMON
)
quit(RETURN_SUCCESS)
func check_headless_mode() -> void:
_headless_mode_ignore = true
func show_options(show_advanced: bool = false) -> void:
_console.prints_color(
"""
Usage:
runtest -a <directory|path of testsuite>
runtest -a <directory> -i <path of testsuite|testsuite_name|testsuite_name:test_name>
""".dedent(),
Color.DARK_SALMON
).prints_color(
"-- Options ---------------------------------------------------------------------------------------",
Color.DARK_SALMON
).new_line()
for option in _cmd_options.default_options():
descripe_option(option)
if show_advanced:
_console.prints_color(
"-- Advanced options --------------------------------------------------------------------------",
Color.DARK_SALMON
).new_line()
for option in _cmd_options.advanced_options():
descripe_option(option)
func descripe_option(cmd_option: CmdOption) -> void:
_console.print_color(
" %-40s" % str(cmd_option.commands()),
Color.CORNFLOWER_BLUE
)
_console.prints_color(
cmd_option.description(),
Color.LIGHT_GREEN
)
if not cmd_option.help().is_empty():
_console.prints_color(
"%-4s %s" % ["", cmd_option.help()],
Color.DARK_TURQUOISE
)
_console.new_line()
func load_test_config(path := GdUnitRunnerConfig.CONFIG_FILE) -> void:
_console.print_color(
"Loading test configuration %s\n" % path,
Color.CORNFLOWER_BLUE
)
_runner_config_file = path
_runner_config.load_config(path)
func show_help() -> void:
show_options()
quit(RETURN_SUCCESS)
func show_advanced_help() -> void:
show_options(true)
quit(RETURN_SUCCESS)
func init_gd_unit() -> void:
_console.prints_color(
"""
--------------------------------------------------------------------------------------------------
GdUnit4 Comandline Tool
--------------------------------------------------------------------------------------------------""".dedent(),
Color.DARK_SALMON
).new_line()
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
var result := cmd_parser.parse(OS.get_cmdline_args())
if result.is_error():
show_options()
_console.prints_error(result.error_message())
_console.prints_error("Abnormal exit with %d" % RETURN_ERROR)
_state = STOP
quit(RETURN_ERROR)
return
if result.is_empty():
show_help()
return
# build runner config by given commands
var commands :Array[CmdCommand] = []
commands.append_array(result.value())
result = (
CmdCommandHandler.new(_cmd_options)
.register_cb("-help", Callable(self, "show_help"))
.register_cb("--help-advanced", Callable(self, "show_advanced_help"))
.register_cb("-a", Callable(_runner_config, "add_test_suite"))
.register_cbv("-a", Callable(_runner_config, "add_test_suites"))
.register_cb("-i", Callable(_runner_config, "skip_test_suite"))
.register_cbv("-i", Callable(_runner_config, "skip_test_suites"))
.register_cb("-rd", set_report_dir)
.register_cb("-rc", set_report_count)
.register_cb("--selftest", run_self_test)
.register_cb("-c", disable_fail_fast)
.register_cb("-conf", load_test_config)
.register_cb("--info", show_version)
.register_cb("--ignoreHeadlessMode", check_headless_mode)
.execute(commands)
)
if result.is_error():
_console.prints_error(result.error_message())
_state = STOP
quit(RETURN_ERROR)
if DisplayServer.get_name() == "headless":
if _headless_mode_ignore:
_console.prints_warning("""
Headless mode is ignored by option '--ignoreHeadlessMode'"
Please note that tests that use UI interaction do not work correctly in headless mode.
Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore
have no effect in the test!
""".dedent()
).new_line()
else:
_console.prints_error("""
Headless mode is not supported!
Please note that tests that use UI interaction do not work correctly in headless mode.
Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore
have no effect in the test!
You can run with '--ignoreHeadlessMode' to swtich off this check.
""".dedent()
).prints_error(
"Abnormal exit with %d" % RETURN_ERROR_HEADLESS_NOT_SUPPORTED
)
quit(RETURN_ERROR_HEADLESS_NOT_SUPPORTED)
return
_test_suites_to_process = load_testsuites(_runner_config)
if _test_suites_to_process.is_empty():
_console.prints_warning("No test suites found, abort test run!")
_console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON)
_state = STOP
quit(RETURN_SUCCESS)
var total_test_count := _collect_test_case_count(_test_suites_to_process)
_on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_test_count))
func load_testsuites(config: GdUnitRunnerConfig) -> Array[Node]:
var test_suites_to_process: Array[Node] = []
# Dictionary[String, Dictionary[String, PackedStringArray]]
var to_execute := config.to_execute()
# scan for the requested test suites
var ts_scanner := GdUnitTestSuiteScanner.new()
for as_resource_path in to_execute.keys() as Array[String]:
var selected_tests: PackedStringArray = to_execute.get(as_resource_path)
var scaned_suites := ts_scanner.scan(as_resource_path)
skip_test_case(scaned_suites, selected_tests)
test_suites_to_process.append_array(scaned_suites)
skip_suites(test_suites_to_process, config)
return test_suites_to_process
func skip_test_case(test_suites: Array[Node], test_case_names: Array[String]) -> void:
if test_case_names.is_empty():
return
for test_suite in test_suites:
for test_case in test_suite.get_children():
if not test_case_names.has(test_case.get_name()):
test_suite.remove_child(test_case)
test_case.free()
func skip_suites(test_suites: Array[Node], config: GdUnitRunnerConfig) -> void:
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:
continue
skip_suite(test_suite, skipped)
# Dictionary[String, PackedStringArray]
func skip_suite(test_suite: Node, skipped: Dictionary) -> void:
var skipped_suites :Array[String] = 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:
# 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
# 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)
test_suite.__is_skipped = true
test_suite.__skip_reason = skip_reason
else:
# skip tests
for test_to_skip in skipped_tests:
var test_case: _TestCase = test_suite.find_child(test_to_skip, true, false)
if test_case:
test_case.skip(true, skip_reason)
_console.prints_warning("Mark test case '%s':%s as skipped" % [suite_to_skip, test_to_skip])
else:
_console.prints_error(
"Can't skip test '%s' checked test suite '%s', no test with given name exists!"
% [test_to_skip, suite_to_skip]
)
func _collect_test_case_count(test_suites: Array[Node]) -> int:
var total: int = 0
for test_suite in test_suites:
total += test_suite.get_child_count()
return total
# gdlint: disable=function-name
func PublishEvent(data: Dictionary) -> void:
_on_gdunit_event(GdUnitEvent.new().deserialize(data))
func _on_gdunit_event(event: GdUnitEvent) -> void:
match event.type():
GdUnitEvent.INIT:
_report = GdUnitHtmlReport.new(_report_dir)
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)
_console.prints_color(
build_executed_test_suite_msg(_report.suite_executed_count(), _report.suite_count()),
Color.DARK_SALMON
).prints_color(
build_executed_test_case_msg(_report.test_executed_count(), _report.test_count()),
Color.DARK_SALMON
).prints_color(
"Total time: %s" % LocalTime.elapsed(_report.duration()),
Color.DARK_SALMON
).prints_color(
"Open Report at: file://%s" % report_path,
Color.CORNFLOWER_BLUE
)
GdUnitEvent.TESTSUITE_BEFORE:
_report.add_testsuite_report(
GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name(), event.total_count())
)
GdUnitEvent.TESTSUITE_AFTER:
_report.update_test_suite_report(
event.resource_path(),
event.elapsed_time(),
event.is_error(),
event.is_failed(),
event.is_warning(),
event.is_skipped(),
event.skipped_count(),
event.failed_count(),
event.orphan_nodes(),
event.reports()
)
GdUnitEvent.TESTCASE_BEFORE:
_report.add_testcase_report(
event.resource_path(),
GdUnitTestCaseReport.new(
event.resource_path(),
event.suite_name(),
event.test_name()
)
)
GdUnitEvent.TESTCASE_AFTER:
var test_report := GdUnitTestCaseReport.new(
event.resource_path(),
event.suite_name(),
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)
print_status(event)
func build_executed_test_suite_msg(executed_count :int, total_count :int) -> String:
if executed_count == total_count:
return "Executed test suites: (%d/%d)" % [executed_count, total_count]
return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)]
func build_executed_test_case_msg(executed_count :int, total_count :int) -> String:
if executed_count == total_count:
return "Executed test cases: (%d/%d)" % [executed_count, total_count]
return "Executed test cases: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)]
func report_exit_code(report: GdUnitHtmlReport) -> int:
if report.error_count() + report.failure_count() > 0:
_console.prints_color("Exit code: %d" % RETURN_ERROR, Color.FIREBRICK)
return RETURN_ERROR
if report.orphan_count() > 0:
_console.prints_color("Exit code: %d" % RETURN_WARNING, Color.GOLDENROD)
return RETURN_WARNING
_console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON)
return RETURN_SUCCESS
func print_status(event: GdUnitEvent) -> void:
match event.type():
GdUnitEvent.TESTSUITE_BEFORE:
_console.prints_color(
"Run Test Suite %s " % event.resource_path(),
Color.ANTIQUE_WHITE
)
GdUnitEvent.TESTCASE_BEFORE:
_console.print_color(
" Run Test: %s > %s :" % [event.resource_path(), event.test_name()],
Color.ANTIQUE_WHITE
).prints_color(
"STARTED",
Color.FOREST_GREEN
).save_cursor()
GdUnitEvent.TESTCASE_AFTER:
#_console.restore_cursor()
_console.print_color(
" Run Test: %s > %s :" % [event.resource_path(), event.test_name()],
Color.ANTIQUE_WHITE
)
_print_status(event)
_print_failure_report(event.reports())
GdUnitEvent.TESTSUITE_AFTER:
_print_failure_report(event.reports())
_print_status(event)
_console.prints_color(
"Statistics: | %d tests cases | %d error | %d failed | %d skipped | %d orphans |\n"
% [
_report.test_count(),
_report.error_count(),
_report.failure_count(),
_report.skipped_count(),
_report.orphan_count()
],
Color.ANTIQUE_WHITE
)
func _print_failure_report(reports: Array[GdUnitReport]) -> void:
for report in reports:
if (
report.is_failure()
or report.is_error()
or report.is_warning()
or report.is_skipped()
):
_console.prints_color(
" Report:",
Color.DARK_TURQUOISE, CmdConsole.BOLD | CmdConsole.UNDERLINE
)
var text := GdUnitTools.richtext_normalize(str(report))
for line in text.split("\n"):
_console.prints_color(" %s" % line, Color.DARK_TURQUOISE)
_console.new_line()
func _print_status(event: GdUnitEvent) -> void:
if 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)
_console.prints_color(
" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE
)
var _cli_runner :CLIRunner
func _initialize() -> void:
if Engine.get_version_info().hex < 0x40200:
prints("GdUnit4 requires a minimum of Godot 4.2.x Version!")
quit(CLIRunner.RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED)
return
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
_cli_runner = CLIRunner.new()
root.add_child(_cli_runner)
# do not use print statements on _finalize it results in random crashes
func _finalize() -> void:
if OS.is_stdout_verbose():
prints("Finallize ..")
prints("-Orphan nodes report-----------------------")
Window.print_orphan_nodes()
prints("Finallize .. done")

View File

@ -0,0 +1,141 @@
#!/usr/bin/env -S godot -s
extends MainLoop
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
# gdlint: disable=max-line-length
const NO_LOG_TEMPLATE = """
<!DOCTYPE html>
<html>
<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"/>
</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>
</div>
</body>
"""
#warning-ignore-all:return_value_discarded
var _cmd_options := CmdOptions.new([
CmdOption.new(
"-rd, --report-directory",
"-rd <directory>",
"Specifies the output directory in which the reports are to be written. The default is res://reports/.",
TYPE_STRING,
true
)
])
var _report_root_path: String
func _init() -> void:
_report_root_path = GdUnitFileAccess.current_dir() + "reports"
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, "")
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():
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)
if result.is_error():
push_error(result.error_message())
return true
_patch_report(report_path, godot_log)
return true
func set_report_directory(path: String) -> void:
_report_root_path = 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):
var file_name := "%s/%s" % [path, file]
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]
func _patch_report(report_path: String, godot_log: String) -> void:
var index_file := FileAccess.open("%s/index.html" % 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)
# 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)

View File

@ -0,0 +1,99 @@
#!/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

@ -0,0 +1,7 @@
[plugin]
name="gdUnit4"
description="Unit Testing Framework for Godot Scripts"
author="Mike Schulze"
version="4.3.1"
script="plugin.gd"

54
addons/gdUnit4/plugin.gd Normal file
View File

@ -0,0 +1,54 @@
@tool
extends EditorPlugin
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 _guard: GdUnitTestDiscoverGuard
func _enter_tree() -> void:
if Engine.get_version_info().hex < 0x40200:
prints("GdUnit4 plugin requires a minimum of Godot 4.2.x Version!")
return
GdUnitSettings.setup()
# install the GdUnit inspector
_gd_inspector = load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn").instantiate()
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()
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()
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()
resource_saved.connect(_on_resource_saved)
func _exit_tree() -> void:
if is_instance_valid(_gd_inspector):
remove_control_from_docks(_gd_inspector)
GodotVersionFixures.free_fix(_gd_inspector)
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()
prints("Unload GdUnit4 Plugin success")
func _on_resource_saved(resource :Resource) -> void:
if resource is Script:
_guard.discover(resource)

View File

@ -0,0 +1,25 @@
@ECHO OFF
CLS
IF NOT DEFINED GODOT_BIN (
ECHO "GODOT_BIN is not set."
ECHO "Please set the environment variable 'setx GODOT_BIN <path to godot.exe>'"
EXIT /b -1
)
REM scan if Godot mono used and compile c# classes
for /f "tokens=5 delims=. " %%i in ('%GODOT_BIN% --version') do set GODOT_TYPE=%%i
IF "%GODOT_TYPE%" == "mono" (
ECHO "Godot mono detected"
ECHO Compiling c# classes ... Please Wait
dotnet build --debug
ECHO done %errorlevel%
)
%GODOT_BIN% -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd %*
SET exit_code=%errorlevel%
%GODOT_BIN% --headless --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd %*
ECHO %exit_code%
EXIT /B %exit_code%

15
addons/gdUnit4/runtest.sh Normal file
View File

@ -0,0 +1,15 @@
#!/bin/sh
if [ -z "$GODOT_BIN" ]; then
echo "'GODOT_BIN' is not set."
echo "Please set the environment variable 'export GODOT_BIN=/Applications/Godot.app/Contents/MacOS/Godot'"
exit 1
fi
"$GODOT_BIN" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $*
exit_code=$?
echo "Run tests ends with $exit_code"
"$GODOT_BIN" --headless --path . --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd $* > /dev/null
exit_code2=$?
exit $exit_code

View File

@ -0,0 +1,12 @@
class_name Comparator
extends Resource
enum {
EQUAL,
LESS_THAN,
LESS_EQUAL,
GREATER_THAN,
GREATER_EQUAL,
BETWEEN_EQUAL,
NOT_BETWEEN_EQUAL,
}

View File

@ -0,0 +1,34 @@
## A fuzzer implementation to provide default implementation
class_name Fuzzers
extends Resource
## Generates an random string with min/max length and given charset
static func rand_str(min_length: int, max_length :int, charset := StringFuzzer.DEFAULT_CHARSET) -> Fuzzer:
return StringFuzzer.new(min_length, max_length, charset)
## Generates an random integer in a range form to
static func rangei(from: int, to: int) -> Fuzzer:
return IntFuzzer.new(from, to)
## Generates a randon float within in a given range
static func rangef(from: float, to: float) -> Fuzzer:
return FloatFuzzer.new(from, to)
## Generates an random Vector2 in a range form to
static func rangev2(from: Vector2, to: Vector2) -> Fuzzer:
return Vector2Fuzzer.new(from, to)
## Generates an random Vector3 in a range form to
static func rangev3(from: Vector3, to: Vector3) -> Fuzzer:
return Vector3Fuzzer.new(from, to)
## Generates an integer in a range form to that can be divided exactly by 2
static func eveni(from: int, to: int) -> Fuzzer:
return IntFuzzer.new(from, to, IntFuzzer.EVEN)
## Generates an integer in a range form to that cannot be divided exactly by 2
static func oddi(from: int, to: int) -> Fuzzer:
return IntFuzzer.new(from, to, IntFuzzer.ODD)

View File

@ -0,0 +1,160 @@
## An Assertion Tool to verify array values
class_name GdUnitArrayAssert
extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitArrayAssert:
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitArrayAssert:
return self
## Verifies that the current Array is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array is equal to the given one, ignoring case considerations.
@warning_ignore("unused_parameter")
func is_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array is not equal to the given one, ignoring case considerations.
@warning_ignore("unused_parameter")
func is_not_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array is empty, it has a size of 0.
func is_empty() -> GdUnitArrayAssert:
return self
## Verifies that the current Array is not empty, it has a size of minimum 1.
func is_not_empty() -> GdUnitArrayAssert:
return self
## Verifies that the current Array is the same. [br]
## Compares the current by object reference equals
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array is NOT the same. [br]
## Compares the current by object reference equals
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_not_same(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array has a size of given value.
@warning_ignore("unused_parameter")
func has_size(expectd: int) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains the given values, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same]
@warning_ignore("unused_parameter")
func contains(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly]
@warning_ignore("unused_parameter")
func contains_exactly(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order]
@warning_ignore("unused_parameter")
func contains_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains the given values, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains]
@warning_ignore("unused_parameter")
func contains_same(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly]
@warning_ignore("unused_parameter")
func contains_same_exactly(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order]
@warning_ignore("unused_parameter")
func contains_same_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array do NOT contains the given values, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same]
## [b]Example:[/b]
## [codeblock]
## # will succeed
## assert_array([1, 2, 3, 4, 5]).not_contains([6])
## # will fail
## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6])
## [/codeblock]
@warning_ignore("unused_parameter")
func not_contains(expected :Variant) -> GdUnitArrayAssert:
return self
## Verifies that the current Array do NOT contains the given values, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method not_contains]
## [b]Example:[/b]
## [codeblock]
## # will succeed
## assert_array([1, 2, 3, 4, 5]).not_contains([6])
## # will fail
## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6])
## [/codeblock]
@warning_ignore("unused_parameter")
func not_contains_same(expected :Variant) -> GdUnitArrayAssert:
return self
## Extracts all values by given function name and optional arguments into a new ArrayAssert.
## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values
@warning_ignore("unused_parameter")
func extract(func_name: String, args := Array()) -> GdUnitArrayAssert:
return self
## Extracts all values by given extractor's into a new ArrayAssert.
## If the elements not extractable than the value is converted to `"n.a"`, expecting null values
@warning_ignore("unused_parameter")
func extractv(
extractor0 :GdUnitValueExtractor,
extractor1 :GdUnitValueExtractor = null,
extractor2 :GdUnitValueExtractor = null,
extractor3 :GdUnitValueExtractor = null,
extractor4 :GdUnitValueExtractor = null,
extractor5 :GdUnitValueExtractor = null,
extractor6 :GdUnitValueExtractor = null,
extractor7 :GdUnitValueExtractor = null,
extractor8 :GdUnitValueExtractor = null,
extractor9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert:
return self

View File

@ -0,0 +1,41 @@
## Base interface of all GdUnit asserts
class_name GdUnitAssert
extends RefCounted
## Verifies that the current value is null.
@warning_ignore("untyped_declaration")
func is_null():
return self
## Verifies that the current value is not null.
@warning_ignore("untyped_declaration")
func is_not_null():
return self
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
@warning_ignore("untyped_declaration")
func is_equal(expected):
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):
return self
@warning_ignore("untyped_declaration")
func test_fail():
return self
## Overrides the default failure message by given custom message.
@warning_ignore("unused_parameter")
@warning_ignore("untyped_declaration")
func override_failure_message(message :String):
return self

View File

@ -0,0 +1,69 @@
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
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
# fail fast if the given source instance invalid
var assert_that := GdUnitAssertImpl.new(signal_name)
var line_number := GdUnitAssertions.get_line_number()
if not is_instance_valid(source):
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
# fail fast if the given source instance invalid
if not is_instance_valid(source):
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]
assert_that.report_error(failure, line_number)
return value
# Waits for a specified signal sent from the <source> between idle frames and aborts with an error after the specified timeout has elapsed
# source: the object from which the signal is emitted
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
var line_number := GdUnitAssertions.get_line_number()
# fail fast if the given source instance invalid
if not is_instance_valid(source):
GdUnitAssertImpl.new(signal_name)\
.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, true)
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]
GdUnitAssertImpl.new(signal_name).report_error(failure, line_number)
return value
# Waits for for a given amount of milliseconds
# example:
# # waits for 100ms
# await GdUnitAwaiter.await_millis(myNode, 100).completed
# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
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)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
timer.start(milliSec / 1000.0)
await timer.timeout
timer.queue_free()
# Waits until the next idle frame
func await_idle_frame() -> void:
await Engine.get_main_loop().process_frame

View File

@ -0,0 +1,41 @@
## An Assertion Tool to verify boolean values
class_name GdUnitBoolAssert
extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitBoolAssert:
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitBoolAssert:
return self
## Verifies that the current value is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitBoolAssert:
return self
## Verifies that the current value is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitBoolAssert:
return self
## Verifies that the current value is true.
func is_true() -> GdUnitBoolAssert:
return self
## Verifies that the current value is false.
func is_false() -> GdUnitBoolAssert:
return self
## Overrides the default failure message by given custom message.
@warning_ignore("unused_parameter")
func override_failure_message(message :String) -> GdUnitBoolAssert:
return self

View File

@ -0,0 +1,6 @@
class_name GdUnitConstants
extends RefCounted
const NO_ARG :Variant = "<--null-->"
const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures"

View File

@ -0,0 +1,105 @@
## An Assertion Tool to verify dictionary
class_name GdUnitDictionaryAssert
extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitDictionaryAssert:
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is equal to the given one, ignoring order.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is not equal to the given one, ignoring order.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is empty, it has a size of 0.
func is_empty() -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is not empty, it has a size of minimum 1.
func is_not_empty() -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is the same. [br]
## Compares the current by object reference equals
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary is NOT the same. [br]
## Compares the current by object reference equals
@warning_ignore("unused_parameter")
func is_not_same(expected :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary has a size of given value.
@warning_ignore("unused_parameter")
func has_size(expected: int) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary contains the given key(s).[br]
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys]
@warning_ignore("unused_parameter")
func contains_keys(expected :Array) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary contains the given key and value.[br]
## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value]
@warning_ignore("unused_parameter")
func contains_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary not contains the given key(s).[br]
## This function is [b]deprecated[/b] you have to use [method not_contains_keys] instead
@warning_ignore("unused_parameter")
func contains_not_keys(expected :Array) -> GdUnitDictionaryAssert:
push_warning("Deprecated: 'contains_not_keys' is deprectated and will be removed soon, use `not_contains_keys` instead!")
return not_contains_keys(expected)
## Verifies that the current dictionary not contains the given key(s).[br]
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys]
@warning_ignore("unused_parameter")
func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary contains the given key(s).[br]
## The keys are compared by object reference, for deep parameter comparision use [method contains_keys]
@warning_ignore("unused_parameter")
func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary contains the given key and value.[br]
## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value]
@warning_ignore("unused_parameter")
func contains_same_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
return self
## Verifies that the current dictionary not contains the given key(s).
## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys]
@warning_ignore("unused_parameter")
func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert:
return self

View File

@ -0,0 +1,31 @@
## An assertion tool to verify GDUnit asserts.
## This assert is for internal use only, to verify that failed asserts work as expected.
class_name GdUnitFailureAssert
extends GdUnitAssert
## Verifies if the executed assert was successful
func is_success() -> GdUnitFailureAssert:
return self
## Verifies if the executed assert has failed
func is_failed() -> GdUnitFailureAssert:
return self
## Verifies the failure line is equal to expected one.
@warning_ignore("unused_parameter")
func has_line(expected :int) -> GdUnitFailureAssert:
return self
## Verifies the failure message is equal to expected one.
@warning_ignore("unused_parameter")
func has_message(expected: String) -> GdUnitFailureAssert:
return self
## Verifies that the failure message starts with the expected message.
@warning_ignore("unused_parameter")
func starts_with_message(expected: String) -> GdUnitFailureAssert:
return self

View File

@ -0,0 +1,19 @@
class_name GdUnitFileAssert
extends GdUnitAssert
func is_file() -> GdUnitFileAssert:
return self
func exists() -> GdUnitFileAssert:
return self
func is_script() -> GdUnitFileAssert:
return self
@warning_ignore("unused_parameter")
func contains_exactly(expected_rows :Array) -> GdUnitFileAssert:
return self

View File

@ -0,0 +1,83 @@
## An Assertion Tool to verify float values
class_name GdUnitFloatAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
func is_equal(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is not equal to expected one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current and expected value are approximately equal.
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is less than the given one.
@warning_ignore("unused_parameter")
func is_less(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is less than or equal the given one.
@warning_ignore("unused_parameter")
func is_less_equal(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is greater than the given one.
@warning_ignore("unused_parameter")
func is_greater(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is greater than or equal the given one.
@warning_ignore("unused_parameter")
func is_greater_equal(expected :float) -> GdUnitFloatAssert:
return self
## Verifies that the current value is negative.
func is_negative() -> GdUnitFloatAssert:
return self
## Verifies that the current value is not negative.
func is_not_negative() -> GdUnitFloatAssert:
return self
## Verifies that the current value is equal to zero.
func is_zero() -> GdUnitFloatAssert:
return self
## Verifies that the current value is not equal to zero.
func is_not_zero() -> GdUnitFloatAssert:
return self
## Verifies that the current value is in the given set of values.
@warning_ignore("unused_parameter")
func is_in(expected :Array) -> GdUnitFloatAssert:
return self
## Verifies that the current value is not in the given set of values.
@warning_ignore("unused_parameter")
func is_not_in(expected :Array) -> GdUnitFloatAssert:
return self
## Verifies that the current value is between the given boundaries (inclusive).
@warning_ignore("unused_parameter")
func is_between(from :float, to :float) -> GdUnitFloatAssert:
return self

View File

@ -0,0 +1,56 @@
## An Assertion Tool to verify function callback values
class_name GdUnitFuncAssert
extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitFuncAssert:
await Engine.get_main_loop().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
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
return self
## Verifies that the current value is true.
func is_true() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
return self
## Verifies that the current value is false.
func is_false() -> GdUnitFuncAssert:
await Engine.get_main_loop().process_frame
return self
## Overrides the default failure message by given custom message.
@warning_ignore("unused_parameter")
func override_failure_message(message :String) -> GdUnitFuncAssert:
return self
## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br]
## e.g.[br]
## do wait until 5s the function `is_state` is returns 10 [br]
## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code]
@warning_ignore("unused_parameter")
func wait_until(timeout :int) -> GdUnitFuncAssert:
return self

View File

@ -0,0 +1,46 @@
## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error().
class_name GdUnitGodotErrorAssert
extends GdUnitAssert
## Verifies if the executed code runs without any runtime errors
## Usage:
## [codeblock]
## await assert_error(<callable>).is_success()
## [/codeblock]
func is_success() -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
return self
## Verifies if the executed code runs into a runtime error
## Usage:
## [codeblock]
## await assert_error(<callable>).is_runtime_error(<expected error message>)
## [/codeblock]
@warning_ignore("unused_parameter")
func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
return self
## Verifies if the executed code has a push_warning() used
## Usage:
## [codeblock]
## await assert_error(<callable>).is_push_warning(<expected push warning message>)
## [/codeblock]
@warning_ignore("unused_parameter")
func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
return self
## Verifies if the executed code has a push_error() used
## Usage:
## [codeblock]
## await assert_error(<callable>).is_push_error(<expected push error message>)
## [/codeblock]
@warning_ignore("unused_parameter")
func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert:
await Engine.get_main_loop().process_frame
return self

View File

@ -0,0 +1,87 @@
## An Assertion Tool to verify integer values
class_name GdUnitIntAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
func is_equal(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is not equal to expected one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is less than the given one.
@warning_ignore("unused_parameter")
func is_less(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is less than or equal the given one.
@warning_ignore("unused_parameter")
func is_less_equal(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is greater than the given one.
@warning_ignore("unused_parameter")
func is_greater(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is greater than or equal the given one.
@warning_ignore("unused_parameter")
func is_greater_equal(expected :int) -> GdUnitIntAssert:
return self
## Verifies that the current value is even.
func is_even() -> GdUnitIntAssert:
return self
## Verifies that the current value is odd.
func is_odd() -> GdUnitIntAssert:
return self
## Verifies that the current value is negative.
func is_negative() -> GdUnitIntAssert:
return self
## Verifies that the current value is not negative.
func is_not_negative() -> GdUnitIntAssert:
return self
## Verifies that the current value is equal to zero.
func is_zero() -> GdUnitIntAssert:
return self
## Verifies that the current value is not equal to zero.
func is_not_zero() -> GdUnitIntAssert:
return self
## Verifies that the current value is in the given set of values.
@warning_ignore("unused_parameter")
func is_in(expected :Array) -> GdUnitIntAssert:
return self
## Verifies that the current value is not in the given set of values.
@warning_ignore("unused_parameter")
func is_not_in(expected :Array) -> GdUnitIntAssert:
return self
## Verifies that the current value is between the given boundaries (inclusive).
@warning_ignore("unused_parameter")
func is_between(from :int, to :int) -> GdUnitIntAssert:
return self

View File

@ -0,0 +1,49 @@
## An Assertion Tool to verify Object values
class_name GdUnitObjectAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitObjectAssert:
return self
## Verifies that the current value is not equal to expected one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitObjectAssert:
return self
## Verifies that the current value is null.
func is_null() -> GdUnitObjectAssert:
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitObjectAssert:
return self
## Verifies that the current value is the same as the given one.
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected :Variant) -> GdUnitObjectAssert:
return self
## Verifies that the current value is not the same as the given one.
@warning_ignore("unused_parameter")
func is_not_same(expected :Variant) -> GdUnitObjectAssert:
return self
## Verifies that the current value is an instance of the given type.
@warning_ignore("unused_parameter")
func is_instanceof(expected :Object) -> GdUnitObjectAssert:
return self
## Verifies that the current value is not an instance of the given type.
@warning_ignore("unused_parameter")
func is_not_instanceof(expected :Variant) -> GdUnitObjectAssert:
return self

View File

@ -0,0 +1,45 @@
## An Assertion Tool to verify Results
class_name GdUnitResultAssert
extends GdUnitAssert
## Verifies that the current value is null.
func is_null() -> GdUnitResultAssert:
return self
## Verifies that the current value is not null.
func is_not_null() -> GdUnitResultAssert:
return self
## Verifies that the result is ends up with empty
func is_empty() -> GdUnitResultAssert:
return self
## Verifies that the result is ends up with success
func is_success() -> GdUnitResultAssert:
return self
## Verifies that the result is ends up with warning
func is_warning() -> GdUnitResultAssert:
return self
## Verifies that the result is ends up with error
func is_error() -> GdUnitResultAssert:
return self
## Verifies that the result contains the given message
@warning_ignore("unused_parameter")
func contains_message(expected :String) -> GdUnitResultAssert:
return self
## Verifies that the result contains the given value
@warning_ignore("unused_parameter")
func is_value(expected :Variant) -> GdUnitResultAssert:
return self

View File

@ -0,0 +1,286 @@
## The scene runner for GdUnit to simmulate scene interactions
class_name GdUnitSceneRunner
extends RefCounted
const NO_ARG = GdUnitConstants.NO_ARG
## Sets the mouse cursor to given position relative to the viewport.
@warning_ignore("unused_parameter")
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
return self
## Gets the current mouse position of the current viewport
func get_mouse_position() -> Vector2:
return Vector2.ZERO
## Gets the current global mouse position of the current window
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
@warning_ignore("unused_parameter")
func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
return self
## Simulates a mouse move to the relative coordinates (offset).[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
## [br]
## [member relative] : The relative position, indicating the mouse position offset.[br]
## [member time] : The time to move the mouse by 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_move_mouse():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_mouse_move_relative(Vector2(100,100))
## [/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
return self
## Simulates a mouse move to the absolute coordinates.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
## [br]
## [member position] : The final position of the mouse.[br]
## [member time] : The time to move the mouse 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_move_mouse():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_mouse_move_absolute(Vector2(100,100))
## [/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
return self
## Simulates a mouse button pressed.[br]
## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
@warning_ignore("unused_parameter")
func simulate_mouse_button_pressed(buttonIndex :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.
@warning_ignore("unused_parameter")
func simulate_mouse_button_press(buttonIndex :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.
@warning_ignore("unused_parameter")
func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner:
return self
## 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.
@warning_ignore("unused_parameter")
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
return self
## Simulates scene processing for a certain number of frames.[br]
## [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
return self
## Simulates scene processing until the given signal is emitted by the scene.[br]
## [member signal_name] : the signal to stop the simulation[br]
## [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
return self
## Simulates scene processing until the given signal is emitted by the given object.[br]
## [member source] : the object that should emit the signal[br]
## [member signal_name] : the signal to stop the simulation[br]
## [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
return self
### Waits for all input events are processed
func await_input_processed() -> void:
await Engine.get_main_loop().process_frame
await Engine.get_main_loop().physics_frame
## Waits for the function return value until specified timeout or fails.[br]
## [member args] : optional function arguments
@warning_ignore("unused_parameter")
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]
## [member args] : optional function arguments
@warning_ignore("unused_parameter")
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
@warning_ignore("unused_parameter")
func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void:
await Engine.get_main_loop().process_frame
pass
## Waits for given signal is emited by the <source> until a specified timeout to fail.[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
@warning_ignore("unused_parameter")
func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void:
pass
## maximizes the window to bring the scene visible
func maximize_view() -> GdUnitSceneRunner:
return self
## Return the current value of the property with the name <name>.[br]
## [member name] : name of property[br]
## [member return] : the value of the property
@warning_ignore("unused_parameter")
func get_property(name :String) -> Variant:
return null
## Set the value <value> of the property with the name <name>.[br]
## [member name] : name of property[br]
## [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:
return false
## executes the function specified by <name> in the scene and returns the result.[br]
## [member name] : the name of the function to execute[br]
## [member args] : optional function arguments[br]
## [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:
return null
## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br]
## [member name] : the name of the node to find[br]
## [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:
return null
## Access to current running scene
func scene() -> Node:
return null

View File

@ -0,0 +1,38 @@
## An Assertion Tool to verify for emitted signals until a waiting time
class_name GdUnitSignalAssert
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
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
return self
## Verifies the signal exists checked the emitter
@warning_ignore("unused_parameter")
func is_signal_exists(name :String) -> GdUnitSignalAssert:
return self
## Overrides the default failure message by given custom message.
@warning_ignore("unused_parameter")
func override_failure_message(message :String) -> GdUnitSignalAssert:
return self
## Sets the assert signal timeout in ms, if the time over a failure is reported.[br]
## e.g.[br]
## do wait until 5s the instance has emitted the signal `signal_a`[br]
## [code]assert_signal(instance).wait_until(5000).is_emitted("signal_a")[/code]
@warning_ignore("unused_parameter")
func wait_until(timeout :int) -> GdUnitSignalAssert:
return self

View File

@ -0,0 +1,79 @@
## An Assertion Tool to verify String values
class_name GdUnitStringAssert
extends GdUnitAssert
## Verifies that the current String is equal to the given one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitStringAssert:
return self
## Verifies that the current String is equal to the given one, ignoring case considerations.
@warning_ignore("unused_parameter")
func is_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
return self
## Verifies that the current String is not equal to the given one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitStringAssert:
return self
## Verifies that the current String is not equal to the given one, ignoring case considerations.
@warning_ignore("unused_parameter")
func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
return self
## Verifies that the current String is empty, it has a length of 0.
func is_empty() -> GdUnitStringAssert:
return self
## Verifies that the current String is not empty, it has a length of minimum 1.
func is_not_empty() -> GdUnitStringAssert:
return self
## Verifies that the current String contains the given String.
@warning_ignore("unused_parameter")
func contains(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String does not contain the given String.
@warning_ignore("unused_parameter")
func not_contains(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String does not contain the given String, ignoring case considerations.
@warning_ignore("unused_parameter")
func contains_ignoring_case(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String does not contain the given String, ignoring case considerations.
@warning_ignore("unused_parameter")
func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String starts with the given prefix.
@warning_ignore("unused_parameter")
func starts_with(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String ends with the given suffix.
@warning_ignore("unused_parameter")
func ends_with(expected: String) -> GdUnitStringAssert:
return self
## Verifies that the current String has the expected length by used comparator.
@warning_ignore("unused_parameter")
func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert:
return self

View File

@ -0,0 +1,620 @@
## The main class for all GdUnit test suites[br]
## This class is the main class to implement your unit tests[br]
## You have to extend and implement your test cases as described[br]
## e.g MyTests.gd [br]
## [codeblock]
## extends GdUnitTestSuite
## # testcase
## func test_case_a():
## assert_that("value").is_equal("value")
## [/codeblock]
## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/
@icon("res://addons/gdUnit4/src/ui/assets/TestSuite.svg")
class_name GdUnitTestSuite
extends Node
const NO_ARG :Variant = GdUnitConstants.NO_ARG
### internal runtime variables that must not be overwritten!!!
@warning_ignore("unused_private_class_variable")
var __is_skipped := false
@warning_ignore("unused_private_class_variable")
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"
### in order to noticeably reduce the loading time of the test suite.
# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
# for more detailed info -> https://github.com/godotengine/godot/issues/67400
func __lazy_load(script_path :String) -> GDScript:
return GdUnitAssertions.__lazy_load(script_path)
func __gdunit_assert() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
func __gdunit_tools() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
func __gdunit_file_access() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitFileAccess.gd")
func __gdunit_awaiter() -> Object:
return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new()
func __gdunit_argument_matchers() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd")
func __gdunit_object_interactions() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitObjectInteractions.gd")
## This function is called before a test suite starts[br]
## You can overwrite to prepare test data or initalizize necessary variables
func before() -> void:
pass
## This function is called at least when a test suite is finished[br]
## You can overwrite to cleanup data created during test running
func after() -> void:
pass
## This function is called before a test case starts[br]
## You can overwrite to prepare test case specific data
func before_test() -> void:
pass
## This function is called after the test case is finished[br]
## You can overwrite to cleanup your test case specific data
func after_test() -> void:
pass
func is_failure(_expected_failure :String = NO_ARG) -> bool:
return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false
func set_active_test_case(test_case :String) -> void:
__active_test_case = test_case
# === Tools ====================================================================
# Mapps Godot error number to a readable error message. See at ERROR
# https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error
func error_as_string(error_number :int) -> String:
return error_string(error_number)
## 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
@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()
## 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:
__gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case)
## Creates a new directory under the temporary directory *user://tmp*[br]
## 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:
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:
__gdunit_file_access().clear_tmp()
## Creates a new file under the temporary directory *user://tmp* + <relative_path>[br]
## 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:
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:
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:
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()
## Waits for given signal is emited by the <source> until a specified timeout to fail[br]
## source: the object from which the signal is emitted[br]
## signal_name: signal name[br]
## 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:
return await __awaiter.await_signal_on(source, signal_name, args, timeout)
## Waits until the next idle frame
func await_idle_frame() -> void:
await __awaiter.await_idle_frame()
## Waits for for a given amount of milliseconds[br]
## example:[br]
## [codeblock]
## # waits for 100ms
## await await_millis(myNode, 100).completed
## [/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:
await __awaiter.await_millis(timeout)
## Creates a new scene runner to allow simulate interactions checked a scene.[br]
## The runner will manage the scene instance and release after the runner is released[br]
## example:[br]
## [codeblock]
## # creates a runner by using a instanciated scene
## var scene = load("res://foo/my_scne.tscn").instantiate()
## var runner := scene_runner(scene)
##
## # or simply creates a runner by using the scene resource path
## var runner := scene_runner("res://foo/my_scne.tscn")
## [/codeblock]
func scene_runner(scene :Variant, verbose := false) -> GdUnitSceneRunner:
return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose))
# === Mocking & Spy ===========================================================
## do return a default value for primitive types or null
const RETURN_DEFAULTS = GdUnitMock.RETURN_DEFAULTS
## do call the real implementation
const CALL_REAL_FUNC = GdUnitMock.CALL_REAL_FUNC
## do return a default value for primitive types and a fully mocked value for Object types
## builds full deep mocked object
const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB
## Creates a mock for given class name
func mock(clazz :Variant, mock_mode := RETURN_DEFAULTS) -> Variant:
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:
return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance)
## Configures a return value for the specified function and used arguments.[br]
## [b]Example:
## [codeblock]
## # overrides the return value of myMock.is_selected() to false
## do_return(false).on(myMock).is_selected()
## [/codeblock]
func do_return(value :Variant) -> GdUnitMock:
return GdUnitMock.new(value)
## Verifies certain behavior happened at least once or exact number of times
func verify(obj :Variant, times := 1) -> Variant:
return __gdunit_object_interactions().verify(obj, times)
## Verifies no interactions is happen checked this mock or spy
func verify_no_interactions(obj :Variant) -> GdUnitAssert:
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:
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:
__gdunit_object_interactions().reset(obj)
## Starts monitoring the specified source to collect all transmitted signals.[br]
## The collected signals can then be checked with 'assert_signal'.[br]
## By default, the specified source is automatically released when the test ends.
## You can control this behavior by setting auto_free to false if you do not want the source to be automatically freed.[br]
## Usage:
## [codeblock]
## var emitter := monitor_signals(MyEmitter.new())
## # call the function to send the signal
## emitter.do_it()
## # verify the signial is emitted
## await assert_signal(emitter).is_emitted('my_signal')
## [/codeblock]
func monitor_signals(source :Object, _auto_free := true) -> Object:
__lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\
.get_current_context()\
.get_signal_collector()\
.register_emitter(source)
return auto_free(source) if _auto_free else source
# === Argument matchers ========================================================
## Argument matcher to match any argument
func any() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().any()
## Argument matcher to match any boolean value
func any_bool() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_BOOL)
## Argument matcher to match any integer value
func any_int() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_INT)
## Argument matcher to match any float value
func any_float() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_FLOAT)
## Argument matcher to match any string value
func any_string() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_STRING)
## Argument matcher to match any Color value
func any_color() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_COLOR)
## Argument matcher to match any Vector typed value
func any_vector() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_types([
TYPE_VECTOR2,
TYPE_VECTOR2I,
TYPE_VECTOR3,
TYPE_VECTOR3I,
TYPE_VECTOR4,
TYPE_VECTOR4I,
])
## Argument matcher to match any Vector2 value
func any_vector2() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2)
## Argument matcher to match any Vector2i value
func any_vector2i() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I)
## Argument matcher to match any Vector3 value
func any_vector3() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3)
## Argument matcher to match any Vector3i value
func any_vector3i() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I)
## Argument matcher to match any Vector4 value
func any_vector4() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4)
## Argument matcher to match any Vector3i value
func any_vector4i() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I)
## Argument matcher to match any Rect2 value
func any_rect2() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_RECT2)
## Argument matcher to match any Plane value
func any_plane() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PLANE)
## Argument matcher to match any Quaternion value
func any_quat() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_QUATERNION)
## Argument matcher to match any AABB value
func any_aabb() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_AABB)
## Argument matcher to match any Basis value
func any_basis() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_BASIS)
## Argument matcher to match any Transform2D value
func any_transform_2d() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D)
## Argument matcher to match any Transform3D value
func any_transform_3d() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D)
## Argument matcher to match any NodePath value
func any_node_path() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH)
## Argument matcher to match any RID value
func any_rid() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_RID)
## Argument matcher to match any Object value
func any_object() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_OBJECT)
## Argument matcher to match any Dictionary value
func any_dictionary() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY)
## Argument matcher to match any Array value
func any_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_ARRAY)
## Argument matcher to match any PackedByteArray value
func any_packed_byte_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY)
## Argument matcher to match any PackedInt32Array value
func any_packed_int32_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY)
## Argument matcher to match any PackedInt64Array value
func any_packed_int64_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY)
## Argument matcher to match any PackedFloat32Array value
func any_packed_float32_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY)
## Argument matcher to match any PackedFloat64Array value
func any_packed_float64_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY)
## Argument matcher to match any PackedStringArray value
func any_packed_string_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY)
## Argument matcher to match any PackedVector2Array value
func any_packed_vector2_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY)
## Argument matcher to match any PackedVector3Array value
func any_packed_vector3_array() -> GdUnitArgumentMatcher:
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY)
## Argument matcher to match any PackedColorArray value
func any_packed_color_array() -> GdUnitArgumentMatcher:
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:
return __gdunit_argument_matchers().any_class(clazz)
# === value extract utils ======================================================
## Builds an extractor by given function name and optional arguments
func extr(func_name :String, args := Array()) -> GdUnitValueExtractor:
return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args)
## Constructs a tuple by given arguments
func tuple(arg0 :Variant,
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) -> GdUnitTuple:
return GdUnitTuple.new(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
# === Asserts ==================================================================
## The common assertion tool to verify values.
## It checks the given value by type to fit to the best assert
func assert_that(current :Variant) -> GdUnitAssert:
match typeof(current):
TYPE_BOOL:
return assert_bool(current)
TYPE_INT:
return assert_int(current)
TYPE_FLOAT:
return assert_float(current)
TYPE_STRING:
return assert_str(current)
TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I:
return assert_vector(current)
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)
TYPE_OBJECT, TYPE_NIL:
return assert_object(current)
_:
return __gdunit_assert().new(current)
## An assertion tool to verify boolean values.
func assert_bool(current :Variant) -> GdUnitBoolAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current)
## An assertion tool to verify String values.
func assert_str(current :Variant) -> GdUnitStringAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current)
## An assertion tool to verify integer values.
func assert_int(current :Variant) -> GdUnitIntAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current)
## An assertion tool to verify float values.
func assert_float(current :Variant) -> GdUnitFloatAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current)
## An assertion tool to verify Vector values.[br]
## This assertion supports all vector types.[br]
## Usage:
## [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)
## An assertion tool to verify arrays.
func assert_array(current :Variant) -> GdUnitArrayAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current)
## An assertion tool to verify dictionaries.
func assert_dict(current :Variant) -> GdUnitDictionaryAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current)
## An assertion tool to verify FileAccess.
func assert_file(current :Variant) -> GdUnitFileAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current)
## An assertion tool to verify Objects.
func assert_object(current :Variant) -> GdUnitObjectAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current)
func assert_result(current :Variant) -> GdUnitResultAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current)
## An assertion tool that waits until a certain time for an expected function return value
func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnitFuncAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args)
## An Assertion Tool to verify for emitted signals until a certain time.
func assert_signal(instance :Object) -> GdUnitSignalAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance)
## An assertion tool to test for failing assertions.[br]
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
## Usage:
## [codeblock]
## assert_failure(func(): assert_bool(true).is_not_equal(true)) \
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure(assertion :Callable) -> GdUnitFailureAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion)
## An assertion tool to test for failing assertions.[br]
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
## Usage:
## [codeblock]
## await assert_failure_await(func(): assert_bool(true).is_not_equal(true)) \
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert:
return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion)
## An assertion tool to verify for Godot errors.[br]
## You can use to verify for certain Godot erros like failing assertions, push_error, push_warn.[br]
## Usage:
## [codeblock]
## # tests no error was occured during execution the code
## await assert_error(func (): return 0 )\
## .is_success()
##
## # tests an push_error('test error') was occured during execution the code
## await assert_error(func (): push_error('test error') )\
## .is_push_error('test error')
## [/codeblock]
func assert_error(current :Callable) -> GdUnitGodotErrorAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current)
func assert_not_yet_implemented() -> void:
__gdunit_assert().new(null).test_fail()
func fail(message :String) -> void:
__gdunit_assert().new(null).report_error(message)
# --- internal stuff do not override!!!
func ResourcePath() -> String:
return get_script().resource_path

View File

@ -0,0 +1,28 @@
## A tuple implementation to hold two or many values
class_name GdUnitTuple
extends RefCounted
const NO_ARG :Variant = GdUnitConstants.NO_ARG
var __values :Array = Array()
func _init(arg0:Variant,
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) -> void:
__values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
func values() -> Array:
return __values
func _to_string() -> String:
return "tuple(%s)" % str(__values)

View File

@ -0,0 +1,9 @@
## This is the base interface for value extraction
class_name GdUnitValueExtractor
extends RefCounted
## Extracts a value by given implementation
func extract_value(value :Variant) -> Variant:
push_error("Uninplemented func 'extract_value'")
return value

View File

@ -0,0 +1,57 @@
## An Assertion Tool to verify Vector values
class_name GdUnitVectorAssert
extends GdUnitAssert
## Verifies that the current value is equal to expected one.
@warning_ignore("unused_parameter")
func is_equal(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is not equal to expected one.
@warning_ignore("unused_parameter")
func is_not_equal(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current and expected value are approximately equal.
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is less than the given one.
@warning_ignore("unused_parameter")
func is_less(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is less than or equal the given one.
@warning_ignore("unused_parameter")
func is_less_equal(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is greater than the given one.
@warning_ignore("unused_parameter")
func is_greater(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is greater than or equal the given one.
@warning_ignore("unused_parameter")
func is_greater_equal(expected :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is between the given boundaries (inclusive).
@warning_ignore("unused_parameter")
func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
return self
## Verifies that the current value is not between the given boundaries (inclusive).
@warning_ignore("unused_parameter")
func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
return self

View File

@ -0,0 +1,25 @@
# a value provider unsing a callback to get `next` value from a certain function
class_name CallBackValueProvider
extends ValueProvider
var _cb :Callable
var _args :Array
func _init(instance :Object, func_name :String, args :Array = Array(), force_error := true) -> void:
_cb = Callable(instance, func_name);
_args = args
if force_error and not _cb.is_valid():
push_error("Can't find function '%s' checked instance %s" % [func_name, instance])
func get_value() -> Variant:
if not _cb.is_valid():
return null
if _args.is_empty():
return await _cb.call()
return await _cb.callv(_args)
func dispose() -> void:
_cb = Callable()

View File

@ -0,0 +1,13 @@
# default value provider, simple returns the initial value
class_name DefaultValueProvider
extends ValueProvider
var _value: Variant
func _init(value: Variant) -> void:
_value = value
func get_value() -> Variant:
return _value

View File

@ -0,0 +1,615 @@
class_name GdAssertMessages
extends Resource
const WARN_COLOR = "#EFF883"
const ERROR_COLOR = "#CD5C5C"
const VALUE_COLOR = "#1E90FF"
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():
return "{ }"
var as_rows := var_to_str(value).split("\n")
for index in range( 1, as_rows.size()-1):
as_rows[index] = " " + as_rows[index]
as_rows[-1] = " " + as_rows[-1]
return "\n".join(as_rows)
# improved version of InputEvent as text
static func input_event_as_text(event :InputEvent) -> String:
var text := ""
if event is InputEventKey:
text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [
event.as_text(), event.pressed, event.keycode, event.physical_keycode]
else:
text += event.as_text()
if event is InputEventMouse:
text += ", global_position %s" % event.global_position
if event is 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]
return text
static func _colored_string_div(characters :String) -> String:
return colored_array_div(characters.to_utf8_buffer())
static func colored_array_div(characters :PackedByteArray) -> String:
if characters.is_empty():
return "<empty>"
var result := PackedByteArray()
var index := 0
var missing_chars := PackedByteArray()
var additional_chars := PackedByteArray()
while index < characters.size():
var character := characters[index]
match character:
GdDiffTool.DIV_ADD:
index += 1
additional_chars.append(characters[index])
GdDiffTool.DIV_SUB:
index += 1
missing_chars.append(characters[index])
_:
if not missing_chars.is_empty():
result.append_array(format_chars(missing_chars, SUB_COLOR))
missing_chars = PackedByteArray()
if not additional_chars.is_empty():
result.append_array(format_chars(additional_chars, ADD_COLOR))
additional_chars = PackedByteArray()
result.append(character)
index += 1
result.append_array(format_chars(missing_chars, SUB_COLOR))
result.append_array(format_chars(additional_chars, ADD_COLOR))
return result.get_string_from_utf8()
static func _typed_value(value :Variant) -> String:
return GdDefaultValueDecoder.decode(value)
static func _warning(error :String) -> String:
return "[color=%s]%s[/color]" % [WARN_COLOR, error]
static func _error(error :String) -> String:
return "[color=%s]%s[/color]" % [ERROR_COLOR, error]
static func _nerror(number :Variant) -> String:
match typeof(number):
TYPE_INT:
return "[color=%s]%d[/color]" % [ERROR_COLOR, number]
TYPE_FLOAT:
return "[color=%s]%f[/color]" % [ERROR_COLOR, number]
_:
return "[color=%s]%s[/color]" % [ERROR_COLOR, str(number)]
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)]
TYPE_INT:
return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value]
TYPE_FLOAT:
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
TYPE_COLOR:
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
TYPE_OBJECT:
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"):
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)]
return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()]
TYPE_DICTIONARY:
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)]
_:
if GdArrayTools.is_array_type(value):
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
return "'[color=%s]%s[/color]'" % [VALUE_COLOR, value]
static func _index_report_as_table(index_reports :Array) -> String:
var table := "[table=3]$cells[/table]"
var header := "[cell][right][b]$text[/b][/right]\t[/cell]"
var cell := "[cell][right]$text[/right]\t[/cell]"
var cells := header.replace("$text", "Index") + header.replace("$text", "Current") + header.replace("$text", "Expected")
for report :Variant in index_reports:
var index :String = str(report["index"])
var current :String = str(report["current"])
var expected :String = str(report["expected"])
cells += cell.replace("$text", index) + cell.replace("$text", current) + cell.replace("$text", expected)
return table.replace("$cells", cells)
static func orphan_detected_on_suite_setup(count :int) -> String:
return "%s\n Detected <%d> orphan nodes during test suite setup stage! [b]Check before() and after()![/b]" % [
_warning("WARNING:"), count]
static func orphan_detected_on_test_setup(count :int) -> String:
return "%s\n Detected <%d> orphan nodes during test setup! [b]Check before_test() and after_test()![/b]" % [
_warning("WARNING:"), count]
static func orphan_detected_on_test(count :int) -> String:
return "%s\n Detected <%d> orphan nodes during test execution!" % [
_warning("WARNING:"), count]
static func fuzzer_interuped(iterations: int, error: String) -> String:
return "%s %s %s\n %s" % [
_error("Found an error after"),
_colored_value(iterations + 1),
_error("test iterations"),
error]
static func test_timeout(timeout :int) -> String:
return "%s\n %s" % [_error("Timeout !"), _colored_value("Test timed out after %s" % LocalTime.elapsed(timeout))]
# gdlint:disable = mixed-tabs-and-spaces
static func test_suite_skipped(hint :String, skip_count :int) -> String:
return """
%s
Tests skipped: %s
Reason: %s
""".dedent().trim_prefix("\n")\
% [_error("Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)]
static func test_skipped(hint :String) -> String:
return """
%s
Reason: %s
""".dedent().trim_prefix("\n")\
% [_error("This test is skipped!"), _colored_value(hint)]
static func error_not_implemented() -> String:
return _error("Test not implemented!")
static func error_is_null(current :Variant) -> String:
return "%s %s but was %s" % [_error("Expecting:"), _colored_value(null), _colored_value(current)]
static func error_is_not_null() -> String:
return "%s %s" % [_error("Expecting: not to be"), _colored_value(null)]
static func error_equal(current :Variant, expected :Variant, index_reports :Array = []) -> String:
var report := """
%s
%s
but was
%s""".dedent().trim_prefix("\n") % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
if not index_reports.is_empty():
report += "\n\n%s\n%s" % [_error("Differences found:"), _index_report_as_table(index_reports)]
return report
static func error_not_equal(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not equal to\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_not_equal_case_insensetiv(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not equal to (case insensitiv)\n %s" % [
_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_is_empty(current :Variant) -> String:
return "%s\n must be empty but was\n %s" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_empty() -> String:
return "%s\n must not be empty" % [_error("Expecting:")]
static func error_is_same(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to refer to the same object\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
@warning_ignore("unused_parameter")
static func error_not_same(_current :Variant, expected :Variant) -> String:
return "%s\n %s" % [_error("Expecting not same:"), _colored_value(expected)]
static func error_not_same_error(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)]
static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String:
return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\
_colored_value(expected.or_else(null)), _colored_value(current.or_else(null))]
# -- Boolean Assert specific messages -----------------------------------------------------
static func error_is_true(current :Variant) -> String:
return "%s %s but is %s" % [_error("Expecting:"), _colored_value(true), _colored_value(current)]
static func error_is_false(current :Variant) -> String:
return "%s %s but is %s" % [_error("Expecting:"), _colored_value(false), _colored_value(current)]
# - Integer/Float Assert specific messages -----------------------------------------------------
static func error_is_even(current :Variant) -> String:
return "%s\n %s must be even" % [_error("Expecting:"), _colored_value(current)]
static func error_is_odd(current :Variant) -> String:
return "%s\n %s must be odd" % [_error("Expecting:"), _colored_value(current)]
static func error_is_negative(current :Variant) -> String:
return "%s\n %s be negative" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_negative(current :Variant) -> String:
return "%s\n %s be not negative" % [_error("Expecting:"), _colored_value(current)]
static func error_is_zero(current :Variant) -> String:
return "%s\n equal to 0 but is %s" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_zero() -> String:
return "%s\n not equal to 0" % [_error("Expecting:")]
static func error_is_wrong_type(current_type :Variant.Type, expected_type :Variant.Type) -> String:
return "%s\n Expecting type %s but is %s" % [
_error("Unexpected type comparison:"),
_colored_value(GdObjects.type_as_string(current_type)),
_colored_value(GdObjects.type_as_string(expected_type))]
static func error_is_value(operation :int, current :Variant, expected :Variant, expected2 :Variant = null) -> String:
match operation:
Comparator.EQUAL:
return "%s\n %s but was '%s'" % [_error("Expecting:"), _colored_value(expected), _nerror(current)]
Comparator.LESS_THAN:
return "%s\n %s but was '%s'" % [_error("Expecting to be less than:"), _colored_value(expected), _nerror(current)]
Comparator.LESS_EQUAL:
return "%s\n %s but was '%s'" % [_error("Expecting to be less than or equal:"), _colored_value(expected), _nerror(current)]
Comparator.GREATER_THAN:
return "%s\n %s but was '%s'" % [_error("Expecting to be greater than:"), _colored_value(expected), _nerror(current)]
Comparator.GREATER_EQUAL:
return "%s\n %s but was '%s'" % [_error("Expecting to be greater than or equal:"), _colored_value(expected), _nerror(current)]
Comparator.BETWEEN_EQUAL:
return "%s\n %s\n in range between\n %s <> %s" % [
_error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
Comparator.NOT_BETWEEN_EQUAL:
return "%s\n %s\n not in range between\n %s <> %s" % [
_error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
return "TODO create expected message"
static func error_is_in(current :Variant, expected :Array) -> String:
return "%s\n %s\n is in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
static func error_is_not_in(current :Variant, expected :Array) -> String:
return "%s\n %s\n is not in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
# - StringAssert ---------------------------------------------------------------------------------
static func error_equal_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s (ignoring case)" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_contains(current :Variant, expected :Variant) -> String:
return "%s\n %s\n do contains\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_not_contains(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not do contain\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_contains_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_not_contains_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not do contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_starts_with(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to start with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_ends_with(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to end with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String:
var current_length :Variant = current.length() if current != null else null
match compare_operator:
Comparator.EQUAL:
return "%s\n %s but was '%s' in\n %s" % [
_error("Expecting size:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
Comparator.LESS_THAN:
return "%s\n %s but was '%s' in\n %s" % [
_error("Expecting size to be less than:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
Comparator.LESS_EQUAL:
return "%s\n %s but was '%s' in\n %s" % [
_error("Expecting size to be less than or equal:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
Comparator.GREATER_THAN:
return "%s\n %s but was '%s' in\n %s" % [
_error("Expecting size to be greater than:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
Comparator.GREATER_EQUAL:
return "%s\n %s but was '%s' in\n %s" % [
_error("Expecting size to be greater than or equal:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
return "TODO create expected message"
# - ArrayAssert specific messgaes ---------------------------------------------------
static func error_arr_contains(current :Variant, expected :Array, not_expect :Array, not_found :Array, 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():
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"
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:
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)
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():
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"
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:
var failure_message := (
"Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME exactly elements:"
)
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():
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"
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:
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():
error += "\n but found elements:\n %s" % _colored_value(found)
return error
# - DictionaryAssert specific messages ----------------------------------------------
static func error_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME keys:"
)
return "%s\n %s\n to contains:\n %s\n but can't find key's:\n %s" % [
_error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
static func error_not_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting NOT contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting NOT contains SAME keys"
)
return "%s\n %s\n do not contains:\n %s\n but contains key's:\n %s" % [
_error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
static func error_contains_key_value(key :Variant, value :Variant, current_value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting contains key and value:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME key and value:"
)
return "%s\n %s : %s\n but contains\n %s : %s" % [
_error(failure), _colored_value(key), _colored_value(value), _colored_value(key), _colored_value(current_value)]
# - ResultAssert specific errors ----------------------------------------------------
static func error_result_is_empty(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.EMPTY)
static func error_result_is_success(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.SUCCESS)
static func error_result_is_warning(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.WARN)
static func error_result_is_error(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.ERROR)
static func error_result_has_message(current :String, expected :String) -> String:
return "%s\n %s\n but was\n %s." % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_result_has_message_on_success(expected :String) -> String:
return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)]
static func error_result_is_value(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)]
static func _result_error_message(current :GdUnitResult, expected_type :int) -> String:
if current == null:
return _error("Expecting the result must be a %s but was <null>." % result_type(expected_type))
if current.is_success():
return _error("Expecting the result must be a %s but was SUCCESS." % result_type(expected_type))
var error := "Expecting the result must be a %s but was %s:" % [result_type(expected_type), result_type(current._state)]
return "%s\n %s" % [_error(error), _colored_value(result_message(current))]
static func error_interrupted(func_name :String, expected :Variant, elapsed :String) -> String:
func_name = humanized(func_name)
if expected == null:
return "%s %s but timed out after %s" % [_error("Expected:"), func_name, elapsed]
return "%s %s %s but timed out after %s" % [_error("Expected:"), func_name, _colored_value(expected), elapsed]
static func error_wait_signal(signal_name :String, args :Array, elapsed :String) -> String:
if args.is_empty():
return "%s %s but timed out after %s" % [
_error("Expecting emit signal:"), _colored_value(signal_name + "()"), elapsed]
return "%s %s but timed out after %s" % [
_error("Expecting emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
static func error_signal_emitted(signal_name :String, args :Array, elapsed :String) -> String:
if args.is_empty():
return "%s %s but is emitted after %s" % [
_error("Expecting do not emit signal:"), _colored_value(signal_name + "()"), elapsed]
return "%s %s but is emitted after %s" % [
_error("Expecting do not emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
static func error_await_signal_on_invalid_instance(source :Variant, signal_name :String, args :Array) -> String:
return "%s\n await_signal_on(%s, %s, %s)" % [
_error("Invalid source! Can't await on signal:"), _colored_value(source), signal_name, args]
static func result_type(type :int) -> String:
match type:
GdUnitResult.SUCCESS: return "SUCCESS"
GdUnitResult.WARN: return "WARNING"
GdUnitResult.ERROR: return "ERROR"
GdUnitResult.EMPTY: return "EMPTY"
return "UNKNOWN"
static func result_message(result :GdUnitResult) -> String:
match result._state:
GdUnitResult.SUCCESS: return ""
GdUnitResult.WARN: return result.warn_message()
GdUnitResult.ERROR: return result.error_message()
GdUnitResult.EMPTY: return ""
return "UNKNOWN"
# -----------------------------------------------------------------------------------
# - Spy|Mock specific errors ----------------------------------------------------
static func error_no_more_interactions(summary :Dictionary) -> String:
var interactions := PackedStringArray()
for args :Variant in summary.keys():
var times :int = summary[args]
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])
return "%s\n%s\n%s\n%s" % [
_error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(interactions)]
static func _format_arguments(args :Array, times :int) -> String:
var fname :String = args[0]
var fargs := args.slice(1) as Array
var typed_args := _to_typed_args(fargs)
var fsignature := _colored_value("%s(%s)" % [fname, ", ".join(typed_args)])
return " %s %d time's" % [fsignature, times]
static func _to_typed_args(args :Array) -> PackedStringArray:
var typed := PackedStringArray()
for arg :Variant in args:
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)
return str(arg)
static func _find_first_diff(left :Array, right :Array) -> String:
for index in left.size():
var l :Variant = left[index]
var r :Variant = "<no entry>" if index >= right.size() else right[index]
if not GdObjects.equals(l, r):
return "at position %s\n '%s' vs '%s'" % [_colored_value(index), _typed_value(l), _typed_value(r)]
return ""
static func error_has_size(current :Variant, expected: int) -> String:
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)]
static func error_contains_exactly(current: Array, expected: Array) -> String:
return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)]
static func format_chars(characters :PackedByteArray, type :Color) -> PackedByteArray:
if characters.size() == 0:# or characters[0] == 10:
return characters
var result := PackedByteArray()
var message := "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [
type.to_html(), characters.get_string_from_utf8().replace("\n", "<LF>")]
result.append_array(message.to_utf8_buffer())
return result
static func format_invalid(value :String) -> String:
return "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [SUB_COLOR.to_html(), value]
static func humanized(value :String) -> String:
return value.replace("_", " ")

View File

@ -0,0 +1,55 @@
class_name GdAssertReports
extends RefCounted
const LAST_ERROR = "last_assert_error_message"
const LAST_ERROR_LINE = "last_assert_error_line"
static func report_success() -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
GdAssertReports.set_last_error_line_number(-1)
Engine.remove_meta(LAST_ERROR)
static func report_warning(message :String, line_number :int) -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
send_report(GdUnitReport.new().create(GdUnitReport.WARN, line_number, message))
static func report_error(message:String, line_number :int) -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(true)
GdAssertReports.set_last_error_line_number(line_number)
Engine.set_meta(LAST_ERROR, message)
# if we expect to fail we handle as success test
if _do_expect_assert_failing():
return
send_report(GdUnitReport.new().create(GdUnitReport.FAILURE, line_number, message))
static func reset_last_error_line_number() -> void:
Engine.remove_meta(LAST_ERROR_LINE)
static func set_last_error_line_number(line_number :int) -> void:
Engine.set_meta(LAST_ERROR_LINE, line_number)
static func get_last_error_line_number() -> int:
if Engine.has_meta(LAST_ERROR_LINE):
return Engine.get_meta(LAST_ERROR_LINE)
return -1
static func _do_expect_assert_failing() -> bool:
if Engine.has_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES):
return Engine.get_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES)
return false
static func current_failure() -> String:
return Engine.get_meta(LAST_ERROR)
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)

View File

@ -0,0 +1,350 @@
extends GdUnitArrayAssert
var _base :GdUnitAssert
var _current_value_provider :ValueProvider
func _init(current :Variant) -> void:
_current_value_provider = DefaultValueProvider.new(current)
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func report_success() -> GdUnitArrayAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitArrayAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitArrayAssert:
_base.override_failure_message(message)
return self
func _validate_value_type(value :Variant) -> bool:
return value == null or GdArrayTools.is_array_type(value)
func get_current_value() -> Variant:
return _current_value_provider.get_value()
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)
var index_report := Array()
for index in current_value.size():
var c := current_value[index]
if index < expected_value.size():
var e := expected_value[index]
if not GdObjects.equals(c, e, case_sensitive):
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})
else:
current_value[index] = GdAssertMessages.format_invalid(c)
index_report.push_back({"index" : index, "current" :c, "expected": "<N/A>"})
for index in range(current.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})
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]:
var not_expect := left.duplicate(true)
var not_found := right.duplicate(true)
for index_c in left.size():
var c :Variant = left[index_c]
for index_e in right.size():
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)
break
return [not_expect, not_found]
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()
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)
#var not_expect := diffs[0] as Array
var not_found := diffs[1] as Array
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:
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 current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], expected, compare_mode))
# has same content in same order
if GdObjects.equals(Array(current_value), Array(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):
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]
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:
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 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
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:
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 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():
return report_success()
var diffs2 := _array_div(compare_mode, expected, diffs[1])
return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected, diffs2[0], compare_mode))
func is_null() -> GdUnitArrayAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitArrayAssert:
_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):
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 current_value == null and expected != null:
return report_error(GdAssertMessages.error_equal(null, expected))
if not GdObjects.equals(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]
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):
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 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)
var expected_as_list := GdArrayTools.as_string(diff[0])
var current_as_list := GdArrayTools.as_string(diff[1])
var index_report :Variant = 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:
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):
return report_error(GdAssertMessages.error_not_equal(current_value, expected))
return report_success()
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)
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:
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:
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
@warning_ignore("unused_parameter", "shadowed_global_identifier")
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()
if not is_same(current, expected):
report_error(GdAssertMessages.error_is_same(current, expected))
return self
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()
if is_same(current, expected):
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:
return report_error(GdAssertMessages.error_has_size(current_value, expected))
return report_success()
func contains(expected :Variant) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
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:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_same(expected :Variant) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
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:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func not_contains(expected :Variant) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func not_contains_same(expected :Variant) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func is_instanceof(expected :Variant) -> GdUnitAssert:
_base.is_instanceof(expected)
return self
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()
if current == null:
_current_value_provider = DefaultValueProvider.new(null)
else:
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)
var extracted_elements := Array()
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] = [
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG,
GdUnitTuple.NO_ARG
]
for index :int in extractors.size():
var extractor :GdUnitValueExtractor = extractors[index]
ev[index] = extractor.extract_value(element)
if extractors.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

View File

@ -0,0 +1,72 @@
extends GdUnitAssert
var _current :Variant
var _current_error_message :String = ""
var _custom_failure_message :String = ""
func _init(current :Variant) -> void:
_current = current
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
GdAssertReports.reset_last_error_line_number()
func failure_message() -> String:
return _current_error_message
func current_value() -> Variant:
return _current
func report_success() -> GdUnitAssert:
GdAssertReports.report_success()
return self
func report_error(error_message :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)
Engine.set_meta("GD_TEST_FAILURE", true)
return self
func test_fail() -> GdUnitAssert:
return report_error(GdAssertMessages.error_not_implemented())
func override_failure_message(message :String) -> GdUnitAssert:
_custom_failure_message = message
return self
func is_equal(expected :Variant) -> GdUnitAssert:
var current :Variant = current_value()
if not GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_equal(current, expected))
return report_success()
func is_not_equal(expected :Variant) -> GdUnitAssert:
var current :Variant = current_value()
if GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_not_equal(current, expected))
return report_success()
func is_null() -> GdUnitAssert:
var current :Variant = current_value()
if current != null:
return report_error(GdAssertMessages.error_is_null(current))
return report_success()
func is_not_null() -> GdUnitAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
return report_success()

View File

@ -0,0 +1,64 @@
# Preloads all GdUnit assertions
class_name GdUnitAssertions
extends RefCounted
func _init() -> void:
# preload all gdunit assertions to speedup testsuite loading time
# gdlint:disable=private-method-call
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd")
### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
### in order to noticeably reduce the loading time of the test suite.
# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
# for more detailed info -> https://github.com/godotengine/godot/issues/67400
# gdlint:disable=function-name
static func __lazy_load(script_path :String) -> GDScript:
return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE)
static func validate_value_type(value :Variant, type :Variant.Type) -> bool:
return value == null or typeof(value) == type
# Scans the current stack trace for the root cause to extract the line number
static func get_line_number() -> int:
var stack_trace := get_stack()
if stack_trace == null or stack_trace.is_empty():
return -1
for index in stack_trace.size():
var stack_info :Dictionary = stack_trace[index]
var function :String = stack_info.get("function")
# we catch helper asserts to skip over to return the correct line number
if function.begins_with("assert_"):
continue
if function.begins_with("test_"):
return stack_info.get("line")
var source :String = stack_info.get("source")
if source.is_empty() \
or source.begins_with("user://") \
or source.ends_with("GdUnitAssert.gd") \
or source.ends_with("GdUnitAssertions.gd") \
or source.ends_with("AssertImpl.gd") \
or source.ends_with("GdUnitTestSuite.gd") \
or source.ends_with("GdUnitSceneRunnerImpl.gd") \
or source.ends_with("GdUnitObjectInteractions.gd") \
or source.ends_with("GdUnitAwaiter.gd"):
continue
return stack_info.get("line")
return -1

View File

@ -0,0 +1,76 @@
extends GdUnitBoolAssert
var _base: GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitBoolAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitBoolAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitBoolAssert:
_base.override_failure_message(message)
return self
# Verifies that the current value is null.
func is_null() -> GdUnitBoolAssert:
_base.is_null()
return self
# Verifies that the current value is not null.
func is_not_null() -> GdUnitBoolAssert:
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitBoolAssert:
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitBoolAssert:
_base.is_not_equal(expected)
return self
func is_true() -> GdUnitBoolAssert:
if current_value() != true:
return report_error(GdAssertMessages.error_is_true(current_value()))
return report_success()
func is_false() -> GdUnitBoolAssert:
if current_value() == true || current_value() == null:
return report_error(GdAssertMessages.error_is_false(current_value()))
return report_success()

View File

@ -0,0 +1,182 @@
extends GdUnitDictionaryAssert
var _base :GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitDictionaryAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func report_success() -> GdUnitDictionaryAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitDictionaryAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitDictionaryAssert:
_base.override_failure_message(message)
return self
func current_value() -> Variant:
return _base.current_value()
func is_null() -> GdUnitDictionaryAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitDictionaryAssert:
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected)))
if not GdObjects.equals(current, expected):
var c := GdAssertMessages.format_dict(current)
var e := GdAssertMessages.format_dict(expected)
var diff := GdDiffTool.string_diff(c, e)
var curent_diff := GdAssertMessages.colored_array_div(diff[1])
return report_error(GdAssertMessages.error_equal(curent_diff, e))
return report_success()
func is_not_equal(expected :Variant) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_not_equal(current, expected))
return report_success()
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected :Variant) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected)))
if not is_same(current, expected):
var c := GdAssertMessages.format_dict(current)
var e := GdAssertMessages.format_dict(expected)
var diff := GdDiffTool.string_diff(c, e)
var curent_diff := GdAssertMessages.colored_array_div(diff[1])
return report_error(GdAssertMessages.error_is_same(curent_diff, e))
return report_success()
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_not_same(expected :Variant) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if is_same(current, expected):
return report_error(GdAssertMessages.error_not_same(current, expected))
return report_success()
func is_empty() -> GdUnitDictionaryAssert:
var current :Variant = current_value()
if current == null or not current.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():
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
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:
return report_error(GdAssertMessages.error_has_size(current, expected))
return report_success()
func _contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
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))
if not keys_not_found.is_empty():
return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode))
return report_success()
func _contains_key_value(key :Variant, value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
var current :Variant = current_value()
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))
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_success()
func _not_contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
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))
if not keys_found.is_empty():
return report_error(GdAssertMessages.error_not_contains_keys(current.keys(), expected, keys_found, compare_mode))
return report_success()
func contains_keys(expected :Array) -> GdUnitDictionaryAssert:
return _contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
return _contains_key_value(key, value, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert:
return _not_contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert:
return _contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func contains_same_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
return _contains_key_value(key, value, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert:
return _not_contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func _filter_by_key(element :Variant, values :Array, compare_mode :GdObjects.COMPARE_MODE, is_not :bool = false) -> bool:
for key :Variant in values:
if GdObjects.equals(key, element, false, compare_mode):
return is_not
return !is_not

View File

@ -0,0 +1,110 @@
extends GdUnitFailureAssert
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
var _is_failed := false
var _failure_message :String
func _set_do_expect_fail(enabled :bool = true) -> void:
Engine.set_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES, enabled)
func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAssert:
# do not report any failure from the original assertion we want to test
_set_do_expect_fail(true)
var thread_context := GdUnitThreadManager.get_current_context()
thread_context.set_assert(null)
GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed)
# execute the given assertion as callable
if do_await:
await assertion.call()
else:
assertion.call()
_set_do_expect_fail(false)
# get the assert instance from current tread context
var current_assert := thread_context.get_assert()
if not is_instance_of(current_assert, GdUnitAssert):
_is_failed = true
_failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'"
return self
_failure_message = current_assert.failure_message()
return self
func execute(assertion :Callable) -> GdUnitFailureAssert:
execute_and_await(assertion, false)
return self
func _on_test_failed(value :bool) -> void:
_is_failed = value
@warning_ignore("unused_parameter")
func is_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert:
return _report_error("Not implemented")
@warning_ignore("unused_parameter")
func is_not_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert:
return _report_error("Not implemented")
func is_null() -> GdUnitFailureAssert:
return _report_error("Not implemented")
func is_not_null() -> GdUnitFailureAssert:
return _report_error("Not implemented")
func is_success() -> GdUnitFailureAssert:
if _is_failed:
return _report_error("Expect: assertion ends successfully.")
return self
func is_failed() -> GdUnitFailureAssert:
if not _is_failed:
return _report_error("Expect: assertion fails.")
return self
func has_line(expected :int) -> GdUnitFailureAssert:
var current := GdAssertReports.get_last_error_line_number()
if current != expected:
return _report_error("Expect: to failed on line '%d'\n but was '%d'." % [expected, current])
return self
func has_message(expected :String) -> GdUnitFailureAssert:
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 self
func starts_with_message(expected :String) -> GdUnitFailureAssert:
var expected_error := GdUnitTools.normalize_text(expected)
var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
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 self
func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert:
var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
GdAssertReports.report_error(error_message, line_number)
return self
func _report_success() -> GdUnitFailureAssert:
GdAssertReports.report_success()
return self

View File

@ -0,0 +1,95 @@
extends GdUnitFileAssert
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
var _base: GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func current_value() -> String:
return _base.current_value() as String
func report_success() -> GdUnitFileAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitFileAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitFileAssert:
_base.override_failure_message(message)
return self
func is_equal(expected :Variant) -> GdUnitFileAssert:
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitFileAssert:
_base.is_not_equal(expected)
return self
func is_file() -> GdUnitFileAssert:
var current := current_value()
if FileAccess.open(current, FileAccess.READ) == null:
return report_error("Is not a file '%s', error code %s" % [current, FileAccess.get_open_error()])
return report_success()
func exists() -> GdUnitFileAssert:
var current := current_value()
if not FileAccess.file_exists(current):
return report_error("The file '%s' not exists" %current)
return report_success()
func is_script() -> 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)
if not script is GDScript:
return report_error("The file '%s' is not a GdScript" % current)
return report_success()
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)
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 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)
return self

View File

@ -0,0 +1,144 @@
extends GdUnitFloatAssert
var _base: GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitFloatAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitFloatAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitFloatAssert:
_base.override_failure_message(message)
return self
func is_null() -> GdUnitFloatAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitFloatAssert:
_base.is_not_null()
return self
func is_equal(expected :float) -> GdUnitFloatAssert:
_base.is_equal(expected)
return self
func is_not_equal(expected :float) -> GdUnitFloatAssert:
_base.is_not_equal(expected)
return self
@warning_ignore("shadowed_global_identifier")
func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert:
return is_between(expected-approx, expected+approx)
func is_less(expected :float) -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current >= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
return report_success()
func is_less_equal(expected :float) -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current > expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
return report_success()
func is_greater(expected :float) -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current <= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
return report_success()
func is_greater_equal(expected :float) -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current < expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
return report_success()
func is_negative() -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current >= 0.0:
return report_error(GdAssertMessages.error_is_negative(current))
return report_success()
func is_not_negative() -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current < 0.0:
return report_error(GdAssertMessages.error_is_not_negative(current))
return report_success()
func is_zero() -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or not is_equal_approx(0.00000000, current):
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):
return report_error(GdAssertMessages.error_is_not_zero())
return report_success()
func is_in(expected :Array) -> GdUnitFloatAssert:
var current :Variant = current_value()
if not expected.has(current):
return report_error(GdAssertMessages.error_is_in(current, expected))
return report_success()
func is_not_in(expected :Array) -> GdUnitFloatAssert:
var current :Variant = current_value()
if expected.has(current):
return report_error(GdAssertMessages.error_is_not_in(current, expected))
return report_success()
func is_between(from :float, to :float) -> GdUnitFloatAssert:
var current :Variant = current_value()
if current == null or current < from or current > to:
return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
return report_success()

View File

@ -0,0 +1,159 @@
extends GdUnitFuncAssert
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const DEFAULT_TIMEOUT := 2000
var _current_value_provider :ValueProvider
var _current_error_message :String = ""
var _custom_failure_message :String = ""
var _line_number := -1
var _timeout := DEFAULT_TIMEOUT
var _interrupted := false
var _sleep_timer :Timer = null
func _init(instance :Object, func_name :String, args := Array()) -> void:
_line_number = GdUnitAssertions.get_line_number()
GdAssertReports.reset_last_error_line_number()
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
# verify at first the function name exists
if not instance.has_method(func_name):
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 report_success() -> GdUnitFuncAssert:
GdAssertReports.report_success()
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)
return self
func failure_message() -> String:
return _current_error_message
func send_report(report :GdUnitReport)-> void:
GdUnitSignals.instance().gdunit_report.emit(report)
func override_failure_message(message :String) -> GdUnitFuncAssert:
_custom_failure_message = message
return self
func wait_until(timeout := 2000) -> GdUnitFuncAssert:
if timeout <= 0:
push_warning("Invalid timeout param, alloed timeouts must be grater than 0. Use default timeout instead")
_timeout = DEFAULT_TIMEOUT
else:
_timeout = timeout
return self
func is_null() -> GdUnitFuncAssert:
await _validate_callback(cb_is_null)
return self
func is_not_null() -> GdUnitFuncAssert:
await _validate_callback(cb_is_not_null)
return self
func is_false() -> GdUnitFuncAssert:
await _validate_callback(cb_is_false)
return self
func is_true() -> GdUnitFuncAssert:
await _validate_callback(cb_is_true)
return self
func is_equal(expected :Variant) -> GdUnitFuncAssert:
await _validate_callback(cb_is_equal, expected)
return self
func is_not_equal(expected :Variant) -> GdUnitFuncAssert:
await _validate_callback(cb_is_not_equal, expected)
return self
# we need actually to define this Callable as functions otherwise we results into leaked scripts here
# this is actually a Godot bug and needs this kind of workaround
func cb_is_null(c :Variant, _e :Variant) -> bool: return c == null
func cb_is_not_null(c :Variant, _e :Variant) -> bool: return c != null
func cb_is_false(c :Variant, _e :Variant) -> bool: return c == false
func cb_is_true(c :Variant, _e :Variant) -> bool: return c == true
func cb_is_equal(c :Variant, e :Variant) -> bool: return GdObjects.equals(c,e)
func cb_is_not_equal(c :Variant, e :Variant) -> bool: return not GdObjects.equals(c, e)
func _validate_callback(predicate :Callable, expected :Variant = null) -> void:
if _interrupted:
return
GdUnitMemoryObserver.guard_instance(self)
var time_scale := Engine.get_time_scale()
var timer := Timer.new()
timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id())
Engine.get_main_loop().root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.timeout.connect(func do_interrupt() -> void:
_interrupted = true
, 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)
while true:
var current :Variant = await next_current_value()
# is interupted or predicate success
if _interrupted or predicate.call(current, expected):
break
if is_instance_valid(_sleep_timer):
_sleep_timer.start(0.05)
await _sleep_timer.timeout
_sleep_timer.stop()
await Engine.get_main_loop().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)))
else:
report_success()
_sleep_timer.free()
timer.free()
GdUnitMemoryObserver.unguard_instance(self)
func next_current_value() -> Variant:
@warning_ignore("redundant_await")
if is_instance_valid(_current_value_provider):
return await _current_value_provider.get_value()
return "invalid value"

View File

@ -0,0 +1,106 @@
extends GdUnitGodotErrorAssert
var _current_error_message :String
var _callable :Callable
func _init(callable :Callable) -> void:
# we only support Godot 4.1.x+ because of await issue https://github.com/godotengine/godot/issues/80292
assert(Engine.get_version_info().hex >= 0x40100,
"This assertion is not supported for Godot 4.0.x. Please upgrade to the minimum version Godot 4.1.0!")
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
GdAssertReports.reset_last_error_line_number()
_callable = callable
func _execute() -> Array[ErrorLogEntry]:
# execute the given code and monitor for runtime errors
if _callable == null or not _callable.is_valid():
_report_error("Invalid Callable '%s'" % _callable)
else:
await _callable.call()
return await _error_monitor().scan(true)
func _error_monitor() -> GodotGdErrorMonitor:
return GdUnitThreadManager.get_current_context().get_execution_context().error_monitor
func failure_message() -> String:
return _current_error_message
func _report_success() -> GdUnitAssert:
GdAssertReports.report_success()
return self
func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert:
var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
_current_error_message = error_message
GdAssertReports.report_error(error_message, line_number)
return self
func _has_log_entry(log_entries :Array[ErrorLogEntry], type :ErrorLogEntry.TYPE, error :String) -> bool:
for entry in log_entries:
if entry._type == type and entry._message == error:
# Erase the log entry we already handled it by this assertion, otherwise it will report at twice
_error_monitor().erase_log_entry(entry)
return true
return false
func _to_list(log_entries :Array[ErrorLogEntry]) -> String:
if log_entries.is_empty():
return "no errors"
if log_entries.size() == 1:
return log_entries[0]._message
var value := ""
for entry in log_entries:
value += "'%s'\n" % entry._message
return value
func is_success() -> GdUnitGodotErrorAssert:
var log_entries := await _execute()
if log_entries.is_empty():
return _report_success()
return _report_error("""
Expecting: no error's are ocured.
but found: '%s'
""".dedent().trim_prefix("\n") % _to_list(log_entries))
func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert:
var log_entries := await _execute()
if _has_log_entry(log_entries, ErrorLogEntry.TYPE.SCRIPT_ERROR, expected_error):
return _report_success()
return _report_error("""
Expecting: a runtime error is triggered.
message: '%s'
found: %s
""".dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)])
func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert:
var log_entries := await _execute()
if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_WARNING, expected_warning):
return _report_success()
return _report_error("""
Expecting: push_warning() is called.
message: '%s'
found: %s
""".dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)])
func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert:
var log_entries := await _execute()
if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_ERROR, expected_error):
return _report_success()
return _report_error("""
Expecting: push_error() is called.
message: '%s'
found: %s
""".dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)])

View File

@ -0,0 +1,153 @@
extends GdUnitIntAssert
var _base: GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitIntAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitIntAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitIntAssert:
_base.override_failure_message(message)
return self
func is_null() -> GdUnitIntAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitIntAssert:
_base.is_not_null()
return self
func is_equal(expected :int) -> GdUnitIntAssert:
_base.is_equal(expected)
return self
func is_not_equal(expected :int) -> GdUnitIntAssert:
_base.is_not_equal(expected)
return self
func is_less(expected :int) -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current >= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
return report_success()
func is_less_equal(expected :int) -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current > expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
return report_success()
func is_greater(expected :int) -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current <= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
return report_success()
func is_greater_equal(expected :int) -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current < expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
return report_success()
func is_even() -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current % 2 != 0:
return report_error(GdAssertMessages.error_is_even(current))
return report_success()
func is_odd() -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current % 2 == 0:
return report_error(GdAssertMessages.error_is_odd(current))
return report_success()
func is_negative() -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current >= 0:
return report_error(GdAssertMessages.error_is_negative(current))
return report_success()
func is_not_negative() -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current < 0:
return report_error(GdAssertMessages.error_is_not_negative(current))
return report_success()
func is_zero() -> GdUnitIntAssert:
var current :Variant = current_value()
if current != 0:
return report_error(GdAssertMessages.error_is_zero(current))
return report_success()
func is_not_zero() -> GdUnitIntAssert:
var current :Variant= current_value()
if current == 0:
return report_error(GdAssertMessages.error_is_not_zero())
return report_success()
func is_in(expected :Array) -> GdUnitIntAssert:
var current :Variant = current_value()
if not expected.has(current):
return report_error(GdAssertMessages.error_is_in(current, expected))
return report_success()
func is_not_in(expected :Array) -> GdUnitIntAssert:
var current :Variant = current_value()
if expected.has(current):
return report_error(GdAssertMessages.error_is_not_in(current, expected))
return report_success()
func is_between(from :int, to :int) -> GdUnitIntAssert:
var current :Variant = current_value()
if current == null or current < from or current > to:
return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
return report_success()

View File

@ -0,0 +1,109 @@
extends GdUnitObjectAssert
var _base :GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if (current != null
and (GdUnitAssertions.validate_value_type(current, TYPE_BOOL)
or GdUnitAssertions.validate_value_type(current, TYPE_INT)
or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT)
or GdUnitAssertions.validate_value_type(current, TYPE_STRING))):
report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitObjectAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitObjectAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitObjectAssert:
_base.override_failure_message(message)
return self
func is_equal(expected :Variant) -> GdUnitObjectAssert:
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitObjectAssert:
_base.is_not_equal(expected)
return self
func is_null() -> GdUnitObjectAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitObjectAssert:
_base.is_not_null()
return self
@warning_ignore("shadowed_global_identifier")
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
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
func is_instanceof(type :Object) -> GdUnitObjectAssert:
var current :Variant = current_value()
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
func is_not_instanceof(type :Variant) -> GdUnitObjectAssert:
var current :Variant = current_value()
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 self
report_success()
return self

View File

@ -0,0 +1,122 @@
extends GdUnitResultAssert
var _base :GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func validate_value_type(value :Variant) -> bool:
return value == null or value is GdUnitResult
func current_value() -> GdUnitResult:
return _base.current_value() as GdUnitResult
func report_success() -> GdUnitResultAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitResultAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitResultAssert:
_base.override_failure_message(message)
return self
func is_null() -> GdUnitResultAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitResultAssert:
_base.is_not_null()
return self
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
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
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
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
func contains_message(expected :String) -> GdUnitResultAssert:
var result := current_value()
if result == null:
report_error(GdAssertMessages.error_result_has_message("<null>", expected))
return self
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
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
func is_equal(expected :Variant) -> GdUnitResultAssert:
return is_value(expected)

View File

@ -0,0 +1,110 @@
extends GdUnitSignalAssert
const DEFAULT_TIMEOUT := 2000
var _signal_collector :GdUnitSignalCollector
var _emitter :Object
var _current_error_message :String = ""
var _custom_failure_message :String = ""
var _line_number := -1
var _timeout := DEFAULT_TIMEOUT
var _interrupted := false
func _init(emitter :Object) -> void:
# save the actual assert instance on the current thread context
var context := GdUnitThreadManager.get_current_context()
context.set_assert(self)
_signal_collector = context.get_signal_collector()
_line_number = GdUnitAssertions.get_line_number()
_emitter = emitter
GdAssertReports.reset_last_error_line_number()
func report_success() -> GdUnitAssert:
GdAssertReports.report_success()
return self
func report_warning(message :String) -> GdUnitAssert:
GdAssertReports.report_warning(message, GdUnitAssertions.get_line_number())
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)
return self
func failure_message() -> String:
return _current_error_message
func send_report(report :GdUnitReport)-> void:
GdUnitSignals.instance().gdunit_report.emit(report)
func override_failure_message(message :String) -> GdUnitSignalAssert:
_custom_failure_message = message
return self
func wait_until(timeout := 2000) -> GdUnitSignalAssert:
if timeout <= 0:
report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!")
_timeout = DEFAULT_TIMEOUT
else:
_timeout = timeout
return self
# Verifies the signal exists checked the emitter
func is_signal_exists(signal_name :String) -> GdUnitSignalAssert:
if not _emitter.has_signal(signal_name):
report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()])
return self
# Verifies that given signal is emitted until waiting time
func is_emitted(name :String, args := []) -> GdUnitSignalAssert:
_line_number = GdUnitAssertions.get_line_number()
return await _wail_until_signal(name, args, false)
# Verifies that given signal is NOT emitted until waiting time
func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert:
_line_number = GdUnitAssertions.get_line_number()
return await _wail_until_signal(name, args, true)
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
# 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
_signal_collector.register_emitter(_emitter)
var time_scale := Engine.get_time_scale()
var timer := Timer.new()
Engine.get_main_loop().root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
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
if is_instance_valid(_emitter):
is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args)
if is_signal_emitted and expect_not_emitted:
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:
report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout)))
timer.free()
if is_instance_valid(_emitter):
_signal_collector.reset_received_signals(_emitter, signal_name, expected_args)
return self

View File

@ -0,0 +1,173 @@
extends GdUnitStringAssert
var _base :GdUnitAssert
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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:
report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func failure_message() -> String:
return _base._current_error_message
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitStringAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitStringAssert:
_base.report_error(error)
return self
func override_failure_message(message :String) -> GdUnitStringAssert:
_base.override_failure_message(message)
return self
func is_null() -> GdUnitStringAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitStringAssert:
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_equal(current, expected))
if not GdObjects.equals(current, expected):
var diffs := GdDiffTool.string_diff(current, expected)
var formatted_current := GdAssertMessages.colored_array_div(diffs[1])
return report_error(GdAssertMessages.error_equal(formatted_current, expected))
return report_success()
func is_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_equal_ignoring_case(current, expected))
if not GdObjects.equals(str(current), expected, true):
var diffs := GdDiffTool.string_diff(current, expected)
var formatted_current := GdAssertMessages.colored_array_div(diffs[1])
return report_error(GdAssertMessages.error_equal_ignoring_case(formatted_current, expected))
return report_success()
func is_not_equal(expected :Variant) -> GdUnitStringAssert:
var current :Variant = current_value()
if GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_not_equal(current, expected))
return report_success()
func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
var current :Variant = current_value()
if GdObjects.equals(current, expected, true):
return report_error(GdAssertMessages.error_not_equal(current, expected))
return report_success()
func is_empty() -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null or not current.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():
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:
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:
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:
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:
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:
return report_error(GdAssertMessages.error_starts_with(current, expected))
return report_success()
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:
return report_error(GdAssertMessages.error_ends_with(current, expected))
return report_success()
# gdlint:disable=max-returns
func has_length(expected :int, comparator := Comparator.EQUAL) -> GdUnitStringAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
match comparator:
Comparator.EQUAL:
if current.length() != expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
Comparator.LESS_THAN:
if current.length() >= expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
Comparator.LESS_EQUAL:
if current.length() > expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
Comparator.GREATER_THAN:
if current.length() <= expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
Comparator.GREATER_EQUAL:
if current.length() < expected:
return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
_:
return report_error("Comparator '%d' not implemented!" % comparator)
return report_success()

View File

@ -0,0 +1,172 @@
extends GdUnitVectorAssert
var _base: GdUnitAssert
var _current_type :int
func _init(current :Variant) -> void:
_base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript",
ResourceLoader.CACHE_MODE_REUSE).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):
report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current))
_current_type = typeof(current)
func _notification(event :int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func _validate_value_type(value :Variant) -> bool:
return (
value == null
or typeof(value) in [
TYPE_VECTOR2,
TYPE_VECTOR2I,
TYPE_VECTOR3,
TYPE_VECTOR3I,
TYPE_VECTOR4,
TYPE_VECTOR4I
]
)
func _validate_is_vector_type(value :Variant) -> bool:
var type := typeof(value)
if type == _current_type or _current_type == TYPE_NIL:
return true
report_error(GdAssertMessages.error_is_wrong_type(_current_type, type))
return false
func current_value() -> Variant:
return _base.current_value()
func report_success() -> GdUnitVectorAssert:
_base.report_success()
return self
func report_error(error :String) -> GdUnitVectorAssert:
_base.report_error(error)
return self
func failure_message() -> String:
return _base._current_error_message
func override_failure_message(message :String) -> GdUnitVectorAssert:
_base.override_failure_message(message)
return self
func is_null() -> GdUnitVectorAssert:
_base.is_null()
return self
func is_not_null() -> GdUnitVectorAssert:
_base.is_not_null()
return self
func is_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
_base.is_equal(expected)
return self
func is_not_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
_base.is_not_equal(expected)
return self
@warning_ignore("shadowed_global_identifier")
func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected) or not _validate_is_vector_type(approx):
return self
var current :Variant = current_value()
var from :Variant = expected - approx
var to :Variant = expected + approx
if current == null or (not _is_equal_approx(current, from, to)):
return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
return report_success()
func _is_equal_approx(current :Variant, from :Variant, to :Variant) -> bool:
match typeof(current):
TYPE_VECTOR2, TYPE_VECTOR2I:
return ((current.x >= from.x and current.y >= from.y)
and (current.x <= to.x and current.y <= to.y))
TYPE_VECTOR3, TYPE_VECTOR3I:
return ((current.x >= from.x and current.y >= from.y and current.z >= from.z)
and (current.x <= to.x and current.y <= to.y and current.z <= to.z))
TYPE_VECTOR4, TYPE_VECTOR4I:
return ((current.x >= from.x and current.y >= from.y and current.z >= from.z and current.w >= from.w)
and (current.x <= to.x and current.y <= to.y and current.z <= to.z and current.w <= to.w))
_:
push_error("Missing implementation '_is_equal_approx' for vector type %s" % typeof(current))
return false
func is_less(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
var current :Variant = current_value()
if current == null or current >= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
return report_success()
func is_less_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
var current :Variant = current_value()
if current == null or current > expected:
return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
return report_success()
func is_greater(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
var current :Variant = current_value()
if current == null or current <= expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
return report_success()
func is_greater_equal(expected :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(expected):
return self
var current :Variant = current_value()
if current == null or current < expected:
return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
return report_success()
func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(from) or not _validate_is_vector_type(to):
return self
var current :Variant = current_value()
if current == null or not (current >= from and current <= to):
return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
return report_success()
func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
if not _validate_is_vector_type(from) or not _validate_is_vector_type(to):
return self
var current :Variant = current_value()
if (current != null and current >= from and current <= to):
return report_error(GdAssertMessages.error_is_value(Comparator.NOT_BETWEEN_EQUAL, current, from, to))
return report_success()

View File

@ -0,0 +1,6 @@
# base interface for assert value provider
class_name ValueProvider
extends RefCounted
func get_value() -> Variant:
return null

View File

@ -0,0 +1,61 @@
class_name CmdArgumentParser
extends RefCounted
var _options :CmdOptions
var _tool_name :String
var _parsed_commands :Dictionary = Dictionary()
func _init(p_options :CmdOptions, p_tool_name :String) -> void:
_options = p_options
_tool_name = p_tool_name
func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult:
_parsed_commands.clear()
# parse until first program argument
while not args.is_empty():
var arg :String = args.pop_front()
if arg.find(_tool_name) != -1:
break
if args.is_empty():
return GdUnitResult.empty()
# now parse all arguments
while not args.is_empty():
var cmd :String = args.pop_front()
var option := _options.get_option(cmd)
if option:
if _parse_cmd_arguments(option, args) == -1:
return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command())
elif not ignore_unknown_cmd:
return GdUnitResult.error("Unknown '%s' command!" % cmd)
return GdUnitResult.success(_parsed_commands.values())
func options() -> CmdOptions:
return _options
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))
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())
elif not option.is_argument_optional():
return -1
_parsed_commands[command_name] = command
return 0
func _is_next_value_argument(args :Array) -> bool:
if args.is_empty():
return false
return _options.get_option(args[0]) == null

View File

@ -0,0 +1,26 @@
class_name CmdCommand
extends RefCounted
var _name: String
var _arguments: PackedStringArray
func _init(p_name :String, p_arguments := []) -> void:
_name = p_name
_arguments = PackedStringArray(p_arguments)
func name() -> String:
return _name
func arguments() -> PackedStringArray:
return _arguments
func add_argument(arg :String) -> void:
_arguments.append(arg)
func _to_string() -> String:
return "%s:%s" % [_name, ", ".join(_arguments)]

View File

@ -0,0 +1,104 @@
class_name CmdCommandHandler
extends RefCounted
const CB_SINGLE_ARG = 0
const CB_MULTI_ARGS = 1
const NO_CB := Callable()
var _cmd_options :CmdOptions
# holds the command callbacks by key:<cmd_name>:String and value: [<cb single arg>, <cb multible args>]:Array
# Dictionary[String, Array[Callback]
var _command_cbs :Dictionary
# we only able to check cb function name since Godot 3.3.x
var _enhanced_fr_test := false
func _init(cmd_options: CmdOptions) -> void:
_cmd_options = cmd_options
var major: int = Engine.get_version_info()["major"]
var minor: int = Engine.get_version_info()["minor"]
if major == 3 and minor == 3:
_enhanced_fr_test = true
# register a callback function for given command
# cmd_name short name of the command
# fr_arg a funcref to a function with a single argument
func register_cb(cmd_name: String, cb: Callable = NO_CB) -> CmdCommandHandler:
var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB])
if registered_cb[CB_SINGLE_ARG]:
push_error("A function for command '%s' is already registered!" % cmd_name)
return self
registered_cb[CB_SINGLE_ARG] = cb
_command_cbs[cmd_name] = registered_cb
return self
# register a callback function for given command
# cb a funcref to a function with a variable number of arguments but expects all parameters to be passed via a single Array.
func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler:
var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB])
if registered_cb[CB_MULTI_ARGS]:
push_error("A function for command '%s' is already registered!" % cmd_name)
return self
registered_cb[CB_MULTI_ARGS] = cb
_command_cbs[cmd_name] = registered_cb
return self
func _validate() -> GdUnitResult:
var errors: = PackedStringArray()
# Dictionary[StringName, String]
var registered_cbs: = Dictionary()
for cmd_name in _command_cbs.keys() as Array[String]:
var cb: Callable = (_command_cbs[cmd_name][CB_SINGLE_ARG]
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():
errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name)
if _cmd_options.get_option(cmd_name) == null:
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]
errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd])
else:
registered_cbs[cb_method] = cmd_name
if errors.is_empty():
return GdUnitResult.success(true)
return GdUnitResult.error("\n".join(errors))
func execute(commands :Array[CmdCommand]) -> GdUnitResult:
var result := _validate()
if result.is_error():
return result
for cmd in commands:
var cmd_name := cmd.name()
if _command_cbs.has(cmd_name):
var cb_s :Callable = _command_cbs.get(cmd_name)[CB_SINGLE_ARG]
var arguments := cmd.arguments()
var cmd_option := _cmd_options.get_option(cmd_name)
if cb_s and arguments.size() == 0:
cb_s.call()
elif cb_s:
if cmd_option.type() == TYPE_BOOL:
cb_s.call(true if arguments[0] == "true" else false)
else:
cb_s.call(arguments[0])
else:
var cb_m :Callable = _command_cbs.get(cmd_name)[CB_MULTI_ARGS]
# 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:
cb_m.callv(arguments)
break
else:
cb_m.call(arguments)
break
return GdUnitResult.success(true)

View File

@ -0,0 +1,145 @@
# prototype of console with CSI support
# https://notes.burke.libbey.me/ansi-escape-codes/
class_name CmdConsole
extends RefCounted
enum {
COLOR_TABLE,
COLOR_RGB
}
const BOLD = 0x1
const ITALIC = 0x2
const UNDERLINE = 0x4
const CSI_BOLD = ""
const CSI_ITALIC = ""
const CSI_UNDERLINE = ""
# Control Sequence Introducer
var _debug_show_color_codes := false
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] )
return self
func save_cursor() -> CmdConsole:
printraw("")
return self
func restore_cursor() -> CmdConsole:
printraw("")
return self
func end_color() -> CmdConsole:
printraw("")
return self
func row_pos(row :int) -> CmdConsole:
printraw("[%d;0H" % row )
return self
func scroll_area(from :int, to :int) -> CmdConsole:
printraw("[%d;%dr" % [from ,to])
return self
func progress_bar(p_progress :int, p_color :Color = Color.POWDER_BLUE) -> CmdConsole:
if p_progress < 0:
p_progress = 0
if p_progress > 100:
p_progress = 100
color(p_color)
printraw("[%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "").rpad(50, "-"), p_progress])
end_color()
return self
func printl(value :String) -> CmdConsole:
printraw(value)
return self
func new_line() -> CmdConsole:
prints()
return self
func reset() -> CmdConsole:
return self
func bold(enable :bool) -> CmdConsole:
if enable:
printraw(CSI_BOLD)
return self
func italic(enable :bool) -> CmdConsole:
if enable:
printraw(CSI_ITALIC)
return self
func underline(enable :bool) -> CmdConsole:
if enable:
printraw(CSI_UNDERLINE)
return self
func prints_error(message :String) -> CmdConsole:
return color(Color.CRIMSON).printl(message).end_color().new_line()
func prints_warning(message :String) -> CmdConsole:
return color(Color.GOLDENROD).printl(message).end_color().new_line()
func prints_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole:
return print_color(p_message, p_color, p_flags).new_line()
func print_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole:
return color(p_color)\
.bold(p_flags&BOLD == BOLD)\
.italic(p_flags&ITALIC == ITALIC)\
.underline(p_flags&UNDERLINE == UNDERLINE)\
.printl(p_message)\
.end_color()
func print_color_table() -> void:
prints_color("Color Table 6x6x6", Color.ANTIQUE_WHITE)
_debug_show_color_codes = true
for green in range(0, 6):
for red in range(0, 6):
for blue in range(0, 6):
print_color("████████ ", Color8(red*42, green*42, blue*42))
new_line()
new_line()
prints_color("Color Table RGB", Color.ANTIQUE_WHITE)
_color_mode = COLOR_RGB
for green in range(0, 6):
for red in range(0, 6):
for blue in range(0, 6):
print_color("████████ ", Color8(red*42, green*42, blue*42))
new_line()
new_line()
_color_mode = COLOR_TABLE
_debug_show_color_codes = false

View File

@ -0,0 +1,61 @@
class_name CmdOption
extends RefCounted
var _commands :PackedStringArray
var _help :String
var _description :String
var _type :int
var _arg_optional :bool = false
# constructs a command option by given arguments
# commands : a string with comma separated list of available commands begining with the short form
# help: a help text show howto use
# description: a full description of the command
# type: the argument type
# arg_optional: defines of the argument optional
func _init(p_commands :String, p_help :String, p_description :String, p_type :int = TYPE_NIL, p_arg_optional :bool = false) -> void:
_commands = p_commands.replace(" ", "").replace("\t", "").split(",")
_help = p_help
_description = p_description
_type = p_type
_arg_optional = p_arg_optional
func commands() -> PackedStringArray:
return _commands
func short_command() -> String:
return _commands[0]
func help() -> String:
return _help
func description() -> String:
return _description
func type() -> int:
return _type
func is_argument_optional() -> bool:
return _arg_optional
func has_argument() -> bool:
return _type != TYPE_NIL
func describe() -> String:
if help().is_empty():
return " %-32s %s \n" % [commands(), description()]
return " %-32s %s \n %-32s %s\n" % [commands(), description(), "", help()]
func _to_string() -> String:
return describe()

View File

@ -0,0 +1,31 @@
class_name CmdOptions
extends RefCounted
var _default_options :Array[CmdOption]
var _advanced_options :Array[CmdOption]
func _init(p_options :Array[CmdOption] = [], p_advanced_options :Array[CmdOption] = []) -> void:
# default help options
_default_options = p_options
_advanced_options = p_advanced_options
func default_options() -> Array[CmdOption]:
return _default_options
func advanced_options() -> Array[CmdOption]:
return _advanced_options
func options() -> Array[CmdOption]:
return default_options() + advanced_options()
func get_option(cmd :String) -> CmdOption:
for option in options():
if Array(option.commands()).has(cmd):
return option
return null

View File

@ -0,0 +1,101 @@
## Small helper tool to work with Godot Arrays
class_name GdArrayTools
extends RefCounted
const max_elements := 32
const ARRAY_TYPES := [
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
]
static func is_array_type(value :Variant) -> bool:
return is_type_array(typeof(value))
static func is_type_array(type :int) -> bool:
return type in ARRAY_TYPES
## 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:
if not is_array_type(array):
return null
var filtered_array :Variant = array.duplicate()
var index :int = filtered_array.find(value)
while index != -1:
filtered_array.remove_at(index)
index = filtered_array.find(value)
return filtered_array
## Erases a value from given array by using equals(l,r) to find the element to erase
static func erase_value(array :Array, value :Variant) -> void:
for element :Variant in array:
if GdObjects.equals(element, value):
array.erase(element)
## Scans for the array build in type on a untyped array[br]
## Returns the buildin type by scan all values and returns the type if all values has the same type.
## If the values has different types TYPE_VARIANT is returend
static func scan_typed(array :Array) -> int:
if array.is_empty():
return TYPE_NIL
var actual_type := GdObjects.TYPE_VARIANT
for value :Variant in array:
var current_type := typeof(value)
if not actual_type in [GdObjects.TYPE_VARIANT, current_type]:
return GdObjects.TYPE_VARIANT
actual_type = current_type
return actual_type
## Converts given array into a string presentation.[br]
## This function is different to the original Godot str(<array>) implementation.
## The string presentaion contains fullquallified typed informations.
##[br]
## Examples:
## [codeblock]
## # will result in PackedString(["a", "b"])
## GdArrayTools.as_string(PackedStringArray("a", "b"))
## # 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!"
var delemiter := ", "
if elements == null:
return "<null>"
if elements.is_empty():
return "<empty>"
var prefix := _typeof_as_string(elements) if encode_value else ""
var formatted := ""
var index := 0
for element :Variant in elements:
if max_elements != -1 and index > max_elements:
return prefix + "[" + formatted + delemiter + "...]"
if formatted.length() > 0 :
formatted += delemiter
formatted += GdDefaultValueDecoder.decode(element) if encode_value else str(element)
index += 1
return prefix + "[" + formatted + "]"
static func _typeof_as_string(value :Variant) -> String:
var type := typeof(value)
# for untyped array we retun empty string
if type == TYPE_ARRAY:
return ""
return GdObjects.typeof_as_string(value)

View File

@ -0,0 +1,155 @@
# A tool to find differences between two objects
class_name GdDiffTool
extends RefCounted
const DIV_ADD :int = 214
const DIV_SUB :int = 215
static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff: Array, rdiff: Array) -> void:
var loffset := lb.size()
var roffset := rb.size()
while true:
#if last character of X and Y matches
if loffset > 0 && roffset > 0 && lb[loffset - 1] == rb[roffset - 1]:
loffset -= 1
roffset -= 1
ldiff.push_front(lb[loffset])
rdiff.push_front(rb[roffset])
continue
#current character of Y is not present in X
else: if (roffset > 0 && (loffset == 0 || lookup[loffset][roffset - 1] >= lookup[loffset - 1][roffset])):
roffset -= 1
ldiff.push_front(rb[roffset])
ldiff.push_front(DIV_ADD)
rdiff.push_front(rb[roffset])
rdiff.push_front(DIV_SUB)
continue
#current character of X is not present in Y
else: if (loffset > 0 && (roffset == 0 || lookup[loffset][roffset - 1] < lookup[loffset - 1][roffset])):
loffset -= 1
ldiff.push_front(lb[loffset])
ldiff.push_front(DIV_SUB)
rdiff.push_front(lb[loffset])
rdiff.push_front(DIV_ADD)
continue
break
# 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()
lookup.resize(lb.size() + 1)
for i in lookup.size():
var x := []
x.resize(rb.size() + 1)
lookup[i] = x
return lookup
static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array:
var lookup := _createLookUp(lb, rb)
# first column of the lookup table will be all 0
for i in lookup.size():
lookup[i][0] = 0
# first row of the lookup table will be all 0
for j :int in lookup[0].size():
lookup[0][j] = 0
# fill the lookup table in bottom-up manner
for i in range(1, lookup.size()):
for j in range(1, lookup[0].size()):
# if current character of left and right matches
if lb[i - 1] == rb[j - 1]:
lookup[i][j] = lookup[i - 1][j - 1] + 1;
# else if current character of left and right don't match
else:
lookup[i][j] = max(lookup[i - 1][j], lookup[i][j - 1]);
return lookup
static func string_diff(left :Variant, right :Variant) -> Array[PackedByteArray]:
var lb := PackedByteArray() if left == null else str(left).to_utf8_buffer()
var rb := PackedByteArray() if right == null else str(right).to_utf8_buffer()
var ldiff := Array()
var rdiff := Array()
var lookup := _buildLookup(lb, rb);
_diff(lb, rb, lookup, ldiff, rdiff)
return [PackedByteArray(ldiff), PackedByteArray(rdiff)]
# prototype
static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray:
var text1Words := text1.split(" ")
var text2Words := text2.split(" ")
var text1WordCount := text1Words.size()
var text2WordCount := text2Words.size()
var solutionMatrix := Array()
for i in text1WordCount+1:
var ar := Array()
for n in text2WordCount+1:
ar.append(0)
solutionMatrix.append(ar)
for i in range(text1WordCount-1, 0, -1):
for j in range(text2WordCount-1, 0, -1):
if text1Words[i] == text2Words[j]:
solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1;
else:
solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]);
var i := 0
var j := 0
var lcsResultList := PackedStringArray();
while (i < text1WordCount && j < text2WordCount):
if text1Words[i] == text2Words[j]:
lcsResultList.append(text2Words[j])
i += 1
j += 1
else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]):
i += 1
else:
j += 1
return lcsResultList
static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String:
var stringBuffer := ""
if text1 == null and lcsList == null:
return stringBuffer
var text1Words := text1.split(" ")
var text2Words := text2.split(" ")
var i := 0
var j := 0
var word1LastIndex := 0
var word2LastIndex := 0
for k in lcsList.size():
while i < text1Words.size() and j < text2Words.size():
if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]:
stringBuffer += "<SPAN>" + lcsList[k] + " </SPAN>"
word1LastIndex = i + 1
word2LastIndex = j + 1
i = text1Words.size()
j = text2Words.size()
else: if text1Words[i] != lcsList[k]:
while i < text1Words.size() and text1Words[i] != lcsList[k]:
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + deleteColor.to_html() + "'>" + text1Words[i] + " </SPAN>"
i += 1
else: if text2Words[j] != lcsList[k]:
while j < text2Words.size() and text2Words[j] != lcsList[k]:
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + insertColor.to_html() + "'>" + text2Words[j] + " </SPAN>"
j += 1
i = word1LastIndex
j = word2LastIndex
while word1LastIndex < text1Words.size():
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + deleteColor.to_html() + "'>" + text1Words[word1LastIndex] + " </SPAN>"
word1LastIndex += 1
while word2LastIndex < text2Words.size():
stringBuffer += "<SPAN style='BACKGROUND-COLOR:" + insertColor.to_html() + "'>" + text2Words[word2LastIndex] + " </SPAN>"
word2LastIndex += 1
return stringBuffer

View File

@ -0,0 +1,189 @@
class_name GdFunctionDoubler
extends RefCounted
const DEFAULT_TYPED_RETURN_VALUES := {
TYPE_NIL: "null",
TYPE_BOOL: "false",
TYPE_INT: "0",
TYPE_FLOAT: "0.0",
TYPE_STRING: "\"\"",
TYPE_STRING_NAME: "&\"\"",
TYPE_VECTOR2: "Vector2.ZERO",
TYPE_VECTOR2I: "Vector2i.ZERO",
TYPE_RECT2: "Rect2()",
TYPE_RECT2I: "Rect2i()",
TYPE_VECTOR3: "Vector3.ZERO",
TYPE_VECTOR3I: "Vector3i.ZERO",
TYPE_VECTOR4: "Vector4.ZERO",
TYPE_VECTOR4I: "Vector4i.ZERO",
TYPE_TRANSFORM2D: "Transform2D()",
TYPE_PLANE: "Plane()",
TYPE_QUATERNION: "Quaternion()",
TYPE_AABB: "AABB()",
TYPE_BASIS: "Basis()",
TYPE_TRANSFORM3D: "Transform3D()",
TYPE_PROJECTION: "Projection()",
TYPE_COLOR: "Color()",
TYPE_NODE_PATH: "NodePath()",
TYPE_RID: "RID()",
TYPE_OBJECT: "null",
TYPE_CALLABLE: "Callable()",
TYPE_SIGNAL: "Signal()",
TYPE_DICTIONARY: "Dictionary()",
TYPE_ARRAY: "Array()",
TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()",
TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()",
TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()",
TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()",
TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()",
TYPE_PACKED_STRING_ARRAY: "PackedStringArray()",
TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()",
TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()",
TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()",
GdObjects.TYPE_VARIANT: "null",
GdObjects.TYPE_ENUM: "0"
}
# @GlobalScript enums
# needs to manually map because of https://github.com/godotengine/godot/issues/73835
const DEFAULT_ENUM_RETURN_VALUES = {
"Side" : "SIDE_LEFT",
"Corner" : "CORNER_TOP_LEFT",
"Orientation" : "HORIZONTAL",
"ClockDirection" : "CLOCKWISE",
"HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT",
"VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP",
"InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO",
"EulerOrder" : "EULER_ORDER_XYZ",
"Key" : "KEY_NONE",
"KeyModifierMask" : "KEY_CODE_MASK",
"MouseButton" : "MOUSE_BUTTON_NONE",
"MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT",
"JoyButton" : "JOY_BUTTON_INVALID",
"JoyAxis" : "JOY_AXIS_INVALID",
"MIDIMessage" : "MIDI_MESSAGE_NONE",
"Error" : "OK",
"PropertyHint" : "PROPERTY_HINT_NONE",
"Variant.Type" : "TYPE_NIL",
}
var _push_errors :String
# Determine the enum default by reflection
static func get_enum_default(value :String) -> Variant:
var script := GDScript.new()
script.source_code = """
extends Resource
static func get_enum_default() -> Variant:
return %s.values()[0]
""".dedent() % value
script.reload()
return script.new().call("get_enum_default")
static func default_return_value(func_descriptor :GdFunctionDescriptor) -> String:
var return_type :Variant = func_descriptor.return_type()
if return_type == GdObjects.TYPE_ENUM:
var enum_class := func_descriptor._return_class
var enum_path := enum_class.split(".")
if enum_path.size() >= 2:
var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1])
if not keys.is_empty():
return "%s.%s" % [enum_path[0], keys[0]]
var enum_value :Variant = get_enum_default(enum_class)
if enum_value != null:
return str(enum_value)
# we need fallback for @GlobalScript enums,
return DEFAULT_ENUM_RETURN_VALUES.get(func_descriptor._return_class, "0")
return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid")
func _init(push_errors :bool = false) -> void:
_push_errors = "true" if push_errors else "false"
for type_key in TYPE_MAX:
if not DEFAULT_TYPED_RETURN_VALUES.has(type_key):
push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX])
prints("missing default definition for type", type_key)
assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!")
@warning_ignore("unused_parameter")
func get_template(return_type :Variant, is_vararg :bool) -> String:
push_error("Must be implemented!")
return ""
func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray:
var func_signature := func_descriptor.typeless()
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 vararg_names := extract_arg_names(varargs)
# save original constructor arguments
if func_name == "_init":
var constructor_args := ",".join(GdFunctionDoubler.extract_constructor_args(args))
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'
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
# 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")
double_src += func_template\
.replace("$(arguments)", ", ".join(arg_names))\
.replace("$(varargs)", ", ".join(vararg_names))\
.replace("$(await)", GdFunctionDoubler.await_is_coroutine(is_coroutine)) \
.replace("$(func_name)", func_name )\
.replace("${default_return_value}", return_value)\
.replace("$(push_errors)", _push_errors)
if is_static:
double_src = double_src.replace("$(instance)", "__instance().")
else:
double_src = double_src.replace("$(instance)", "")
return double_src.split("\n")
func extract_arg_names(argument_signatures :Array[GdFunctionArgument]) -> PackedStringArray:
var arg_names := PackedStringArray()
for arg in argument_signatures:
arg_names.append(arg._name)
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 default_value := get_default(arg)
if default_value == "null":
constructor_args.append(arg_name + ":Variant=" + default_value)
else:
constructor_args.append(arg_name + ":=" + default_value)
return constructor_args
static func get_default(arg :GdFunctionArgument) -> String:
if arg.has_default():
return arg.value_as_string()
else:
return DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null")
static func await_is_coroutine(is_coroutine :bool) -> String:
return "await " if is_coroutine else ""

View File

@ -0,0 +1,691 @@
# This is a helper class to compare two objects by equals
class_name GdObjects
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
# missing Godot types
const TYPE_CONTROL = TYPE_MAX + 2002
const TYPE_CANVAS = TYPE_MAX + 2003
const TYPE_ENUM = TYPE_MAX + 2004
# used as default value for varargs
const TYPE_VARARG_PLACEHOLDER_VALUE = "__null__"
const TYPE_AS_STRING_MAPPINGS := {
TYPE_NIL: "null",
TYPE_BOOL: "bool",
TYPE_INT: "int",
TYPE_FLOAT: "float",
TYPE_STRING: "String",
TYPE_VECTOR2: "Vector2",
TYPE_VECTOR2I: "Vector2i",
TYPE_RECT2: "Rect2",
TYPE_RECT2I: "Rect2i",
TYPE_VECTOR3: "Vector3",
TYPE_VECTOR3I: "Vector3i",
TYPE_TRANSFORM2D: "Transform2D",
TYPE_VECTOR4: "Vector4",
TYPE_VECTOR4I: "Vector4i",
TYPE_PLANE: "Plane",
TYPE_QUATERNION: "Quaternion",
TYPE_AABB: "AABB",
TYPE_BASIS: "Basis",
TYPE_TRANSFORM3D: "Transform3D",
TYPE_PROJECTION: "Projection",
TYPE_COLOR: "Color",
TYPE_STRING_NAME: "StringName",
TYPE_NODE_PATH: "NodePath",
TYPE_RID: "RID",
TYPE_OBJECT: "Object",
TYPE_CALLABLE: "Callable",
TYPE_SIGNAL: "Signal",
TYPE_DICTIONARY: "Dictionary",
TYPE_ARRAY: "Array",
TYPE_PACKED_BYTE_ARRAY: "PackedByteArray",
TYPE_PACKED_INT32_ARRAY: "PackedInt32Array",
TYPE_PACKED_INT64_ARRAY: "PackedInt64Array",
TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array",
TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array",
TYPE_PACKED_STRING_ARRAY: "PackedStringArray",
TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array",
TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array",
TYPE_PACKED_COLOR_ARRAY: "PackedColorArray",
TYPE_VOID: "void",
TYPE_VARARG: "VarArg",
TYPE_FUNC: "Func",
TYPE_FUZZER: "Fuzzer",
TYPE_VARIANT: "Variant"
}
const NOTIFICATION_AS_STRING_MAPPINGS := {
TYPE_OBJECT: {
Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE",
Object.NOTIFICATION_PREDELETE: "PREDELETE",
EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED",
},
TYPE_NODE: {
Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE",
Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE",
Node.NOTIFICATION_CHILD_ORDER_CHANGED: "CHILD_ORDER_CHANGED",
Node.NOTIFICATION_READY: "READY",
Node.NOTIFICATION_PAUSED: "PAUSED",
Node.NOTIFICATION_UNPAUSED: "UNPAUSED",
Node.NOTIFICATION_PHYSICS_PROCESS: "PHYSICS_PROCESS",
Node.NOTIFICATION_PROCESS: "PROCESS",
Node.NOTIFICATION_PARENTED: "PARENTED",
Node.NOTIFICATION_UNPARENTED: "UNPARENTED",
Node.NOTIFICATION_SCENE_INSTANTIATED: "INSTANCED",
Node.NOTIFICATION_DRAG_BEGIN: "DRAG_BEGIN",
Node.NOTIFICATION_DRAG_END: "DRAG_END",
Node.NOTIFICATION_PATH_RENAMED: "PATH_CHANGED",
Node.NOTIFICATION_INTERNAL_PROCESS: "INTERNAL_PROCESS",
Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS: "INTERNAL_PHYSICS_PROCESS",
Node.NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE",
Node.NOTIFICATION_WM_MOUSE_ENTER: "WM_MOUSE_ENTER",
Node.NOTIFICATION_WM_MOUSE_EXIT: "WM_MOUSE_EXIT",
Node.NOTIFICATION_APPLICATION_FOCUS_IN: "WM_FOCUS_IN",
Node.NOTIFICATION_APPLICATION_FOCUS_OUT: "WM_FOCUS_OUT",
#Node.NOTIFICATION_WM_QUIT_REQUEST: "WM_QUIT_REQUEST",
Node.NOTIFICATION_WM_GO_BACK_REQUEST: "WM_GO_BACK_REQUEST",
Node.NOTIFICATION_WM_WINDOW_FOCUS_OUT: "WM_UNFOCUS_REQUEST",
Node.NOTIFICATION_OS_MEMORY_WARNING: "OS_MEMORY_WARNING",
Node.NOTIFICATION_TRANSLATION_CHANGED: "TRANSLATION_CHANGED",
Node.NOTIFICATION_WM_ABOUT: "WM_ABOUT",
Node.NOTIFICATION_CRASH: "CRASH",
Node.NOTIFICATION_OS_IME_UPDATE: "OS_IME_UPDATE",
Node.NOTIFICATION_APPLICATION_RESUMED: "APP_RESUMED",
Node.NOTIFICATION_APPLICATION_PAUSED: "APP_PAUSED",
Node3D.NOTIFICATION_TRANSFORM_CHANGED: "TRANSFORM_CHANGED",
Node3D.NOTIFICATION_ENTER_WORLD: "ENTER_WORLD",
Node3D.NOTIFICATION_EXIT_WORLD: "EXIT_WORLD",
Node3D.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
Skeleton3D.NOTIFICATION_UPDATE_SKELETON: "UPDATE_SKELETON",
CanvasItem.NOTIFICATION_DRAW: "DRAW",
CanvasItem.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
CanvasItem.NOTIFICATION_ENTER_CANVAS: "ENTER_CANVAS",
CanvasItem.NOTIFICATION_EXIT_CANVAS: "EXIT_CANVAS",
#Popup.NOTIFICATION_POST_POPUP: "POST_POPUP",
#Popup.NOTIFICATION_POPUP_HIDE: "POPUP_HIDE",
},
TYPE_CONTROL : {
Object.NOTIFICATION_PREDELETE: "PREDELETE",
Container.NOTIFICATION_SORT_CHILDREN: "SORT_CHILDREN",
Control.NOTIFICATION_RESIZED: "RESIZED",
Control.NOTIFICATION_MOUSE_ENTER: "MOUSE_ENTER",
Control.NOTIFICATION_MOUSE_EXIT: "MOUSE_EXIT",
Control.NOTIFICATION_FOCUS_ENTER: "FOCUS_ENTER",
Control.NOTIFICATION_FOCUS_EXIT: "FOCUS_EXIT",
Control.NOTIFICATION_THEME_CHANGED: "THEME_CHANGED",
#Control.NOTIFICATION_MODAL_CLOSE: "MODAL_CLOSE",
Control.NOTIFICATION_SCROLL_BEGIN: "SCROLL_BEGIN",
Control.NOTIFICATION_SCROLL_END: "SCROLL_END",
}
}
enum COMPARE_MODE {
OBJECT_REFERENCE,
PARAMETER_DEEP_TEST
}
# prototype of better object to dictionary
static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary:
if obj == null:
return {}
var clazz_name := obj.get_class()
var dict := 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
else:
clazz_name = clazz_path.get_file().replace(".gd", "")
dict["@path"] = clazz_path
for property in obj.get_property_list():
var property_name :String = property["name"]
var property_type :int = property["type"]
var property_value :Variant = obj.get(property_name)
if property_value is GDScript or property_value is Callable or property_value is RegEx:
continue
if (property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE|PROPERTY_USAGE_DEFAULT
and not property["usage"] & PROPERTY_USAGE_CATEGORY
and not property["usage"] == 0):
if property_type == TYPE_OBJECT:
# prevent recursion
if hashed_objects.has(obj):
dict[property_name] = str(property_value)
continue
hashed_objects[obj] = true
dict[property_name] = obj2dict(property_value, hashed_objects)
else:
dict[property_name] = property_value
if obj.has_method("get_children"):
var childrens :Array = obj.get_children()
dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
return {"%s" % clazz_name : dict}
static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
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()
a.sort()
b.sort()
return equals(a, b, case_sensitive, compare_mode)
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)
if stack_depth > 32:
prints("stack_depth", stack_depth, deep_stack)
push_error("GdUnit equals has max stack deep reached!")
return false
# use argument matcher if requested
if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher:
return (obj_a as GdUnitArgumentMatcher).is_match(obj_b)
if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher:
return (obj_b as GdUnitArgumentMatcher).is_match(obj_a)
stack_depth += 1
# fast fail is different types
if not _is_type_equivalent(type_a, type_b):
return false
# is same instance
if obj_a == obj_b:
return true
# handle null values
if obj_a == null and obj_b != null:
return false
if obj_b == null and obj_a != null:
return false
match type_a:
TYPE_OBJECT:
if deep_stack.has(obj_a) or deep_stack.has(obj_b):
return true
deep_stack.append(obj_a)
deep_stack.append(obj_b)
if compare_mode == COMPARE_MODE.PARAMETER_DEEP_TEST:
# fail fast
if not is_instance_valid(obj_a) or not is_instance_valid(obj_b):
return false
if obj_a.get_class() != obj_b.get_class():
return false
var a := obj2dict(obj_a)
var b := obj2dict(obj_b)
return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth)
return obj_a == obj_b
TYPE_ARRAY:
if obj_a.size() != obj_b.size():
return false
for index :int in obj_a.size():
if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth):
return false
return true
TYPE_DICTIONARY:
if obj_a.size() != obj_b.size():
return false
for key :Variant in obj_a.keys():
var value_a :Variant = obj_a[key] if obj_a.has(key) else null
var value_b :Variant = obj_b[key] if obj_b.has(key) else null
if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth):
return false
return true
TYPE_STRING:
if case_sensitive:
return obj_a.to_lower() == obj_b.to_lower()
else:
return obj_a == obj_b
return obj_a == obj_b
@warning_ignore("shadowed_variable_base_class")
static func notification_as_string(instance :Variant, notification :int) -> String:
var error := "Unknown notification: '%s' at instance: %s" % [notification, instance]
if instance is Node and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].has(notification):
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error)
if instance is Control and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].has(notification):
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].get(notification, error)
return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_OBJECT].get(notification, error)
static func string_to_type(value :String) -> int:
for type :int in TYPE_AS_STRING_MAPPINGS.keys():
if TYPE_AS_STRING_MAPPINGS.get(type) == value:
return type
return TYPE_NIL
static func to_camel_case(value :String) -> String:
var p := to_pascal_case(value)
if not p.is_empty():
p[0] = p[0].to_lower()
return p
static func to_pascal_case(value :String) -> String:
return value.capitalize().replace(" ", "")
static func to_snake_case(value :String) -> String:
var result := PackedStringArray()
for ch in value:
var lower_ch := ch.to_lower()
if ch != lower_ch and result.size() > 1:
result.append('_')
result.append(lower_ch)
return ''.join(result)
static func is_snake_case(value :String) -> bool:
for ch in value:
if ch == '_':
continue
if ch == ch.to_upper():
return false
return true
static func type_as_string(type :int) -> String:
return TYPE_AS_STRING_MAPPINGS.get(type, "Variant")
static func typeof_as_string(value :Variant) -> String:
return TYPE_AS_STRING_MAPPINGS.get(typeof(value), "Unknown type")
static func all_types() -> PackedInt32Array:
return PackedInt32Array(TYPE_AS_STRING_MAPPINGS.keys())
static func string_as_typeof(type_name :String) -> int:
var type :Variant = TYPE_AS_STRING_MAPPINGS.find_key(type_name)
return type if type != null else TYPE_VARIANT
static func is_primitive_type(value :Variant) -> bool:
return typeof(value) in [TYPE_BOOL, TYPE_STRING, TYPE_STRING_NAME, TYPE_INT, TYPE_FLOAT]
static func _is_type_equivalent(type_a :int, type_b :int) -> bool:
# don't test for TYPE_STRING_NAME equivalenz
if type_a == TYPE_STRING_NAME or type_b == TYPE_STRING_NAME:
return true
if GdUnitSettings.is_strict_number_type_compare():
return type_a == type_b
return (
(type_a == TYPE_FLOAT and type_b == TYPE_INT)
or (type_a == TYPE_INT and type_b == TYPE_FLOAT)
or type_a == type_b)
static func is_engine_type(value :Object) -> bool:
if value is GDScript or value is ScriptExtension:
return false
return value.is_class("GDScriptNativeClass")
static func is_type(value :Variant) -> bool:
# is an build-in type
if typeof(value) != TYPE_OBJECT:
return false
# is a engine class type
if is_engine_type(value):
return true
# is a custom class type
if value is GDScript and value.can_instantiate():
return true
return false
static func _is_same(left :Variant, right :Variant) -> bool:
var left_type := -1 if left == null else typeof(left)
var right_type := -1 if right == null else typeof(right)
# if typ different can't be the same
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()
return equals(left, right)
static func is_object(value :Variant) -> bool:
return typeof(value) == TYPE_OBJECT
static func is_script(value :Variant) -> bool:
return is_object(value) and value is Script
static func is_test_suite(script :Script) -> bool:
return is_gd_testsuite(script) or GdUnit4CSharpApiLoader.is_test_suite(script.resource_path)
static func is_native_class(value :Variant) -> bool:
return is_object(value) and is_engine_type(value)
static func is_scene(value :Variant) -> bool:
return is_object(value) and value is PackedScene
static func is_scene_resource_path(value :Variant) -> bool:
return value is String and value.ends_with(".tscn")
static func is_gd_script(script :Script) -> bool:
return script is GDScript
static func is_cs_script(script :Script) -> bool:
# we need to check by stringify name because checked non mono Godot the class CSharpScript is not available
return str(script).find("CSharpScript") != -1
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
if base != null:
if base.resource_path.find("GdUnitTestSuite") != -1:
return true
stack.push_back(base)
return false
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):
return true
return false
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() == "":
return true
if is_scene(value):
return true
return not value.has_method('new') and not value.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
return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty()
return false
static func can_be_instantiate(obj :Variant) -> bool:
if not obj or is_engine_type(obj):
return false
return obj.has_method("new")
static func create_instance(clazz :Variant) -> GdUnitResult:
match typeof(clazz):
TYPE_OBJECT:
# test is given clazz already an instance
if is_instance(clazz):
return GdUnitResult.success(clazz)
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))
else:
var clazz_path :String = extract_class_path(clazz)[0]
if not FileAccess.file_exists(clazz_path):
return GdUnitResult.error("Class '%s' not found." % clazz)
var script := 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)
static func extract_class_path(clazz :Variant) -> PackedStringArray:
var clazz_path := PackedStringArray()
if clazz is String:
clazz_path.append(clazz)
return clazz_path
if is_instance(clazz):
# is instance a script instance?
var script := clazz.script as GDScript
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)
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 clazz_info := inst_to_dict(instance)
GdUnitTools.free_instance(instance)
clazz_path.append(clazz_info["@path"])
if clazz_info.has("@subpath"):
var sub_path :String = clazz_info["@subpath"]
if not sub_path.is_empty():
var sub_paths := sub_path.split("/")
clazz_path += sub_paths
return clazz_path
return clazz_path
static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> String:
var base_clazz := clazz_path[0]
# return original class name if engine class
if ClassDB.class_exists(base_clazz):
return base_clazz
var clazz_name := to_pascal_case(base_clazz.get_basename().get_file())
for path_index in range(1, clazz_path.size()):
clazz_name += "." + clazz_path[path_index]
return clazz_name
static func extract_class_name(clazz :Variant) -> GdUnitResult:
if clazz == null:
return GdUnitResult.error("Can't extract class name form a null value.")
if is_instance(clazz):
# is instance a script instance?
var script := clazz.script as GDScript
if script != null:
return extract_class_name(script)
return GdUnitResult.success(clazz.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)
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():
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
var instance :Variant = clazz.new()
if instance == null:
return GdUnitResult.error("Can't create a instance for class '%s'" % clazz)
var result := extract_class_name(instance)
GdUnitTools.free_instance(instance)
return result
static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStringArray) -> PackedStringArray:
var inner_classes := PackedStringArray()
if ClassDB.class_exists(clazz_name):
return inner_classes
var script :GDScript = load(script_path[0])
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 := extract_class_path(value)
inner_classes.append(class_path[1])
return inner_classes
static func extract_class_functions(clazz_name :String, script_path :PackedStringArray) -> Array:
if ClassDB.class_get_method_list(clazz_name):
return ClassDB.class_get_method_list(clazz_name)
if not FileAccess.file_exists(script_path[0]):
return Array()
var script :GDScript = load(script_path[0])
if script is GDScript:
# if inner class on class path we have to load the script from the script_constant_map
if script_path.size() == 2 and script_path[1] != "":
var inner_classes := script_path[1]
var map := script.get_script_constant_map()
script = map[inner_classes]
var clazz_functions :Array = script.get_method_list()
var base_clazz :String = script.get_instance_base_type()
if base_clazz:
return extract_class_functions(base_clazz, script_path)
return clazz_functions
return Array()
# scans all registert script classes for given <clazz_name>
# if the class is public in the global space than return true otherwise false
# public class means the script class is defined by 'class_name <name>'
static func is_public_script_class(clazz_name :String) -> bool:
var script_classes:Array[Dictionary] = ProjectSettings.get_global_class_list()
for class_info in script_classes:
if class_info.has("class"):
if class_info["class"] == clazz_name:
return true
return false
static func build_function_default_arguments(script :GDScript, func_name :String) -> Array:
var arg_list := Array()
for func_sig in script.get_script_method_list():
if func_sig["name"] == func_name:
var args :Array[Dictionary] = func_sig["args"]
for arg in args:
var value_type :int = arg["type"]
var default_value :Variant = default_value_by_type(value_type)
arg_list.append(default_value)
return arg_list
return arg_list
static func default_value_by_type(type :int) -> Variant:
assert(type < TYPE_MAX)
assert(type >= 0)
match type:
TYPE_NIL: return null
TYPE_BOOL: return false
TYPE_INT: return 0
TYPE_FLOAT: return 0.0
TYPE_STRING: return ""
TYPE_VECTOR2: return Vector2.ZERO
TYPE_VECTOR2I: return Vector2i.ZERO
TYPE_VECTOR3: return Vector3.ZERO
TYPE_VECTOR3I: return Vector3i.ZERO
TYPE_VECTOR4: return Vector4.ZERO
TYPE_VECTOR4I: return Vector4i.ZERO
TYPE_RECT2: return Rect2()
TYPE_RECT2I: return Rect2i()
TYPE_TRANSFORM2D: return Transform2D()
TYPE_PLANE: return Plane()
TYPE_QUATERNION: return Quaternion()
TYPE_AABB: return AABB()
TYPE_BASIS: return Basis()
TYPE_TRANSFORM3D: return Transform3D()
TYPE_COLOR: return Color()
TYPE_NODE_PATH: return NodePath()
TYPE_RID: return RID()
TYPE_OBJECT: return null
TYPE_ARRAY: return []
TYPE_DICTIONARY: return {}
TYPE_PACKED_BYTE_ARRAY: return PackedByteArray()
TYPE_PACKED_COLOR_ARRAY: return PackedColorArray()
TYPE_PACKED_INT32_ARRAY: return PackedInt32Array()
TYPE_PACKED_INT64_ARRAY: return PackedInt64Array()
TYPE_PACKED_FLOAT32_ARRAY: return PackedFloat32Array()
TYPE_PACKED_FLOAT64_ARRAY: return PackedFloat64Array()
TYPE_PACKED_STRING_ARRAY: return PackedStringArray()
TYPE_PACKED_VECTOR2_ARRAY: return PackedVector2Array()
TYPE_PACKED_VECTOR3_ARRAY: return PackedVector3Array()
push_error("Can't determine a default value for type: '%s', Please create a Bug issue and attach the stacktrace please." % type)
return null
static func find_nodes_by_class(root: Node, cls: String, recursive: bool = false) -> Array[Node]:
if not recursive:
return _find_nodes_by_class_no_rec(root, cls)
return _find_nodes_by_class(root, cls)
static func _find_nodes_by_class_no_rec(parent: Node, cls: String) -> Array[Node]:
var result :Array[Node] = []
for ch in parent.get_children():
if ch.get_class() == cls:
result.append(ch)
return result
static func _find_nodes_by_class(root: Node, cls: String) -> Array[Node]:
var result :Array[Node] = []
var stack :Array[Node] = [root]
while stack:
var node :Node = stack.pop_back()
if node.get_class() == cls:
result.append(node)
for ch in node.get_children():
stack.push_back(ch)
return result

View File

@ -0,0 +1,57 @@
class_name GdUnit4Version
extends RefCounted
const VERSION_PATTERN = "[center][color=#9887c4]gd[/color][color=#7a57d6]Unit[/color][color=#9887c4]4[/color] [color=#9887c4]${version}[/color][/center]"
var _major :int
var _minor :int
var _patch :int
func _init(major :int, minor :int, patch :int) -> void:
_major = major
_minor = minor
_patch = patch
static func parse(value :String) -> GdUnit4Version:
var regex := RegEx.new()
regex.compile("[a-zA-Z:,-]+")
var cleaned := regex.sub(value, "", true)
var parts := cleaned.split(".")
var major := parts[0].to_int()
var minor := parts[1].to_int()
var patch := parts[2].to_int() if parts.size() > 2 else 0
return GdUnit4Version.new(major, minor, patch)
static func current() -> GdUnit4Version:
var config := ConfigFile.new()
config.load('addons/gdUnit4/plugin.cfg')
return parse(config.get_value('plugin', 'version'))
func equals(other :GdUnit4Version) -> bool:
return _major == other._major and _minor == other._minor and _patch == other._patch
func is_greater(other :GdUnit4Version) -> bool:
if _major > other._major:
return true
if _major == other._major and _minor > other._minor:
return true
return _major == other._major and _minor == other._minor and _patch > other._patch
static func init_version_label(label :Control) -> void:
var config := ConfigFile.new()
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)
else:
label.text = "gdUnit4 " + version
func _to_string() -> String:
return "v%d.%d.%d" % [_major, _minor, _patch]

View File

@ -0,0 +1,122 @@
# A class doubler used to mock and spy checked implementations
class_name GdUnitClassDoubler
extends RefCounted
const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_"
const DOUBLER_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd")
const EXCLUDE_VIRTUAL_FUNCTIONS = [
# we have to exclude notifications because NOTIFICATION_PREDELETE is try
# to delete already freed spy/mock resources and will result in a conflict
"_notification",
# https://github.com/godotengine/godot/issues/67461
"get_name",
"get_path",
"duplicate",
]
# define functions to be exclude when spy or mock checked a scene
const EXLCUDE_SCENE_FUNCTIONS = [
# needs to exclude get/set script functions otherwise it endsup in recursive endless loop
"set_script",
"get_script",
# needs to exclude otherwise verify fails checked collection arguments checked calling to string
"_to_string",
]
const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"]
static func check_leaked_instances() -> void:
## we check that all registered spy/mock instances are removed from the engine meta data
for key in Engine.get_meta_list():
if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX):
var instance :Variant = Engine.get_meta(key)
push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS])
# loads the doubler template
# class_info = { "class_name": <>, "class_path" : <>}
static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray:
# store instance id
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"))
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(".", "_"))
lines.insert(1, extends_clazz(class_info))
# append Object interactions stuff
lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n"))
return lines
static func extends_clazz(class_info :Dictionary) -> String:
var clazz_name :String = class_info.get("class_name")
var clazz_path :PackedStringArray = class_info.get("class_path", [])
# is inner class?
if clazz_path.size() > 1:
return "extends %s" % clazz_name
if clazz_path.size() == 1 and clazz_path[0].ends_with(".gd"):
return "extends '%s'" % clazz_path[0]
return "extends %s" % clazz_name
# double all functions of given instance
static func double_functions(instance :Object, clazz_name :String, clazz_path :PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions :Array) -> PackedStringArray:
var doubled_source := PackedStringArray()
var parser := GdScriptParser.new()
var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions
var functions := Array()
# double script functions
if not ClassDB.class_exists(clazz_name):
var result := parser.parse(clazz_name, clazz_path)
if result.is_error():
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()
# double regular class functions
var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path)
for method : Dictionary in clazz_functions:
var func_descriptor := GdFunctionDescriptor.extract_from(method)
# exclude private core functions
if func_descriptor.is_private():
continue
if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()):
continue
# GD-110: Hotfix do not double invalid engine functions
if is_invalid_method_descriptior(method):
#prints("'%s': invalid method descriptor found! %s" % [clazz_name, method])
continue
# do not double on not implemented virtual functions
if instance != null and not instance.has_method(func_descriptor.name()):
#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))
return doubled_source
# GD-110
static func is_invalid_method_descriptior(method :Dictionary) -> bool:
var return_info :Dictionary = method["return"]
var type :int = return_info["type"]
var usage :int = return_info["usage"]
var clazz_name :String = return_info["class_name"]
# is method returning a type int with a given 'class_name' we have an enum
# and the PROPERTY_USAGE_CLASS_IS_ENUM must be set
if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM):
return true
if clazz_name == "Variant.Type":
return true
return false

View File

@ -0,0 +1,211 @@
class_name GdUnitFileAccess
extends RefCounted
const GDUNIT_TEMP := "user://tmp"
static func current_dir() -> String:
return ProjectSettings.globalize_path("res://")
static func clear_tmp() -> void:
delete_directory(GDUNIT_TEMP)
# Creates a new file under
static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess:
var file_path := create_temp_dir(relative_path) + "/" + file_name
var file := FileAccess.open(file_path, mode)
if file == null:
push_error("Error creating temporary file at: %s, %s" % [file_path, error_string(FileAccess.get_open_error())])
return file
static func temp_dir() -> String:
if not DirAccess.dir_exists_absolute(GDUNIT_TEMP):
DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP)
return GDUNIT_TEMP
static func create_temp_dir(folder_name :String) -> String:
var new_folder := temp_dir() + "/" + folder_name
if not DirAccess.dir_exists_absolute(new_folder):
DirAccess.make_dir_recursive_absolute(new_folder)
return new_folder
static func copy_file(from_file :String, to_dir :String) -> GdUnitResult:
var dir := DirAccess.open(to_dir)
if dir != null:
var to_file := to_dir + "/" + from_file.get_file()
prints("Copy %s to %s" % [from_file, to_file])
var error := dir.copy(from_file, to_file)
if error != OK:
return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_string(error)])
return GdUnitResult.success(to_file)
return GdUnitResult.error("Directory not found: " + to_dir)
static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool:
if not DirAccess.dir_exists_absolute(from_dir):
push_error("Source directory not found '%s'" % from_dir)
return false
# check if destination exists
if not DirAccess.dir_exists_absolute(to_dir):
# create it
var err := DirAccess.make_dir_recursive_absolute(to_dir)
if err != OK:
push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)])
return false
var source_dir := DirAccess.open(from_dir)
var dest_dir := DirAccess.open(to_dir)
if source_dir != null:
source_dir.list_dir_begin()
var next := "."
while next != "":
next = source_dir.get_next()
if next == "" or next == "." or next == "..":
continue
var source := source_dir.get_current_dir() + "/" + next
var dest := dest_dir.get_current_dir() + "/" + next
if source_dir.current_is_dir():
if recursive:
copy_directory(source + "/", dest, recursive)
continue
var err := source_dir.copy(source, dest)
if err != OK:
push_error("Error checked copy file '%s' to '%s'" % [source, dest])
return false
return true
else:
push_error("Directory not found: " + from_dir)
return false
static func delete_directory(path :String, only_content := false) -> void:
var dir := DirAccess.open(path)
if dir != null:
dir.list_dir_begin()
var file_name := "."
while file_name != "":
file_name = dir.get_next()
if file_name.is_empty() or file_name == "." or file_name == "..":
continue
var next := path + "/" +file_name
if dir.current_is_dir():
delete_directory(next)
else:
# delete file
var err := dir.remove(next)
if err:
push_error("Delete %s failed: %s" % [next, error_string(err)])
if not only_content:
var err := dir.remove(path)
if err:
push_error("Delete %s failed: %s" % [path, error_string(err)])
static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int:
var dir := DirAccess.open(path)
if dir == null:
return 0
var deleted := 0
dir.list_dir_begin()
var next := "."
while next != "":
next = dir.get_next()
if next.is_empty() or next == "." or next == "..":
continue
if next.begins_with(prefix):
var current_index := next.split("_")[1].to_int()
if current_index <= index:
deleted += 1
delete_directory(path + "/" + next)
return deleted
# scans given path for sub directories by given prefix and returns the highest index numer
# e.g. <prefix_%d>
static func find_last_path_index(path :String, prefix :String) -> int:
var dir := DirAccess.open(path)
if dir == null:
return 0
var last_iteration := 0
dir.list_dir_begin()
var next := "."
while next != "":
next = dir.get_next()
if next.is_empty() or next == "." or next == "..":
continue
if next.begins_with(prefix):
var iteration := next.split("_")[1].to_int()
if iteration > last_iteration:
last_iteration = iteration
return last_iteration
static func scan_dir(path :String) -> PackedStringArray:
var dir := DirAccess.open(path)
if dir == null or not dir.dir_exists(path):
return PackedStringArray()
var content := PackedStringArray()
dir.list_dir_begin()
var next := "."
while next != "":
next = dir.get_next()
if next.is_empty() or next == "." or next == "..":
continue
content.append(next)
return content
static func resource_as_array(resource_path :String) -> PackedStringArray:
var file := FileAccess.open(resource_path, FileAccess.READ)
if file == null:
push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
return PackedStringArray()
var file_content := PackedStringArray()
while not file.eof_reached():
file_content.append(file.get_line())
return file_content
static func resource_as_string(resource_path :String) -> String:
var file := FileAccess.open(resource_path, FileAccess.READ)
if file == null:
push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
return ""
return file.get_as_text(true)
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
return path
static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult:
var zip: ZIPReader = ZIPReader.new()
var err := zip.open(zip_package)
if err != OK:
return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err])
var zip_entries: PackedStringArray = zip.get_files()
# Get base path and step over archive folder
var archive_path := zip_entries[0]
zip_entries.remove_at(0)
for zip_entry in zip_entries:
var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "")
if zip_entry.ends_with("/"):
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))
zip.close()
return GdUnitResult.success(dest_path)

View File

@ -0,0 +1,42 @@
class_name GdUnitObjectInteractions
extends RefCounted
static func verify(interaction_object :Object, interactions_times :int) -> Variant:
if not _is_mock_or_spy(interaction_object, "__verify"):
return interaction_object
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("")
if not _is_mock_or_spy(interaction_object, "__verify"):
return __gd_assert.report_success()
var __summary :Dictionary = interaction_object.__verify_no_interactions()
if __summary.is_empty():
return __gd_assert.report_success()
return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary))
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("")
if not _is_mock_or_spy(interaction_object, "__verify_no_more_interactions"):
return __gd_assert
var __summary :Dictionary = interaction_object.__verify_no_more_interactions()
if __summary.is_empty():
return __gd_assert
return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary))
static func reset(interaction_object :Object) -> Object:
if not _is_mock_or_spy(interaction_object, "__reset"):
return interaction_object
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):
push_error("Error: You try to use a non mock or spy!")
return false
return true

View File

@ -0,0 +1,91 @@
const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
var __expected_interactions :int = -1
var __saved_interactions := Dictionary()
var __verified_interactions := Array()
func __save_function_interaction(function_args :Array[Variant]) -> void:
var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true)
for __index in __saved_interactions.keys().size():
var __key :Variant = __saved_interactions.keys()[__index]
if __matcher.is_match(__key):
__saved_interactions[__key] += 1
return
__saved_interactions[function_args] = 1
func __is_verify_interactions() -> bool:
return __expected_interactions != -1
func __do_verify_interactions(interactions_times :int = 1) -> Object:
__expected_interactions = interactions_times
return self
func __verify_interactions(function_args :Array[Variant]) -> void:
var __summary := Dictionary()
var __total_interactions := 0
var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true)
for __index in __saved_interactions.keys().size():
var __key :Variant = __saved_interactions.keys()[__index]
if __matcher.is_match(__key):
var __interactions :int = __saved_interactions.get(__key, 0)
__total_interactions += __interactions
__summary[__key] = __interactions
# add as verified
__verified_interactions.append(__key)
var __gd_assert := GdUnitAssertImpl.new("")
if __total_interactions != __expected_interactions:
var __expected_summary := {function_args : __expected_interactions}
var __error_message :String
# if no __interactions macht collect not verified __interactions for failure report
if __summary.is_empty():
var __current_summary := __verify_no_more_interactions()
__error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary)
else:
__error_message = GdAssertMessages.error_validate_interactions(__summary, __expected_summary)
__gd_assert.report_error(__error_message)
else:
__gd_assert.report_success()
__expected_interactions = -1
func __verify_no_interactions() -> Dictionary:
var __summary := Dictionary()
if not __saved_interactions.is_empty():
for __index in __saved_interactions.keys().size():
var func_call :Variant = __saved_interactions.keys()[__index]
__summary[func_call] = __saved_interactions[func_call]
return __summary
func __verify_no_more_interactions() -> Dictionary:
var __summary := Dictionary()
var called_functions :Array[Variant] = __saved_interactions.keys()
if called_functions != __verified_interactions:
# collect the not verified functions
var called_but_not_verified := called_functions.duplicate()
for __index in __verified_interactions.size():
called_but_not_verified.erase(__verified_interactions[__index])
for __index in called_but_not_verified.size():
var not_verified :Variant = called_but_not_verified[__index]
__summary[not_verified] = __saved_interactions[not_verified]
return __summary
func __reset_interactions() -> void:
__saved_interactions.clear()
func __filter_vargs(arg_values :Array[Variant]) -> Array[Variant]:
var filtered :Array[Variant] = []
for __index in arg_values.size():
var arg :Variant = arg_values[__index]
if typeof(arg) == TYPE_STRING and arg == GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE:
continue
filtered.append(arg)
return filtered

View File

@ -0,0 +1,72 @@
class_name GdUnitProperty
extends RefCounted
var _name :String
var _help :String
var _type :int
var _value :Variant
var _value_set :PackedStringArray
var _default :Variant
func _init(p_name :String, p_type :int, p_value :Variant, p_default_value :Variant, p_help :="", p_value_set := PackedStringArray()) -> void:
_name = p_name
_type = p_type
_value = p_value
_value_set = p_value_set
_default = p_default_value
_help = p_help
func name() -> String:
return _name
func type() -> int:
return _type
func value() -> Variant:
return _value
func value_set() -> PackedStringArray:
return _value_set
func is_selectable_value() -> bool:
return not _value_set.is_empty()
func set_value(p_value :Variant) -> void:
match _type:
TYPE_STRING:
_value = str(p_value)
TYPE_BOOL:
_value = bool(p_value)
TYPE_INT:
_value = int(p_value)
TYPE_FLOAT:
_value = float(p_value)
_:
_value = p_value
func default() -> Variant:
return _default
func category() -> String:
var elements := _name.split("/")
if elements.size() > 3:
return elements[2]
return ""
func help() -> String:
return _help
func _to_string() -> String:
return "%-64s %-10s %-10s (%s) help:%s set:%s" % [name(), type(), value(), default(), help(), _value_set]

View File

@ -0,0 +1,104 @@
class_name GdUnitResult
extends RefCounted
enum {
SUCCESS,
WARN,
ERROR,
EMPTY
}
var _state :Variant
var _warn_message := ""
var _error_message := ""
var _value :Variant = null
static func empty() -> GdUnitResult:
var result := GdUnitResult.new()
result._state = EMPTY
return result
static func success(p_value :Variant) -> GdUnitResult:
assert(p_value != null, "The value must not be NULL")
var result := GdUnitResult.new()
result._value = p_value
result._state = SUCCESS
return result
static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResult:
assert(not p_warn_message.is_empty()) #,"The message must not be empty")
var result := GdUnitResult.new()
result._value = p_value
result._warn_message = p_warn_message
result._state = WARN
return result
static func error(p_error_message :String) -> GdUnitResult:
assert(not p_error_message.is_empty(), "The message must not be empty")
var result := GdUnitResult.new()
result._value = null
result._error_message = p_error_message
result._state = ERROR
return result
func is_success() -> bool:
return _state == SUCCESS
func is_warn() -> bool:
return _state == WARN
func is_error() -> bool:
return _state == ERROR
func is_empty() -> bool:
return _state == EMPTY
func value() -> Variant:
return _value
func or_else(p_value :Variant) -> Variant:
if not is_success():
return p_value
return value()
func error_message() -> String:
return _error_message
func warn_message() -> String:
return _warn_message
func _to_string() -> String:
return str(GdUnitResult.serialize(self))
static func serialize(result :GdUnitResult) -> Dictionary:
if result == null:
push_error("Can't serialize a Null object from type GdUnitResult")
return {
"state" : result._state,
"value" : var_to_str(result._value),
"warn_msg" : result._warn_message,
"err_msg" : result._error_message
}
static func deserialize(config :Dictionary) -> GdUnitResult:
var result := GdUnitResult.new()
result._value = str_to_var(config.get("value", ""))
result._warn_message = config.get("warn_msg", null)
result._error_message = config.get("err_msg", null)
result._state = config.get("state")
return result

View File

@ -0,0 +1,169 @@
extends Node
signal sync_rpc_id_result_received
@onready var _client :GdUnitTcpClient = $GdUnitTcpClient
@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new()
enum {
INIT,
RUN,
STOP,
EXIT
}
const GDUNIT_RUNNER = "GdUnitRunner"
var _config := GdUnitRunnerConfig.new()
var _test_suites_to_process :Array[Node]
var _state :int = INIT
var _cs_executor :RefCounted
func _init() -> void:
# minimize scene window checked debug mode
if OS.get_cmdline_args().size() == 1:
DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)")
else:
DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)")
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
# store current runner instance to engine meta data to can be access in as a singleton
Engine.set_meta(GDUNIT_RUNNER, self)
_cs_executor = GdUnit4CSharpApiLoader.create_executor(self)
func _ready() -> void:
var config_result := _config.load_config()
if config_result.is_error():
push_error(config_result.error_message())
_state = EXIT
return
_client.connect("connection_failed", _on_connection_failed)
GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event)
var result := _client.start("127.0.0.1", _config.server_port())
if result.is_error():
push_error(result.error_message())
return
_state = INIT
func _on_connection_failed(message :String) -> void:
prints("_on_connection_failed", message, _test_suites_to_process)
_state = STOP
func _notification(what :int) -> void:
#prints("GdUnitRunner", self, GdObjects.notification_as_string(what))
if what == NOTIFICATION_PREDELETE:
Engine.remove_meta(GDUNIT_RUNNER)
func _process(_delta :float) -> void:
match _state:
INIT:
# wait until client is connected to the GdUnitServer
if _client.is_client_connected():
var time := LocalTime.now()
prints("Scan for test suites.")
_test_suites_to_process = load_test_suits()
prints("Scanning of %d test suites took" % _test_suites_to_process.size(), time.elapsed_since())
gdUnitInit()
_state = RUN
RUN:
# all test suites executed
if _test_suites_to_process.is_empty():
_state = STOP
else:
# process next test suite
set_process(false)
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
else:
await _executor.execute(test_suite)
set_process(true)
STOP:
_state = EXIT
# give the engine small amount time to finish the rpc
_on_gdunit_event(GdUnitStop.new())
await get_tree().create_timer(0.1).timeout
await get_tree().process_frame
get_tree().quit(0)
func load_test_suits() -> Array[Node]:
var to_execute := _config.to_execute()
if to_execute.is_empty():
prints("No tests selected to execute!")
_state = EXIT
return []
# scan for the requested test suites
var test_suites :Array[Node] = []
var _scanner := GdUnitTestSuiteScanner.new()
for resource_path :String in to_execute.keys():
var selected_tests :PackedStringArray = to_execute.get(resource_path)
var scaned_suites := _scanner.scan(resource_path)
_filter_test_case(scaned_suites, selected_tests)
test_suites += scaned_suites
return test_suites
func gdUnitInit() -> void:
#enable_manuall_polling()
send_message("Scaned %d test suites" % _test_suites_to_process.size())
var total_count := _collect_test_case_count(_test_suites_to_process)
_on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_count))
if not GdUnitSettings.is_test_discover_enabled():
for test_suite in _test_suites_to_process:
send_test_suite(test_suite)
func _filter_test_case(test_suites :Array[Node], included_tests :PackedStringArray) -> void:
if included_tests.is_empty():
return
for test_suite in test_suites:
for test_case in test_suite.get_children():
_do_filter_test_case(test_suite, test_case, included_tests)
func _do_filter_test_case(test_suite :Node, test_case :Node, included_tests :PackedStringArray) -> void:
for included_test in included_tests:
var test_meta :PackedStringArray = included_test.split(":")
var test_name := test_meta[0]
if test_case.get_name() == test_name:
# we have a paremeterized test selection
if test_meta.size() > 1:
var test_param_index := test_meta[1]
test_case.set_test_parameter_index(test_param_index.to_int())
return
# the test is filtered out
test_suite.remove_child(test_case)
test_case.free()
func _collect_test_case_count(testSuites :Array[Node]) -> int:
var total :int = 0
for test_suite in testSuites:
total += test_suite.get_child_count()
return total
# RPC send functions
func send_message(message :String) -> void:
_client.rpc_send(RPCMessage.of(message))
func send_test_suite(test_suite :Node) -> void:
_client.rpc_send(RPCGdUnitTestSuite.of(test_suite))
func _on_gdunit_event(event :GdUnitEvent) -> void:
_client.rpc_send(RPCGdUnitEvent.of(event))
# Event bridge from C# GdUnit4.ITestEventListener.cs
func PublishEvent(data :Dictionary) -> void:
var event := GdUnitEvent.new().deserialize(data)
_client.rpc_send(RPCGdUnitEvent.of(event))

View File

@ -0,0 +1,10 @@
[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"]
[ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitRunner.gd" id="1"]
[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"]
[node name="Control" type="Node"]
script = ExtResource("1")
[node name="GdUnitTcpClient" type="Node" parent="."]
script = ExtResource("2")

View File

@ -0,0 +1,154 @@
class_name GdUnitRunnerConfig
extends Resource
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const CONFIG_VERSION = "1.0"
const VERSION = "version"
const INCLUDED = "included"
const SKIPPED = "skipped"
const SERVER_PORT = "server_port"
const EXIT_FAIL_FAST ="exit_on_first_fail"
const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg"
var _config := {
VERSION : CONFIG_VERSION,
# a set of directories or testsuite paths as key and a optional set of testcases as values
INCLUDED : Dictionary(),
# a set of skipped directories or testsuite paths
SKIPPED : Dictionary(),
# the port of running test server for this session
SERVER_PORT : -1
}
func clear() -> GdUnitRunnerConfig:
_config[INCLUDED] = Dictionary()
_config[SKIPPED] = Dictionary()
return self
func set_server_port(port :int) -> GdUnitRunnerConfig:
_config[SERVER_PORT] = port
return self
func server_port() -> int:
return _config.get(SERVER_PORT, -1)
func self_test() -> GdUnitRunnerConfig:
add_test_suite("res://addons/gdUnit4/test/")
add_test_suite("res://addons/gdUnit4/mono/test/")
return self
func add_test_suite(p_resource_path :String) -> GdUnitRunnerConfig:
var to_execute_ := to_execute()
to_execute_[p_resource_path] = to_execute_.get(p_resource_path, PackedStringArray())
return self
func add_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig:
for resource_path_ in resource_paths:
add_test_suite(resource_path_)
return self
func add_test_case(p_resource_path :String, test_name :StringName, test_param_index :int = -1) -> GdUnitRunnerConfig:
var to_execute_ := to_execute()
var test_cases :PackedStringArray = to_execute_.get(p_resource_path, PackedStringArray())
if test_param_index != -1:
test_cases.append("%s:%d" % [test_name, test_param_index])
else:
test_cases.append(test_name)
to_execute_[p_resource_path] = test_cases
return self
# supports full path or suite name with optional test case name
# <test_suite_name|path>[:<test_case_name>]
# '/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(":")
if parts[0] == "res":
parts.pop_front()
parts[0] = GdUnitFileAccess.make_qualified_path(parts[0])
match parts.size():
1: skipped()[parts[0]] = PackedStringArray()
2: skip_test_case(parts[0], parts[1])
return self
func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig:
for resource_path_ in resource_paths:
skip_test_suite(resource_path_)
return self
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())
test_cases.append(test_name)
to_ignore[p_resource_path] = test_cases
return self
# Dictionary[String, Dictionary[String, PackedStringArray]]
func to_execute() -> Dictionary:
return _config.get(INCLUDED, {"res://" : PackedStringArray()})
func skipped() -> Dictionary:
return _config.get(SKIPPED, {})
func save_config(path :String = CONFIG_FILE) -> GdUnitResult:
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
var error := FileAccess.get_open_error()
return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)])
_config[VERSION] = CONFIG_VERSION
file.store_string(JSON.stringify(_config))
return GdUnitResult.success(path)
func load_config(path :String = CONFIG_FILE) -> GdUnitResult:
if not FileAccess.file_exists(path):
return GdUnitResult.error("Can't find test runner configuration '%s'! Please select a test to run." % path)
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
var error := FileAccess.get_open_error()
return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, error_string(error)])
var content := file.get_as_text()
if not content.is_empty() and content[0] == '{':
# Parse as json
var test_json_conv := JSON.new()
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
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)
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])
func convert_Array_to_PackedStringArray(data :Dictionary) -> void:
for key in data.keys() as Array[String]:
var values :Array = data[key]
data[key] = PackedStringArray(values)
func _to_string() -> String:
return str(_config)

View File

@ -0,0 +1,486 @@
# This class provides a runner for scense to simulate interactions like keyboard or mouse
class_name GdUnitSceneRunnerImpl
extends GdUnitSceneRunner
var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
# mapping of mouse buttons and his masks
const MAP_MOUSE_BUTTON_MASKS := {
MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT,
MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT,
MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE,
# https://github.com/godotengine/godot/issues/73632
MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1),
MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1),
MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1,
MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2,
}
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 _mouse_button_on_press := []
var _key_on_press := []
var _action_on_press := []
var _curent_mouse_position :Vector2
# time factor settings
var _time_factor := 1.0
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:
_verbose = p_verbose
_saved_iterations_per_second = Engine.get_physics_ticks_per_second()
set_time_factor(1)
# handle scene loading by resource path
if typeof(p_scene) == TYPE_STRING:
if !ResourceLoader.exists(p_scene):
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
if !str(p_scene).ends_with(".tscn") and !str(p_scene).ends_with(".scn") and !str(p_scene).begins_with("uid://"):
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()
_scene_auto_free = true
else:
# verify we have a node instance
if not p_scene is Node:
if not p_hide_push_errors:
push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene)
return
_current_scene = p_scene
if _current_scene == null:
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
_scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void:
if child == _current_scene:
_reset_input_to_default()
)
_simulate_start_time = LocalTime.now()
# we need to set inital a valid window otherwise the warp_mouse() is not handled
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
# set inital mouse pos to 0,0
var max_iteration_to_wait := 0
while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100:
Input.warp_mouse(Vector2.ZERO)
max_iteration_to_wait += 1
func _notification(what :int) -> void:
if what == NOTIFICATION_PREDELETE and is_instance_valid(self):
# reset time factor to normal
__deactivate_time_factor()
if is_instance_valid(_current_scene):
_scene_tree().root.remove_child(_current_scene)
# do only free scenes instanciated by this runner
if _scene_auto_free:
_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:
simulate_action_press(action)
simulate_action_release(action)
return self
func simulate_action_press(action :String) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventAction.new()
event.pressed = true
event.action = action
_action_on_press.append(action)
return _handle_input_event(event)
func simulate_action_release(action :String) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventAction.new()
event.pressed = false
event.action = 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:
simulate_key_press(key_code, shift_pressed, ctrl_pressed)
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:
__print_current_focus()
var event := InputEventKey.new()
event.pressed = true
event.keycode = key_code as Key
event.physical_keycode = key_code as Key
event.alt_pressed = key_code == KEY_ALT
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
_apply_input_modifiers(event)
_key_on_press.append(key_code)
return _handle_input_event(event)
func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
__print_current_focus()
var event := InputEventKey.new()
event.pressed = false
event.keycode = key_code as Key
event.physical_keycode = key_code as Key
event.alt_pressed = key_code == KEY_ALT
event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
_apply_input_modifiers(event)
_key_on_press.erase(key_code)
return _handle_input_event(event)
func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner:
var event := InputEventMouseMotion.new()
event.position = pos
event.global_position = get_global_mouse_position()
_apply_input_modifiers(event)
return _handle_input_event(event)
func get_mouse_position() -> Vector2:
if _last_input_event is InputEventMouse:
return _last_input_event.position
var current_scene := scene()
if current_scene != null:
return current_scene.get_viewport().get_mouse_position()
return Vector2.ZERO
func get_global_mouse_position() -> Vector2:
return Engine.get_main_loop().root.get_mouse_position()
func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner:
var event := InputEventMouseMotion.new()
event.position = pos
event.relative = pos - get_mouse_position()
event.global_position = get_global_mouse_position()
_apply_input_mouse_mask(event)
_apply_input_modifiers(event)
return _handle_input_event(event)
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()
var final_position := _curent_mouse_position + relative
tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type)
tween.play()
while not get_mouse_position().is_equal_approx(final_position):
simulate_mouse_move(_curent_mouse_position)
await _scene_tree().process_frame
return self
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()
tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type)
tween.play()
while not get_mouse_position().is_equal_approx(position):
simulate_mouse_move(_curent_mouse_position)
await _scene_tree().process_frame
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)
return self
func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner:
var event := InputEventMouseButton.new()
event.button_index = buttonIndex
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)
return _handle_input_event(event)
func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner:
var event := InputEventMouseButton.new()
event.button_index = buttonIndex
event.pressed = false
_apply_input_mouse_position(event)
_apply_input_mouse_mask(event)
_apply_input_modifiers(event)
_mouse_button_on_press.erase(buttonIndex)
return _handle_input_event(event)
func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner:
_time_factor = min(9.0, time_factor)
__activate_time_factor()
__print("set time factor: %f" % _time_factor)
__print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor))
return self
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:
await _scene_tree().process_frame
else:
await _scene_tree().create_timer(delta_milli * 0.001).timeout
return self
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)
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)
await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000)
return self
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:
return GdUnitFuncAssertImpl.new(instance, func_name, args)
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:
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:
return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name)
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:
if not _property_exists(name):
push_error("The property named '%s' cannot be set, it does not exist!" % name)
return false;
scene().set(name, value)
return true
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)
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:
return scene().find_child(name, recursive, owned)
func _scene_name() -> String:
var scene_script :GDScript = scene().get_script()
var scene_name :String = scene().get_name()
if not scene_script:
return scene_name
if not scene_name.begins_with("@"):
return scene_name
return scene_script.resource_name.get_basename()
func __activate_time_factor() -> void:
Engine.set_time_scale(_time_factor)
Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int)
func __deactivate_time_factor() -> void:
Engine.set_time_scale(1)
Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int)
# copy over current active modifiers
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
# 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:
# first apply last mask
if _last_input_event is InputEventMouse and event is InputEventMouse:
event.button_mask |= _last_input_event.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
else:
event.button_mask ^= button_mask
# copy over last mouse position if need
func _apply_input_mouse_position(event :InputEvent) -> void:
if _last_input_event is InputEventMouse and event is InputEventMouseButton:
event.position = _last_input_event.position
## handle input action via Input modifieres
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()])
if event.is_pressed():
Input.action_press(event.action, InputMap.action_get_deadzone(event.action))
else:
Input.action_release(event.action)
return true
# 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:
if event is InputEventMouse:
Input.warp_mouse(event.position)
Input.parse_input_event(event)
if event is InputEventAction:
_handle_actions(event)
Input.flush_buffered_events()
var current_scene := scene()
if is_instance_valid(current_scene):
__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)
if(current_scene.has_method("_unhandled_input")):
current_scene._unhandled_input(event)
current_scene.get_viewport().set_input_as_handled()
# save last input event needs to be merged with next InputEventMouseButton
_last_input_event = event
return self
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():
if Input.is_mouse_button_pressed(m_button):
simulate_mouse_button_release(m_button)
_mouse_button_on_press.clear()
for key_scancode :int in _key_on_press.duplicate():
if Input.is_key_pressed(key_scancode):
simulate_key_release(key_scancode)
_key_on_press.clear()
for action :String in _action_on_press.duplicate():
if Input.is_action_pressed(action):
simulate_action_release(action)
_action_on_press.clear()
Input.flush_buffered_events()
_last_input_event = null
func __print(message :String) -> void:
if _verbose:
prints(message)
func __print_current_focus() -> void:
if not _verbose:
return
var focused_node := scene().get_viewport().gui_get_focus_owner()
if focused_node:
prints(" focus checked %s" % focused_node)
else:
prints(" no focus set")
func scene() -> Node:
if is_instance_valid(_current_scene):
return _current_scene
if not _is_disposed:
push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.")
return null

View File

@ -0,0 +1,16 @@
class_name GdUnitScriptType
extends RefCounted
const UNKNOWN := ""
const CS := "cs"
const GD := "gd"
static func type_of(script :Script) -> String:
if script == null:
return UNKNOWN
if GdObjects.is_gd_script(script):
return GD
if GdObjects.is_cs_script(script):
return CS
return UNKNOWN

View File

@ -0,0 +1,378 @@
@tool
class_name GdUnitSettings
extends RefCounted
const MAIN_CATEGORY = "gdunit4"
# Common Settings
const COMMON_SETTINGS = MAIN_CATEGORY + "/settings"
const GROUP_COMMON = COMMON_SETTINGS + "/common"
const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled"
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_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery"
# Report Setiings
const REPORT_SETTINGS = MAIN_CATEGORY + "/report"
const GROUP_GODOT = REPORT_SETTINGS + "/godot"
const REPORT_PUSH_ERRORS = GROUP_GODOT + "/push_error"
const REPORT_SCRIPT_ERRORS = GROUP_GODOT + "/script_error"
const REPORT_ORPHANS = REPORT_SETTINGS + "/verbose_orphans"
const GROUP_ASSERT = REPORT_SETTINGS + "/assert"
const REPORT_ASSERT_WARNINGS = GROUP_ASSERT + "/verbose_warnings"
const REPORT_ASSERT_ERRORS = GROUP_ASSERT + "/verbose_errors"
const REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE = GROUP_ASSERT + "/strict_number_type_compare"
# Godot debug stdout/logging settings
const CATEGORY_LOGGING := "debug/file_logging/"
const STDOUT_ENABLE_TO_FILE = CATEGORY_LOGGING + "enable_file_logging"
const STDOUT_WITE_TO_FILE = CATEGORY_LOGGING + "log_path"
# GdUnit Templates
const TEMPLATES = MAIN_CATEGORY + "/templates"
const TEMPLATES_TS = TEMPLATES + "/testsuite"
const TEMPLATE_TS_GD = TEMPLATES_TS + "/GDScript"
const TEMPLATE_TS_CS = TEMPLATES_TS + "/CSharpScript"
# UI Setiings
const UI_SETTINGS = MAIN_CATEGORY + "/ui"
const GROUP_UI_INSPECTOR = UI_SETTINGS + "/inspector"
const INSPECTOR_NODE_COLLAPSE = GROUP_UI_INSPECTOR + "/node_collapse"
const INSPECTOR_TREE_VIEW_MODE = GROUP_UI_INSPECTOR + "/tree_view_mode"
const INSPECTOR_TREE_SORT_MODE = GROUP_UI_INSPECTOR + "/tree_sort_mode"
# Shortcut Setiings
const SHORTCUT_SETTINGS = MAIN_CATEGORY + "/Shortcuts"
const GROUP_SHORTCUT_INSPECTOR = SHORTCUT_SETTINGS + "/inspector"
const SHORTCUT_INSPECTOR_RERUN_TEST = GROUP_SHORTCUT_INSPECTOR + "/rerun_test"
const SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_debug"
const SHORTCUT_INSPECTOR_RUN_TEST_OVERALL = GROUP_SHORTCUT_INSPECTOR + "/run_test_overall"
const SHORTCUT_INSPECTOR_RUN_TEST_STOP = GROUP_SHORTCUT_INSPECTOR + "/run_test_stop"
const GROUP_SHORTCUT_EDITOR = SHORTCUT_SETTINGS + "/editor"
const SHORTCUT_EDITOR_RUN_TEST = GROUP_SHORTCUT_EDITOR + "/run_test"
const SHORTCUT_EDITOR_RUN_TEST_DEBUG = GROUP_SHORTCUT_EDITOR + "/run_test_debug"
const SHORTCUT_EDITOR_CREATE_TEST = GROUP_SHORTCUT_EDITOR + "/create_test"
const GROUP_SHORTCUT_FILESYSTEM = SHORTCUT_SETTINGS + "/filesystem"
const SHORTCUT_FILESYSTEM_RUN_TEST = GROUP_SHORTCUT_FILESYSTEM + "/run_test"
const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_test_debug"
# Toolbar Setiings
const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar"
const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall"
# defaults
# server connection timeout in minutes
const DEFAULT_SERVER_TIMEOUT :int = 30
# test case runtime timeout in seconds
const DEFAULT_TEST_TIMEOUT :int = 60*5
# the folder to create new test-suites
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)"
enum NAMING_CONVENTIONS {
AUTO_DETECT,
SNAKE_CASE,
PASCAL_CASE,
}
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(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)")
# inspector
create_property_if_need(INSPECTOR_NODE_COLLAPSE, true,
"Enables/Disables that the testsuite node is closed 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())
create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED,
"Sets the inspector panel presentation.", 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")
create_shortcut_properties_if_need()
migrate_properties()
static func migrate_properties() -> void:
var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder"
if get_property(TEST_ROOT_FOLDER) != null:
migrate_property(TEST_ROOT_FOLDER,\
TEST_LOOKUP_FOLDER,\
DEFAULT_TEST_LOOKUP_FOLDER,\
HELP_TEST_LOOKUP_FOLDER,\
func(value :Variant) -> String: return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value)
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.")
# 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.")
# 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).")
static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void:
if not ProjectSettings.has_setting(name):
#prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)])
ProjectSettings.set_setting(name, default)
ProjectSettings.set_initial_value(name, default)
help += "" if value_set.is_empty() else " %s" % value_set
set_help(name, default, help)
static func set_help(property_name :String, value :Variant, help :String) -> void:
ProjectSettings.add_property_info({
"name": property_name,
"type": typeof(value),
"hint": PROPERTY_HINT_TYPE_STRING,
"hint_string": help
})
static func get_setting(name :String, default :Variant) -> Variant:
if ProjectSettings.has_setting(name):
return ProjectSettings.get_setting(name)
return default
static func is_update_notification_enabled() -> bool:
if ProjectSettings.has_setting(UPDATE_NOTIFICATION_ENABLED):
return ProjectSettings.get_setting(UPDATE_NOTIFICATION_ENABLED)
return false
static func set_update_notification(enable :bool) -> void:
ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable)
ProjectSettings.save()
static func get_log_path() -> String:
return ProjectSettings.get_setting(STDOUT_WITE_TO_FILE)
static func set_log_path(path :String) -> void:
ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true)
ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path)
ProjectSettings.save()
static func set_inspector_tree_sort_mode(sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void:
var property := get_property(INSPECTOR_TREE_SORT_MODE)
property.set_value(sort_mode)
update_property(property)
static func get_inspector_tree_sort_mode() -> GdUnitInspectorTreeConstants.SORT_MODE:
var property := get_property(INSPECTOR_TREE_SORT_MODE)
return property.value() if property != null else GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED
static func set_inspector_tree_view_mode(tree_view_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void:
var property := get_property(INSPECTOR_TREE_VIEW_MODE)
property.set_value(tree_view_mode)
update_property(property)
static func get_inspector_tree_view_mode() -> GdUnitInspectorTreeConstants.TREE_VIEW_MODE:
var property := get_property(INSPECTOR_TREE_VIEW_MODE)
return property.value() if property != null else GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE
# the configured server connection timeout in ms
static func server_timeout() -> int:
return get_setting(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT) * 60 * 1000
# the configured test case timeout in ms
static func test_timeout() -> int:
return get_setting(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT) * 1000
# the root folder to store/generate test-suites
static func test_root_folder() -> String:
return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER)
static func is_verbose_assert_warnings() -> bool:
return get_setting(REPORT_ASSERT_WARNINGS, true)
static func is_verbose_assert_errors() -> bool:
return get_setting(REPORT_ASSERT_ERRORS, true)
static func is_verbose_orphans() -> bool:
return get_setting(REPORT_ORPHANS, true)
static func is_strict_number_type_compare() -> bool:
return get_setting(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true)
static func is_report_push_errors() -> bool:
return get_setting(REPORT_PUSH_ERRORS, false)
static func is_report_script_errors() -> bool:
return get_setting(REPORT_SCRIPT_ERRORS, true)
static func is_inspector_node_collapse() -> bool:
return get_setting(INSPECTOR_NODE_COLLAPSE, true)
static func is_inspector_toolbar_button_show() -> bool:
return get_setting(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, true)
static func is_test_discover_enabled() -> bool:
return get_setting(TEST_DISCOVER_ENABLED, false)
static func set_test_discover_enabled(enable :bool) -> void:
var property := get_property(TEST_DISCOVER_ENABLED)
property.set_value(enable)
update_property(property)
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] = []
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))
return settings
static func extract_value_set_from_help(value :String) -> PackedStringArray:
var regex := RegEx.new()
regex.compile("\\[(.+)\\]")
var matches := regex.search_all(value)
if matches.is_empty():
return PackedStringArray()
var values :String = matches[0].get_string(1)
return values.replacen(" ", "").replacen("\"", "").split(",", false)
static func update_property(property :GdUnitProperty) -> Variant:
var current_value :Variant = ProjectSettings.get_setting(property.name())
if current_value != property.value():
var error :Variant = validate_property_value(property)
if error != null:
return error
ProjectSettings.set_setting(property.name(), property.value())
GdUnitSignals.instance().gdunit_settings_changed.emit(property)
_save_settings()
return null
static func reset_property(property :GdUnitProperty) -> void:
ProjectSettings.set_setting(property.name(), property.default())
GdUnitSignals.instance().gdunit_settings_changed.emit(property)
_save_settings()
static func validate_property_value(property :GdUnitProperty) -> Variant:
match property.name():
TEST_LOOKUP_FOLDER:
return validate_lookup_folder(property.value())
_: return null
static func validate_lookup_folder(value :String) -> Variant:
if value.is_empty() or value == "/":
return null
if value.contains("res:"):
return "Test Lookup Folder: do not allowed to contains 'res://'"
if not value.is_valid_filename():
return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)"
return null
static func save_property(name :String, value :Variant) -> void:
ProjectSettings.set_setting(name, value)
_save_settings()
static func _save_settings() -> void:
var err := ProjectSettings.save()
if err != OK:
push_error("Save GdUnit4 settings failed : %s" % error_string(err))
return
static func has_property(name :String) -> bool:
return ProjectSettings.get_property_list().any(func(property :Dictionary) -> bool: return property["name"] == name)
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 null
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:
prints("Migration not possible, property '%s' not found" % old_property)
return
var value :Variant = converter.call(property.value()) if converter.is_valid() else property.value()
ProjectSettings.set_setting(new_property, value)
ProjectSettings.set_initial_value(new_property, default_value)
set_help(new_property, value, help)
ProjectSettings.clear(old_property)
prints("Succesfull migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value])
static func dump_to_tmp() -> void:
ProjectSettings.save_custom("user://project_settings.godot")
static func restore_dump_from_tmp() -> void:
DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot")

View File

@ -0,0 +1,76 @@
class_name GdUnitSignalAwaiter
extends RefCounted
signal signal_emitted(action :Variant)
const NO_ARG :Variant = GdUnitConstants.NO_ARG
var _wait_on_idle_frame := false
var _interrupted := false
var _time_left :float = 0
var _timeout_millis :int
func _init(timeout_millis :int, wait_on_idle_frame := false) -> void:
_timeout_millis = timeout_millis
_wait_on_idle_frame = wait_on_idle_frame
func _on_signal_emmited(
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) -> void:
var signal_args :Variant = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
signal_emitted.emit(signal_args)
func is_interrupted() -> bool:
return _interrupted
func elapsed_time() -> float:
return _time_left
func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant:
# register checked signal to wait for
source.connect(signal_name, _on_signal_emmited)
# install timeout timer
var timer := Timer.new()
Engine.get_main_loop().root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED)
timer.start(_timeout_millis * 0.001 * Engine.get_time_scale())
# holds the emited value
var value :Variant
# wait for signal is emitted or a timeout is happen
while true:
value = await signal_emitted
if _interrupted:
break
if not (value is Array):
value = [value]
if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args):
break
await Engine.get_main_loop().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:
return value[0]
return value
func _do_interrupt() -> void:
_interrupted = true
signal_emitted.emit(null)

View File

@ -0,0 +1,115 @@
# It connects to all signals of given emitter and collects received signals and arguments
# The collected signals are cleand finally when the emitter is freed.
class_name GdUnitSignalCollector
extends RefCounted
const NO_ARG :Variant = GdUnitConstants.NO_ARG
const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"]
# {
# emitter<Object> : {
# signal_name<String> : [signal_args<Array>],
# ...
# }
# }
var _collected_signals :Dictionary = {}
func clear() -> void:
for emitter :Object in _collected_signals.keys():
if is_instance_valid(emitter):
unregister_emitter(emitter)
# connect to all possible signals defined by the emitter
# prepares the signal collection to store received signals and arguments
func register_emitter(emitter :Object) -> void:
if is_instance_valid(emitter):
# check emitter is already registerd
if _collected_signals.has(emitter):
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))
# 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"]
# set inital collected to empty
if not is_signal_collecting(emitter, signal_name):
_collected_signals[emitter][signal_name] = Array()
if SIGNAL_BLACK_LIST.find(signal_name) != -1:
continue
if !emitter.is_connected(signal_name, _on_signal_emmited):
var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name))
if err != OK:
push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)])
# unregister all acquired resources/connections, otherwise it ends up in orphans
# is called when the emitter is removed from the parent
func unregister_emitter(emitter :Object) -> void:
if is_instance_valid(emitter):
for signal_def in emitter.get_signal_list():
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))
_collected_signals.erase(emitter)
# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements
func _on_signal_emmited(
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,
arg10 :Variant= NO_ARG,
arg11 :Variant= NO_ARG) -> void:
var signal_args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG)
# extract the emitter and signal_name from the last two arguments (see line 61 where is added)
var signal_name :String = signal_args.pop_back()
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)
func reset_received_signals(emitter :Object, signal_name: String, signal_args :Array) -> void:
#_debug_signal_list("before claer");
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)
#_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)
func match(emitter :Object, signal_name :String, args :Array) -> bool:
#prints("match", signal_name, _collected_signals[emitter][signal_name]);
if _collected_signals.is_empty() or not _collected_signals.has(emitter):
return false
for received_args :Variant in _collected_signals[emitter][signal_name]:
#prints("testing", signal_name, received_args, "vs", args)
if GdObjects.equals(received_args, args):
return true
return false
func _debug_signal_list(message :String) -> void:
prints("-----", message, "-------")
prints("senders {")
for emitter :Object in _collected_signals:
prints("\t", emitter)
for signal_name :String in _collected_signals[emitter]:
var args :Variant = _collected_signals[emitter][signal_name]
prints("\t\t", signal_name, args)
prints("}")

View File

@ -0,0 +1,36 @@
class_name GdUnitSignals
extends RefCounted
signal gdunit_client_connected(client_id :int)
signal gdunit_client_disconnected(client_id :int)
signal gdunit_client_terminated()
signal gdunit_event(event :GdUnitEvent)
signal gdunit_event_debug(event :GdUnitEvent)
signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto)
signal gdunit_message(message :String)
signal gdunit_report(execution_context_id :int, report :GdUnitReport)
signal gdunit_set_test_failed(is_failed :bool)
signal gdunit_settings_changed(property :GdUnitProperty)
const META_KEY := "GdUnitSignals"
static func instance() -> GdUnitSignals:
if Engine.has_meta(META_KEY):
return Engine.get_meta(META_KEY)
var instance_ := GdUnitSignals.new()
Engine.set_meta(META_KEY, instance_)
return instance_
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"])
Engine.remove_meta(META_KEY)
while signals.get_reference_count() > 0:
signals.unreference()

View File

@ -0,0 +1,53 @@
################################################################################
# Provides access to a global accessible singleton
#
# This is a workarount to the existing auto load singleton because of some bugs
# around plugin handling
################################################################################
class_name GdUnitSingleton
extends Object
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const MEATA_KEY := "GdUnitSingletons"
static func instance(name :String, clazz :Callable) -> Variant:
if Engine.has_meta(name):
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()])
return
Engine.set_meta(name, singleton)
GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton])
var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
singletons.append(name)
Engine.set_meta(MEATA_KEY, singletons)
return singleton
static func unregister(p_singleton :String) -> void:
var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray())
if singletons.has(p_singleton):
GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton);
var index := singletons.find(p_singleton)
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_)
Engine.remove_meta(p_singleton)
GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton)
Engine.set_meta(MEATA_KEY, singletons)
static func dispose() -> void:
# use a copy because unregister is modify the singletons array
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)
Engine.remove_meta(MEATA_KEY)
GdUnitTools.prints_verbose("----------------------------------------------------------------")

View File

@ -0,0 +1,18 @@
class_name GdUnitTestSuiteBuilder
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
ScriptEditorControls.save_an_open_script(source.resource_path)
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)
var parser := GdScriptParser.new()
var lines := source.source_code.split("\n")
var current_line := lines[line_number]
var func_name := parser.parse_func_name(current_line)
if func_name.is_empty():
return GdUnitResult.error("No function found at line: %d." % line_number)
return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path)

View File

@ -0,0 +1,368 @@
class_name GdUnitTestSuiteScanner
extends RefCounted
const TEST_FUNC_TEMPLATE ="""
func test_${func_name}() -> void:
# remove this line and complete your test
assert_not_yet_implemented()
"""
# we exclude the gdunit source directorys by default
const exclude_scan_directories = [
"res://addons/gdUnit4/bin",
"res://addons/gdUnit4/src",
"res://reports"]
var _script_parser := GdScriptParser.new()
var _included_resources :PackedStringArray = []
var _excluded_resources :PackedStringArray = []
var _expression_runner := GdUnitExpressionRunner.new()
var _regex_extends_clazz_name := RegEx.create_from_string("extends[\\s]+([\\S]+)")
func prescan_testsuite_classes() -> void:
# scan and cache extends GdUnitTestSuite by class name an resource paths
var script_classes :Array[Dictionary] = ProjectSettings.get_global_class_list()
for script_meta in script_classes:
var base_class :String = script_meta["base"]
var resource_path :String = script_meta["path"]
if base_class == "GdUnitTestSuite":
_included_resources.append(resource_path)
elif ClassDB.class_exists(base_class):
_excluded_resources.append(resource_path)
func scan(resource_path :String) -> Array[Node]:
prescan_testsuite_classes()
# if single testsuite requested
if FileAccess.file_exists(resource_path):
var test_suite := _parse_is_test_suite(resource_path)
if test_suite != null:
return [test_suite]
return [] as Array[Node]
var base_dir := DirAccess.open(resource_path)
if base_dir == null:
prints("Given directory or file does not exists:", resource_path)
return []
return _scan_test_suites(base_dir, [])
func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[Node]:
if exclude_scan_directories.has(dir.get_current_dir()):
return collected_suites
prints("Scanning for test suites in:", dir.get_current_dir())
dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547
var file_name := dir.get_next()
while file_name != "":
var resource_path := GdUnitTestSuiteScanner._file(dir, file_name)
if dir.current_is_dir():
var sub_dir := DirAccess.open(resource_path)
if sub_dir != null:
_scan_test_suites(sub_dir, collected_suites)
else:
var time := LocalTime.now()
var test_suite := _parse_is_test_suite(resource_path)
if test_suite:
collected_suites.append(test_suite)
if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300:
push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since())
file_name = dir.get_next()
return collected_suites
static func _file(dir :DirAccess, file_name :String) -> String:
var current_dir := dir.get_current_dir()
if current_dir.ends_with("/"):
return current_dir + file_name
return current_dir + "/" + file_name
func _parse_is_test_suite(resource_path :String) -> Node:
if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path):
return null
if GdUnit4CSharpApiLoader.is_test_suite(resource_path):
return GdUnit4CSharpApiLoader.parse_test_suite(resource_path)
# We use the global cache to fast scan for test suites.
if _excluded_resources.has(resource_path):
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))
# 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
var extends_from := get_extends_classname(resource_path)
# If not extends is defined or extends from a Godot class
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)
if not GdObjects.is_test_suite(script):
return null
return _parse_test_suite(ResourceLoader.load(resource_path))
static func _is_script_format_supported(resource_path :String) -> bool:
var ext := resource_path.get_extension()
if ext == "gd":
return true
return GdUnit4CSharpApiLoader.is_csharp_file(resource_path)
func _parse_test_suite(script :GDScript) -> GdUnitTestSuite:
if not GdObjects.is_test_suite(script):
return null
var test_suite :GdUnitTestSuite = script.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()
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
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:
for arg in fd.args():
match arg.name():
_TestCase.ARGUMENT_SKIP:
var result :Variant = _expression_runner.execute(script, arg.value_as_string())
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())
_TestCase.ARGUMENT_SKIP_REASON:
test_suite.__skip_reason = arg.value_as_string()
_:
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:
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 test := _TestCase.new()
for arg in fd.args():
# verify argument is allowed
# is test using fuzzers?
if arg.type() == GdObjects.TYPE_FUZZER:
fuzzers.append(arg)
elif arg.has_default():
match arg.name():
_TestCase.ARGUMENT_TIMEOUT:
timeout = arg.default()
_TestCase.ARGUMENT_SKIP:
var result :Variant = _expression_runner.execute(script, arg.value_as_string())
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())
_TestCase.ARGUMENT_SKIP_REASON:
skip_reason = arg.value_as_string()
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)
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:
var test_cases_to_find := Array(test_case_names)
var functions_to_scan := test_case_names.duplicate()
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)
for fd in function_descriptors:
if fd.name() == "before":
_handle_test_suite_arguments(test_suite, script, fd)
if test_cases_to_find.has(fd.name()):
_handle_test_case_arguments(test_suite, script, fd)
const TEST_CASE_ARGUMENTS = [_TestCase.ARGUMENT_TIMEOUT, _TestCase.ARGUMENT_SKIP, _TestCase.ARGUMENT_SKIP_REASON, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ARGUMENT_SEED]
func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void:
if fd.is_parameterized():
return
for argument in fd.args():
if argument.type() == GdObjects.TYPE_FUZZER or argument.name() in TEST_CASE_ARGUMENTS:
continue
test_case.skip(true, "Unknown test case argument '%s' found." % argument.name())
# 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)
match nc:
GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT:
if GdObjects.is_snake_case(file_name):
return GdObjects.to_snake_case(file_name + "Test")
return GdObjects.to_pascal_case(file_name + "Test")
GdUnitSettings.NAMING_CONVENTIONS.SNAKE_CASE:
return GdObjects.to_snake_case(file_name + "Test")
GdUnitSettings.NAMING_CONVENTIONS.PASCAL_CASE:
return GdObjects.to_pascal_case(file_name + "Test")
push_error("Unexpected case")
return "-<Unexpected>-"
static func resolve_test_suite_path(source_script_path :String, test_root_folder :String = "test") -> String:
var file_name := source_script_path.get_basename().get_file()
var suite_name := _to_naming_convention(file_name)
if test_root_folder.is_empty() or test_root_folder == "/":
return source_script_path.replace(file_name, suite_name)
# is user tmp
if source_script_path.begins_with("user://tmp"):
return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name)
# at first look up is the script under a "src" folder located
var test_suite_path :String
var src_folder := source_script_path.find("/src/")
if src_folder != -1:
test_suite_path = source_script_path.replace("/src/", "/"+test_root_folder+"/")
else:
var paths := source_script_path.split("/", false)
# is a plugin script?
if paths[1] == "addons":
test_suite_path = "%s//addons/%s/%s" % [paths[0], paths[2], test_root_folder]
# rebuild plugin path
for index in range(3, paths.size()):
test_suite_path += "/" + paths[index]
else:
test_suite_path = paths[0] + "//" + test_root_folder
for index in range(1, paths.size()):
test_suite_path += "/" + paths[index]
return normalize_path(test_suite_path).replace(file_name, suite_name)
static func normalize_path(path :String) -> String:
return path.replace("///", "/")
static func create_test_suite(test_suite_path :String, source_path :String) -> GdUnitResult:
# create directory if not exists
if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()):
var error_ := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir())
if error_ != OK:
return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error_])
var script := GDScript.new()
script.source_code = GdUnitTestSuiteTemplate.build_template(source_path)
var error := ResourceSaver.save(script, test_suite_path)
if error != OK:
return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error])
return GdUnitResult.success(test_suite_path)
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())
line_number += 1
# ignore comments and empty lines and not test functions
if row.begins_with("#") || row.length() == 0 || row.find("functest") == -1:
continue
# abort if test case name found
if script_parser.parse_func_name(row) == "test_" + func_name:
return line_number
return -1
func get_extends_classname(resource_path :String) -> String:
var file := FileAccess.open(resource_path, FileAccess.READ)
if file != null:
while not file.eof_reached():
var row := file.get_line()
# skip comments and empty lines
if row.begins_with("#") || row.length() == 0:
continue
# Stop at first function
if row.contains("func"):
return ""
var result := _regex_extends_clazz_name.search(row)
if result != null:
return result.get_string(1)
return ""
static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult:
var script := load(resource_path) as GDScript
# 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)
if Engine.is_editor_hint():
var settings := EditorInterface.get_editor_settings()
var ident_type :int = settings.get_setting("text_editor/behavior/indent/type")
var ident_size :int = settings.get_setting("text_editor/behavior/indent/size")
if ident_type == 1:
func_body = func_body.replace(" ", "".lpad(ident_size, " "))
script.source_code += func_body
var error := ResourceSaver.save(script, resource_path)
if error != OK:
return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error])
return GdUnitResult.success({ "path" : resource_path, "line" : line_number})
static func count_lines(script : GDScript) -> int:
return script.source_code.split("\n").size()
static func test_suite_exists(test_suite_path :String) -> bool:
return FileAccess.file_exists(test_suite_path)
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
for f in script.get_script_method_list():
if f["name"] == "test_" + func_name:
return true
return false
static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> GdUnitResult:
if test_case_exists(test_suite_path, func_name):
var line_number := get_test_case_line_number(test_suite_path, func_name)
return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number})
if not test_suite_exists(test_suite_path):
var result := create_test_suite(test_suite_path, source_script_path)
if result.is_error():
return result
return add_test_case(test_suite_path, func_name)

View File

@ -0,0 +1,115 @@
extends RefCounted
static var _richtext_normalize: RegEx
static func normalize_text(text :String) -> String:
return text.replace("\r", "");
static func richtext_normalize(input :String) -> String:
if _richtext_normalize == null:
_richtext_normalize = to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]")
return _richtext_normalize.sub(input, "", true).replace("\r", "")
static func to_regex(pattern :String) -> RegEx:
var regex := RegEx.new()
var err := regex.compile(pattern)
if err != OK:
push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)])
return regex
static func prints_verbose(message :String) -> void:
if OS.is_stdout_verbose():
prints(message)
static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool:
if instance is Array:
for element :Variant in instance:
free_instance(element)
instance.clear()
return true
# do not free an already freed instance
if not is_instance_valid(instance):
return false
# do not free a class refernece
if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"):
return false
if is_stdout_verbose:
print_verbose("GdUnit4:gc():free instance ", instance)
release_double(instance)
if instance is RefCounted:
instance.notification(Object.NOTIFICATION_PREDELETE)
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
return true
if instance is Node and instance.get_parent() != null:
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()
return !is_instance_valid(instance)
static func _release_connections(instance :Object) -> void:
if is_instance_valid(instance):
# disconnect from all connected signals to force freeing, otherwise it ends up in orphans
for connection in instance.get_incoming_connections():
var signal_ :Signal = connection["signal"]
var callable_ :Callable = connection["callable"]
#prints(instance, connection)
#prints("signal", signal_.get_name(), signal_.get_object())
#prints("callable", callable_.get_object())
if instance.has_signal(signal_.get_name()) and instance.is_connected(signal_.get_name(), callable_):
#prints("disconnect signal", signal_.get_name(), callable_)
instance.disconnect(signal_.get_name(), callable_)
release_timers()
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:
return
for node :Node in Engine.get_main_loop().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()
# the finally cleaup unfreed resources and singletons
static func dispose_all() -> void:
release_timers()
GdUnitSignals.dispose()
GdUnitSingleton.dispose()
# if instance an mock or spy we need manually freeing the self reference
static func release_double(instance :Object) -> void:
if instance.has_method("__release_double"):
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)
test_case.expect_to_interupt()

View File

@ -0,0 +1,29 @@
## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version
class_name GodotVersionFixures
extends RefCounted
@warning_ignore("shadowed_global_identifier")
static func type_convert(value: Variant, type: int) -> Variant:
return convert(value, type)
@warning_ignore("shadowed_global_identifier")
static func convert(value: Variant, type: int) -> Variant:
return type_convert(value, type)
# handle global_position fixed by https://github.com/godotengine/godot/pull/88473
static func set_event_global_position(event: InputEventMouseMotion, global_position: Vector2) -> void:
if Engine.get_version_info().hex >= 0x40202 or Engine.get_version_info().hex == 0x40104:
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

@ -0,0 +1,110 @@
# This class provides Date/Time functionallity to Godot
class_name LocalTime
extends Resource
enum TimeUnit {
MILLIS = 1,
SECOND = 2,
MINUTE = 3,
HOUR = 4,
DAY = 5,
MONTH = 6,
YEAR = 7
}
const SECONDS_PER_MINUTE:int = 60
const MINUTES_PER_HOUR:int = 60
const HOURS_PER_DAY:int = 24
const MILLIS_PER_SECOND:int = 1000
const MILLIS_PER_MINUTE:int = MILLIS_PER_SECOND * SECONDS_PER_MINUTE
const MILLIS_PER_HOUR:int = MILLIS_PER_MINUTE * MINUTES_PER_HOUR
var _time :int
var _hour :int
var _minute :int
var _second :int
var _millisecond :int
static func now() -> LocalTime:
return LocalTime.new(_get_system_time_msecs())
static func of_unix_time(time_ms :int) -> LocalTime:
return LocalTime.new(time_ms)
static func local_time(hours :int, minutes :int, seconds :int, milliseconds :int) -> LocalTime:
return LocalTime.new(MILLIS_PER_HOUR * hours\
+ MILLIS_PER_MINUTE * minutes\
+ MILLIS_PER_SECOND * seconds\
+ milliseconds)
func elapsed_since() -> String:
return LocalTime.elapsed(LocalTime._get_system_time_msecs() - _time)
func elapsed_since_ms() -> int:
return LocalTime._get_system_time_msecs() - _time
func plus(time_unit :TimeUnit, value :int) -> LocalTime:
var addValue:int = 0
match time_unit:
TimeUnit.MILLIS:
addValue = value
TimeUnit.SECOND:
addValue = value * MILLIS_PER_SECOND
TimeUnit.MINUTE:
addValue = value * MILLIS_PER_MINUTE
TimeUnit.HOUR:
addValue = value * MILLIS_PER_HOUR
_init(_time + addValue)
return self
static func elapsed(p_time_ms :int) -> String:
var local_time_ := LocalTime.new(p_time_ms)
if local_time_._hour > 0:
return "%dh %dmin %ds %dms" % [local_time_._hour, local_time_._minute, local_time_._second, local_time_._millisecond]
if local_time_._minute > 0:
return "%dmin %ds %dms" % [local_time_._minute, local_time_._second, local_time_._millisecond]
if local_time_._second > 0:
return "%ds %dms" % [local_time_._second, local_time_._millisecond]
return "%dms" % local_time_._millisecond
@warning_ignore("integer_division")
# create from epoch timestamp in ms
func _init(time :int) -> void:
_time = time
_hour = (time / MILLIS_PER_HOUR) % 24
_minute = (time / MILLIS_PER_MINUTE) % 60
_second = (time / MILLIS_PER_SECOND) % 60
_millisecond = time % 1000
func hour() -> int:
return _hour
func minute() -> int:
return _minute
func second() -> int:
return _second
func millis() -> int:
return _millisecond
func _to_string() -> String:
return "%02d:%02d:%02d.%03d" % [_hour, _minute, _second, _millisecond]
# wraper to old OS.get_system_time_msecs() function
static func _get_system_time_msecs() -> int:
return Time.get_unix_time_from_system() * 1000 as int

View File

@ -0,0 +1,238 @@
class_name _TestCase
extends Node
signal completed()
# default timeout 5min
const DEFAULT_TIMEOUT := -1
const ARGUMENT_TIMEOUT := "timeout"
const ARGUMENT_SKIP := "do_skip"
const ARGUMENT_SKIP_REASON := "skip_reason"
var _iterations: int = 1
var _current_iteration: int = -1
var _seed: int
var _fuzzers: Array[GdFunctionArgument] = []
var _test_param_index := -1
var _line_number: int = -1
var _script_path: String
var _skipped := false
var _skip_reason := ""
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
var timeout: int = DEFAULT_TIMEOUT:
set(value):
timeout = value
get:
if timeout == DEFAULT_TIMEOUT:
timeout = GdUnitSettings.test_timeout()
return timeout
@warning_ignore("shadowed_variable_base_class")
func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout: int=DEFAULT_TIMEOUT, p_fuzzers: Array[GdFunctionArgument]=[], p_iterations: int=1, p_seed: int=-1) -> _TestCase:
set_name(p_name)
_line_number = p_line_number
_fuzzers = p_fuzzers
_iterations = p_iterations
_seed = p_seed
_script_path = p_script_path
timeout = p_timeout
return self
func execute(p_test_parameter := Array(), p_iteration := 0) -> void:
_failure_received(false)
_current_iteration = p_iteration - 1
if _current_iteration == - 1:
_set_failure_handler()
set_timeout()
if not p_test_parameter.is_empty():
update_fuzzers(p_test_parameter, p_iteration)
_execute_test_case(name, p_test_parameter)
else:
_execute_test_case(name, [])
await completed
func execute_paramaterized(p_test_parameter: Array) -> void:
_failure_received(false)
set_timeout()
# We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used.
# This prevents objects in the argument list from being unnecessarily re-instantiated.
var test_parameters := p_test_parameter.duplicate() # is strictly need to duplicate the paramters before extend
test_parameters.append([])
_execute_test_case(name, test_parameters)
await completed
func dispose() -> void:
if _is_disposed:
return
_is_disposed = true
Engine.remove_meta("GD_TEST_FAILURE")
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
completed.emit()
func update_fuzzers(input_values: Array, iteration: int) -> void:
for fuzzer :Variant in input_values:
if fuzzer is Fuzzer:
fuzzer._iteration_index = iteration + 1
func set_timeout() -> void:
if is_instance_valid(_timer):
return
var time: float = timeout / 1000.0
_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)
_timer.set_one_shot(true)
_timer.set_wait_time(time)
_timer.set_autostart(false)
_timer.start()
func _set_failure_handler() -> void:
if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received):
GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received)
func _remove_failure_handler() -> void:
if GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received):
GdUnitSignals.instance().gdunit_set_test_failed.disconnect(_failure_received)
func _failure_received(is_failed: bool) -> void:
# is already failed?
if _failed:
return
_failed = is_failed
Engine.set_meta("GD_TEST_FAILURE", is_failed)
func stop_timer() -> void:
# finish outstanding timeouts
if is_instance_valid(_timer):
_timer.stop()
_timer.call_deferred("free")
_timer = null
func expect_to_interupt() -> void:
_expect_to_interupt = true
func is_interupted() -> bool:
return _interupted
func is_expect_interupted() -> bool:
return _expect_to_interupt
func is_parameterized() -> bool:
return _parameter_set_resolver.is_parameterized()
func is_skipped() -> bool:
return _skipped
func report() -> GdUnitReport:
return _report
func skip_info() -> String:
return _skip_reason
func line_number() -> int:
return _line_number
func iterations() -> int:
return _iterations
func seed_value() -> int:
return _seed
func is_fuzzed() -> bool:
return not _fuzzers.is_empty()
func fuzzer_arguments() -> Array[GdFunctionArgument]:
return _fuzzers
func script_path() -> String:
return _script_path
func ResourcePath() -> String:
return _script_path
func generate_seed() -> void:
if _seed != -1:
seed(_seed)
func skip(skipped: bool, reason: String="") -> void:
_skipped = skipped
_skip_reason = reason
func set_function_descriptor(fd: GdFunctionDescriptor) -> void:
_parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd)
func set_test_parameter_index(index: int) -> void:
_test_param_index = index
func test_parameter_index() -> int:
return _test_param_index
func test_case_names() -> PackedStringArray:
return _parameter_set_resolver.build_test_case_names(self)
func load_parameter_sets() -> Array:
return _parameter_set_resolver.load_parameter_sets(self, true)
func parameter_set_resolver() -> GdUnitTestParameterSetResolver:
return _parameter_set_resolver
func _to_string() -> String:
return "%s :%d (%dms)" % [get_name(), _line_number, timeout]

View File

@ -0,0 +1,41 @@
class_name GdUnitCommand
extends RefCounted
func _init(p_name :String, p_is_enabled: Callable, p_runnable: Callable, p_shortcut :GdUnitShortcut.ShortCut = GdUnitShortcut.ShortCut.NONE) -> void:
assert(p_name != null, "(%s) missing parameter 'name'" % p_name)
assert(p_is_enabled != null, "(%s) missing parameter 'is_enabled'" % p_name)
assert(p_runnable != null, "(%s) missing parameter 'runnable'" % p_name)
assert(p_shortcut != null, "(%s) missing parameter 'shortcut'" % p_name)
self.name = p_name
self.is_enabled = p_is_enabled
self.shortcut = p_shortcut
self.runnable = p_runnable
var name: String:
set(value):
name = value
get:
return name
var shortcut: GdUnitShortcut.ShortCut:
set(value):
shortcut = value
get:
return shortcut
var is_enabled: Callable:
set(value):
is_enabled = value
get:
return is_enabled
var runnable: Callable:
set(value):
runnable = value
get:
return runnable

View File

@ -0,0 +1,364 @@
class_name GdUnitCommandHandler
extends Object
signal gdunit_runner_start()
signal gdunit_runner_stop(client_id :int)
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
const CMD_RUN_OVERALL = "Debug Overall TestSuites"
const CMD_RUN_TESTCASE = "Run TestCases"
const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)"
const CMD_RUN_TESTSUITE = "Run TestSuites"
const CMD_RUN_TESTSUITE_DEBUG = "Run TestSuites (Debug)"
const CMD_RERUN_TESTS = "ReRun Tests"
const CMD_RERUN_TESTS_DEBUG = "ReRun Tests (Debug)"
const CMD_STOP_TEST_RUN = "Stop Test Run"
const CMD_CREATE_TESTCASE = "Create TestCase"
const SETTINGS_SHORTCUT_MAPPING := {
"N/A" : GdUnitShortcut.ShortCut.NONE,
GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST : GdUnitShortcut.ShortCut.RERUN_TESTS,
GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG,
GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_OVERALL : GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL,
GdUnitSettings.SHORTCUT_INSPECTOR_RUN_TEST_STOP : GdUnitShortcut.ShortCut.STOP_TEST_RUN,
GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE,
GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG,
GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST : GdUnitShortcut.ShortCut.CREATE_TEST,
GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE,
GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG
}
# the current test runner config
var _runner_config := GdUnitRunnerConfig.new()
# holds the current connected gdUnit runner client id
var _client_id :int
# if no debug mode we have an process id
var _current_runner_process_id :int = 0
# hold is current an test running
var _is_running :bool = false
# holds if the current running tests started in debug mode
var _running_debug_mode :bool
var _commands := {}
var _shortcuts := {}
static func instance() -> GdUnitCommandHandler:
return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new())
func _init() -> void:
assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING)
GdUnitSignals.instance().gdunit_event.connect(_on_event)
GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected)
GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected)
GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed)
# preload previous test execution
_runner_config.load_config()
init_shortcuts()
var is_running := func(_script :Script) -> bool: return _is_running
var is_not_running := func(_script :Script) -> bool: return !_is_running
register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL))
register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE))
register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG))
register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false)))
register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true)))
register_command(GdUnitCommand.new(CMD_RERUN_TESTS, is_not_running, cmd_run.bind(false), GdUnitShortcut.ShortCut.RERUN_TESTS))
register_command(GdUnitCommand.new(CMD_RERUN_TESTS_DEBUG, is_not_running, cmd_run.bind(true), GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG))
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)
timer.timeout.connect(cmd_discover_tests)
func _notification(what :int) -> void:
if what == NOTIFICATION_PREDELETE:
_commands.clear()
_shortcuts.clear()
func _do_process() -> void:
check_test_run_stopped_manually()
# is checking if the user has press the editor stop scene
func check_test_run_stopped_manually() -> void:
if is_test_running_but_stop_pressed():
if GdUnitSettings.is_verbose_assert_warnings():
push_warning("Test Runner scene was stopped manually, force stopping the current test run!")
cmd_stop(_client_id)
func is_test_running_but_stop_pressed() -> bool:
return _running_debug_mode and _is_running and not EditorInterface.is_playing_scene()
func assert_shortcut_mappings(mappings :Dictionary) -> void:
for shortcut :int in GdUnitShortcut.ShortCut.values():
assert(mappings.values().has(shortcut), "missing settings mapping for shortcut '%s'!" % GdUnitShortcut.ShortCut.keys()[shortcut])
func init_shortcuts() -> void:
for shortcut :int in GdUnitShortcut.ShortCut.values():
if shortcut == GdUnitShortcut.ShortCut.NONE:
continue
var property_name :String = SETTINGS_SHORTCUT_MAPPING.find_key(shortcut)
var property := GdUnitSettings.get_property(property_name)
var keys := GdUnitShortcut.default_keys(shortcut)
if property != null:
keys = property.value()
var inputEvent := create_shortcut_input_even(keys)
register_shortcut(shortcut, inputEvent)
func create_shortcut_input_even(key_codes : PackedInt32Array) -> InputEventKey:
var inputEvent :InputEventKey = InputEventKey.new()
inputEvent.pressed = true
for key_code in key_codes:
match key_code:
KEY_ALT:
inputEvent.alt_pressed = true
KEY_SHIFT:
inputEvent.shift_pressed = true
KEY_CTRL:
inputEvent.ctrl_pressed = true
_:
inputEvent.keycode = key_code as Key
inputEvent.physical_keycode = key_code as Key
return inputEvent
func register_shortcut(p_shortcut :GdUnitShortcut.ShortCut, p_input_event :InputEvent) -> void:
GdUnitTools.prints_verbose("register shortcut: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[p_shortcut], p_input_event.as_text()])
var shortcut := Shortcut.new()
shortcut.set_events([p_input_event])
var command_name :String = get_shortcut_command(p_shortcut)
_shortcuts[p_shortcut] = GdUnitShortcutAction.new(p_shortcut, shortcut, command_name)
func get_shortcut(shortcut_type :GdUnitShortcut.ShortCut) -> Shortcut:
return get_shortcut_action(shortcut_type).shortcut
func get_shortcut_action(shortcut_type :GdUnitShortcut.ShortCut) -> GdUnitShortcutAction:
return _shortcuts.get(shortcut_type)
func get_shortcut_command(p_shortcut :GdUnitShortcut.ShortCut) -> String:
return GdUnitShortcut.CommandMapping.get(p_shortcut, "unknown command")
func register_command(p_command :GdUnitCommand) -> void:
_commands[p_command.name] = p_command
func command(cmd_name :String) -> GdUnitCommand:
return _commands.get(cmd_name)
func cmd_run_test_suites(test_suite_paths :PackedStringArray, debug :bool, rerun := false) -> void:
# create new runner runner_config for fresh run otherwise use saved one
if not rerun:
var result := _runner_config.clear()\
.add_test_suites(test_suite_paths)\
.save_config()
if result.is_error():
push_error(result.error_message())
return
cmd_run(debug)
func cmd_run_test_case(test_suite_resource_path :String, test_case :String, test_param_index :int, debug :bool, rerun := false) -> void:
# create new runner config for fresh run otherwise use saved one
if not rerun:
var result := _runner_config.clear()\
.add_test_case(test_suite_resource_path, test_case, test_param_index)\
.save_config()
if result.is_error():
push_error(result.error_message())
return
cmd_run(debug)
func cmd_run_overall(debug :bool) -> void:
var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), [])
var result := _runner_config.clear()\
.add_test_suites(test_suite_paths)\
.save_config()
if result.is_error():
push_error(result.error_message())
return
cmd_run(debug)
func cmd_run(debug :bool) -> void:
# don't start is already running
if _is_running:
return
# save current selected excution config
var result := _runner_config.set_server_port(Engine.get_meta("gdunit_server_port")).save_config()
if result.is_error():
push_error(result.error_message())
return
# before start we have to save all changes
ScriptEditorControls.save_all_open_script()
gdunit_runner_start.emit()
_current_runner_process_id = -1
_running_debug_mode = debug
if debug:
run_debug_mode()
else:
run_release_mode()
func cmd_stop(client_id :int) -> void:
# don't stop if is already stopped
if not _is_running:
return
_is_running = false
gdunit_runner_stop.emit(client_id)
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)
_current_runner_process_id = -1
func cmd_editor_run_test(debug :bool) -> void:
var cursor_line := active_base_editor().get_caret_line()
#run test case?
var regex := RegEx.new()
regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)")
var result := regex.search(active_base_editor().get_line(cursor_line))
if result:
var func_name := result.get_string(2).strip_edges()
prints("Run test:", func_name, "debug", debug)
if func_name.begins_with("test_"):
cmd_run_test_case(active_script().resource_path, func_name, -1, debug)
return
# otherwise run the full test suite
var selected_test_suites := [active_script().resource_path]
cmd_run_test_suites(selected_test_suites, debug)
func cmd_create_test() -> void:
var cursor_line := active_base_editor().get_caret_line()
var result := GdUnitTestSuiteBuilder.create(active_script(), cursor_line)
if result.is_error():
# 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"))
func cmd_discover_tests() -> void:
await GdUnitTestDiscoverer.run()
static func scan_test_directorys(base_directory :String, test_directory: String, test_suite_paths :PackedStringArray) -> PackedStringArray:
print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory])
for directory in DirAccess.get_directories_at(base_directory):
if directory.begins_with("."):
continue
var current_directory := normalize_path(base_directory + "/" + directory)
if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory):
continue
if match_test_directory(directory, test_directory):
test_suite_paths.append(current_directory)
else:
scan_test_directorys(current_directory, test_directory, test_suite_paths)
return test_suite_paths
static func normalize_path(path :String) -> String:
return path.replace("///", "//")
static func match_test_directory(directory :String, test_directory: String) -> bool:
return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://"
func run_debug_mode() -> void:
EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/GdUnitRunner.tscn")
_is_running = true
func run_release_mode() -> void:
var arguments := Array()
if OS.is_stdout_verbose():
arguments.append("--verbose")
arguments.append("--no-window")
arguments.append("--path")
arguments.append(ProjectSettings.globalize_path("res://"))
arguments.append("res://addons/gdUnit4/src/core/GdUnitRunner.tscn")
_current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false);
_is_running = true
func active_base_editor() -> TextEdit:
return EditorInterface.get_script_editor().get_current_editor().get_base_editor()
func active_script() -> Script:
return EditorInterface.get_script_editor().get_current_script()
################################################################################
# signals handles
################################################################################
func _on_event(event :GdUnitEvent) -> void:
if event.type() == GdUnitEvent.STOP:
cmd_stop(_client_id)
func _on_stop_pressed() -> void:
cmd_stop(_client_id)
func _on_run_pressed(debug := false) -> void:
cmd_run(debug)
func _on_run_overall_pressed(_debug := false) -> void:
cmd_run_overall(true)
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())
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)
timer.timeout.connect(cmd_discover_tests)
################################################################################
# Network stuff
################################################################################
func _on_client_connected(client_id :int) -> void:
_client_id = client_id
func _on_client_disconnected(client_id :int) -> void:
# only stops is not in debug mode running and the current client
if not _running_debug_mode and _client_id == client_id:
cmd_stop(client_id)
_client_id = -1

View File

@ -0,0 +1,58 @@
class_name GdUnitShortcut
extends RefCounted
enum ShortCut {
NONE,
RUN_TESTS_OVERALL,
RUN_TESTCASE,
RUN_TESTCASE_DEBUG,
RERUN_TESTS,
RERUN_TESTS_DEBUG,
STOP_TEST_RUN,
CREATE_TEST,
}
const CommandMapping = {
ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL,
ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE,
ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG,
ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS,
ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG,
ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN,
ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE,
}
const DEFAULTS_MACOS := {
ShortCut.NONE : [],
ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5],
ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6],
ShortCut.RUN_TESTS_OVERALL : [Key.KEY_META, Key.KEY_F7],
ShortCut.STOP_TEST_RUN : [Key.KEY_META, Key.KEY_F8],
ShortCut.RERUN_TESTS : [Key.KEY_META, Key.KEY_F5],
ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_META, Key.KEY_F6],
ShortCut.CREATE_TEST : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F10],
}
const DEFAULTS_WINDOWS := {
ShortCut.NONE : [],
ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5],
ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6],
ShortCut.RUN_TESTS_OVERALL : [Key.KEY_CTRL, Key.KEY_F7],
ShortCut.STOP_TEST_RUN : [Key.KEY_CTRL, Key.KEY_F8],
ShortCut.RERUN_TESTS : [Key.KEY_CTRL, Key.KEY_F5],
ShortCut.RERUN_TESTS_DEBUG : [Key.KEY_CTRL, Key.KEY_F6],
ShortCut.CREATE_TEST : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F10],
}
static func default_keys(shortcut :ShortCut) -> PackedInt32Array:
match OS.get_name().to_lower():
'windows':
return DEFAULTS_WINDOWS[shortcut]
'macos':
return DEFAULTS_MACOS[shortcut]
_:
return DEFAULTS_WINDOWS[shortcut]

View File

@ -0,0 +1,36 @@
class_name GdUnitShortcutAction
extends RefCounted
func _init(p_type :GdUnitShortcut.ShortCut, p_shortcut :Shortcut, p_command :String) -> void:
assert(p_type != null, "missing parameter 'type'")
assert(p_shortcut != null, "missing parameter 'shortcut'")
assert(p_command != null, "missing parameter 'command'")
self.type = p_type
self.shortcut = p_shortcut
self.command = p_command
var type: GdUnitShortcut.ShortCut:
set(value):
type = value
get:
return type
var shortcut: Shortcut:
set(value):
shortcut = value
get:
return shortcut
var command: String:
set(value):
command = value
get:
return command
func _to_string() -> String:
return "GdUnitShortcutAction: %s (%s) -> %s" % [GdUnitShortcut.ShortCut.keys()[type], shortcut.get_as_text(), command]

View File

@ -0,0 +1,86 @@
extends RefCounted
# 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 := {}
func _init() -> void:
# Register for discovery events to sync the cache
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] = []
for test_case in dto.test_cases():
discovered_test_cases.append(test_case.name())
_discover_cache[resource_path] = discovered_test_cases
func discover(script: Script) -> void:
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 dto :GdUnitTestSuiteDto = GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script.resource_path, test_suite.get_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])
# first detect removed/renamed tests
for test_case in discovered_test_cases:
if not script_test_cases.has(test_case):
tests_removed.append(test_case)
# second detect new added tests
for test_case in script_test_cases:
if not discovered_test_cases.has(test_case):
tests_added.append(test_case)
# 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))
# emit new discovered tests
for test_name in tests_added:
discovered_test_cases.append(test_name)
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))
# update the cache
_discover_cache[script.resource_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 map_func_names(method_info :Dictionary) -> String:
return method_info["name"]
func filter_test_cases(value :String) -> bool:
return value.begins_with("test_")
func filter_by_test_cases(method_info :Dictionary, value :String) -> bool:
return method_info["name"] == value

View File

@ -0,0 +1,25 @@
class_name GdUnitTestDiscoverer
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
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))
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 in _test_suites_to_process:
var ts_dto := GdUnitTestSuiteDto.of(test_suite)
GdUnitSignals.instance().gdunit_add_test_suite.emit(ts_dto)
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

View File

@ -0,0 +1,190 @@
class_name GdUnitEvent
extends Resource
const WARNINGS = "warnings"
const FAILED = "failed"
const ERRORS = "errors"
const SKIPPED = "skipped"
const ELAPSED_TIME = "elapsed_time"
const ORPHAN_NODES = "orphan_nodes"
const ERROR_COUNT = "error_count"
const FAILED_COUNT = "failed_count"
const SKIPPED_COUNT = "skipped_count"
enum {
INIT,
STOP,
TESTSUITE_BEFORE,
TESTSUITE_AFTER,
TESTCASE_BEFORE,
TESTCASE_AFTER,
DISCOVER_START,
DISCOVER_END,
DISCOVER_SUITE_ADDED,
DISCOVER_TEST_ADDED,
DISCOVER_TEST_REMOVED,
}
var _event_type :int
var _resource_path :String
var _suite_name :String
var _test_name :String
var _total_count :int = 0
var _statistics := Dictionary()
var _reports :Array[GdUnitReport] = []
func suite_before(p_resource_path :String, p_suite_name :String, p_total_count :int) -> GdUnitEvent:
_event_type = TESTSUITE_BEFORE
_resource_path = p_resource_path
_suite_name = p_suite_name
_test_name = "before"
_total_count = p_total_count
return self
func suite_after(p_resource_path :String, p_suite_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent:
_event_type = TESTSUITE_AFTER
_resource_path = p_resource_path
_suite_name = p_suite_name
_test_name = "after"
_statistics = p_statistics
_reports = p_reports
return self
func test_before(p_resource_path :String, p_suite_name :String, p_test_name :String) -> GdUnitEvent:
_event_type = TESTCASE_BEFORE
_resource_path = p_resource_path
_suite_name = p_suite_name
_test_name = p_test_name
return self
func test_after(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent:
_event_type = TESTCASE_AFTER
_resource_path = p_resource_path
_suite_name = p_suite_name
_test_name = p_test_name
_statistics = p_statistics
_reports = p_reports
return self
func type() -> int:
return _event_type
func suite_name() -> String:
return _suite_name
func test_name() -> String:
return _test_name
func elapsed_time() -> int:
return _statistics.get(ELAPSED_TIME, 0)
func orphan_nodes() -> int:
return _statistics.get(ORPHAN_NODES, 0)
func statistic(p_type :String) -> int:
return _statistics.get(p_type, 0)
func total_count() -> int:
return _total_count
func success_count() -> int:
return total_count() - error_count() - failed_count() - skipped_count()
func error_count() -> int:
return _statistics.get(ERROR_COUNT, 0)
func failed_count() -> int:
return _statistics.get(FAILED_COUNT, 0)
func skipped_count() -> int:
return _statistics.get(SKIPPED_COUNT, 0)
func resource_path() -> String:
return _resource_path
func is_success() -> bool:
return not is_warning() and not is_failed() and not is_error() and not is_skipped()
func is_warning() -> bool:
return _statistics.get(WARNINGS, false)
func is_failed() -> bool:
return _statistics.get(FAILED, false)
func is_error() -> bool:
return _statistics.get(ERRORS, false)
func is_skipped() -> bool:
return _statistics.get(SKIPPED, false)
func reports() -> Array[GdUnitReport]:
return _reports
func _to_string() -> String:
return "Event: %s %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports]
func serialize() -> Dictionary:
var serialized := {
"type" : _event_type,
"resource_path": _resource_path,
"suite_name" : _suite_name,
"test_name" : _test_name,
"total_count" : _total_count,
"statistics" : _statistics
}
serialized["reports"] = _serialize_TestReports()
return serialized
func deserialize(serialized :Dictionary) -> GdUnitEvent:
_event_type = serialized.get("type", null)
_resource_path = serialized.get("resource_path", null)
_suite_name = serialized.get("suite_name", null)
_test_name = serialized.get("test_name", "unknown")
_total_count = serialized.get("total_count", 0)
_statistics = serialized.get("statistics", Dictionary())
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"))
_reports = _deserialize_reports(reports_to_deserializ)
return self
func _serialize_TestReports() -> Array[Dictionary]:
var serialized_reports :Array[Dictionary] = []
for report in _reports:
serialized_reports.append(report.serialize())
return serialized_reports
func _deserialize_reports(p_reports :Array[Dictionary]) -> Array[GdUnitReport]:
var deserialized_reports :Array[GdUnitReport] = []
for report in p_reports:
var test_report := GdUnitReport.new().deserialize(report)
deserialized_reports.append(test_report)
return deserialized_reports

View File

@ -0,0 +1,19 @@
class_name GdUnitInit
extends GdUnitEvent
var _total_testsuites :int
func _init(p_total_testsuites :int, p_total_count :int) -> void:
_event_type = INIT
_total_testsuites = p_total_testsuites
_total_count = p_total_count
func total_test_suites() -> int:
return _total_testsuites
func total_tests() -> int:
return _total_count

View File

@ -0,0 +1,6 @@
class_name GdUnitStop
extends GdUnitEvent
func _init() -> void:
_event_type = STOP

View File

@ -0,0 +1,19 @@
class_name GdUnitEventTestDiscoverEnd
extends GdUnitEvent
var _total_testsuites: int
func _init(testsuite_count: int, test_count: int) -> void:
_event_type = DISCOVER_END
_total_testsuites = testsuite_count
_total_count = test_count
func total_test_suites() -> int:
return _total_testsuites
func total_tests() -> int:
return _total_count

View File

@ -0,0 +1,6 @@
class_name GdUnitEventTestDiscoverStart
extends GdUnitEvent
func _init() -> void:
_event_type = DISCOVER_START

View File

@ -0,0 +1,17 @@
class_name GdUnitEventTestDiscoverTestAdded
extends GdUnitEvent
var _test_case_dto: GdUnitTestCaseDto
func _init(arg_resource_path: String, arg_suite_name: String, arg_test_case_dto: GdUnitTestCaseDto) -> void:
_event_type = DISCOVER_TEST_ADDED
_resource_path = arg_resource_path
_suite_name = arg_suite_name
_test_name = arg_test_case_dto.name()
_test_case_dto = arg_test_case_dto
func test_case_dto() -> GdUnitTestCaseDto:
return _test_case_dto

View File

@ -0,0 +1,9 @@
class_name GdUnitEventTestDiscoverTestRemoved
extends GdUnitEvent
func _init(arg_resource_path: String, arg_suite_name: String, arg_test_name: String) -> void:
_event_type = DISCOVER_TEST_REMOVED
_resource_path = arg_resource_path
_suite_name = arg_suite_name
_test_name = arg_test_name

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