Add GdUnit4 and ignore animation warning concerning invalid tracks.
parent
5c1509f06f
commit
437762b2f6
|
@ -0,0 +1 @@
|
|||
{"included":{"res://tests/scenes/game_tests.gd":["test_save_game"]},"server_port":31002,"skipped":{},"version":"1.0"}
|
|
@ -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.
|
|
@ -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()
|
|
@ -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")
|
|
@ -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("[0m", "")\
|
||||
.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)
|
|
@ -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)
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="gdUnit4"
|
||||
description="Unit Testing Framework for Godot Scripts"
|
||||
author="Mike Schulze"
|
||||
version="4.3.1"
|
||||
script="plugin.gd"
|
|
@ -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)
|
|
@ -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%
|
|
@ -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
|
|
@ -0,0 +1,12 @@
|
|||
class_name Comparator
|
||||
extends Resource
|
||||
|
||||
enum {
|
||||
EQUAL,
|
||||
LESS_THAN,
|
||||
LESS_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_EQUAL,
|
||||
BETWEEN_EQUAL,
|
||||
NOT_BETWEEN_EQUAL,
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
class_name GdUnitConstants
|
||||
extends RefCounted
|
||||
|
||||
const NO_ARG :Variant = "<--null-->"
|
||||
|
||||
const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures"
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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("_", " ")
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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"
|
|
@ -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)])
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
|
@ -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()
|
|
@ -0,0 +1,6 @@
|
|||
# base interface for assert value provider
|
||||
class_name ValueProvider
|
||||
extends RefCounted
|
||||
|
||||
func get_value() -> Variant:
|
||||
return null
|
|
@ -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
|
|
@ -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)]
|
|
@ -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)
|
|
@ -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 = "[1m"
|
||||
const CSI_ITALIC = "[3m"
|
||||
const CSI_UNDERLINE = "[4m"
|
||||
|
||||
# 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("[s")
|
||||
return self
|
||||
|
||||
|
||||
func restore_cursor() -> CmdConsole:
|
||||
printraw("[u")
|
||||
return self
|
||||
|
||||
|
||||
func end_color() -> CmdConsole:
|
||||
printraw("[0m")
|
||||
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
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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 ""
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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))
|
|
@ -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")
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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")
|
|
@ -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)
|
|
@ -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("}")
|
|
@ -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()
|
|
@ -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("----------------------------------------------------------------")
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
class_name GdUnitStop
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = STOP
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
class_name GdUnitEventTestDiscoverStart
|
||||
extends GdUnitEvent
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_event_type = DISCOVER_START
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue