diff --git a/addons/gdUnit4/GdUnitRunner.cfg b/addons/gdUnit4/GdUnitRunner.cfg index 59dd7687..e3dc0ecd 100644 --- a/addons/gdUnit4/GdUnitRunner.cfg +++ b/addons/gdUnit4/GdUnitRunner.cfg @@ -1 +1 @@ -{"included":{"res://test/":[]},"server_port":31002,"skipped":{},"version":"1.0"} \ No newline at end of file +{"included":{"res://test/scene/mock_scene_test.gd":["test_instantiate_mock_data_via_scene"]},"server_port":31002,"skipped":{},"version":"1.0"} \ No newline at end of file diff --git a/addons/gdUnit4/bin/GdUnitBuildTool.gd b/addons/gdUnit4/bin/GdUnitBuildTool.gd index c4ef66cc..6e5d588c 100644 --- a/addons/gdUnit4/bin/GdUnitBuildTool.gd +++ b/addons/gdUnit4/bin/GdUnitBuildTool.gd @@ -68,7 +68,7 @@ func _idle(_delta :float) -> void: exit(RETURN_ERROR, result.error_message()) return _console.prints_color("Added testcase: %s" % result.value(), Color.CORNFLOWER_BLUE) - print_json_result(result.value()) + print_json_result(result.value() as Dictionary) exit(RETURN_SUCCESS) @@ -85,7 +85,7 @@ func exit(code :int, message :String = "") -> void: func print_json_result(result :Dictionary) -> void: # convert back to system path - var path := ProjectSettings.globalize_path(result["path"]); + var path := ProjectSettings.globalize_path(result["path"] as String) var json := 'JSON_RESULT:{"TestCases" : [{"line":%d, "path": "%s"}]}' % [result["line"], path] prints(json) diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index 7e905afc..07d0926b 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -33,6 +33,7 @@ class CLIRunner: var _headless_mode_ignore := false var _runner_config := GdUnitRunnerConfig.new() var _runner_config_file := "" + var _debug_cmd_args: = PackedStringArray() var _console := CmdConsole.new() var _cmd_options := CmdOptions.new([ CmdOption.new( @@ -105,9 +106,10 @@ class CLIRunner: func _ready() -> void: _state = INIT _report_dir = GdUnitFileAccess.current_dir() + "reports" - _executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new() + _executor = GdUnitTestSuiteExecutor.new() # stop checked first test failure to fail fast - _executor.fail_fast(true) + @warning_ignore("unsafe_cast") + (_executor as GdUnitTestSuiteExecutor).fail_fast(true) if GdUnit4CSharpApiLoader.is_mono_supported(): prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) _cs_executor = GdUnit4CSharpApiLoader.create_executor(self) @@ -123,6 +125,7 @@ class CLIRunner: prints("Finallize .. done") + @warning_ignore("unsafe_method_access") func _process(_delta :float) -> void: match _state: INIT: @@ -135,7 +138,8 @@ class CLIRunner: else: set_process(false) # process next test suite - var test_suite := _test_suites_to_process.pop_front() as Node + var test_suite: Node = _test_suites_to_process.pop_front() + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): _cs_executor.Execute(test_suite) await _cs_executor.ExecutionCompleted @@ -185,6 +189,7 @@ class CLIRunner: "Disabled fail fast!", Color.DEEP_SKY_BLUE ) + @warning_ignore("unsafe_method_access") _executor.fail_fast(false) @@ -199,13 +204,13 @@ class CLIRunner: func show_version() -> void: _console.prints_color( - "Godot %s" % Engine.get_version_info().get("string"), + "Godot %s" % Engine.get_version_info().get("string") as String, Color.DARK_SALMON ) var config := ConfigFile.new() config.load("addons/gdUnit4/plugin.cfg") _console.prints_color( - "GdUnit4 %s" % config.get_value("plugin", "version"), + "GdUnit4 %s" % config.get_value("plugin", "version") as String, Color.DARK_SALMON ) quit(RETURN_SUCCESS) @@ -274,6 +279,12 @@ class CLIRunner: quit(RETURN_SUCCESS) + func get_cmdline_args() -> PackedStringArray: + if _debug_cmd_args.is_empty(): + return OS.get_cmdline_args() + return _debug_cmd_args + + func init_gd_unit() -> void: _console.prints_color( """ @@ -284,7 +295,7 @@ class CLIRunner: ).new_line() var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") - var result := cmd_parser.parse(OS.get_cmdline_args()) + var result := cmd_parser.parse(get_cmdline_args()) if result.is_error(): show_options() _console.prints_error(result.error_message()) @@ -297,7 +308,8 @@ class CLIRunner: return # build runner config by given commands var commands :Array[CmdCommand] = [] - commands.append_array(result.value()) + @warning_ignore("unsafe_cast") + commands.append_array(result.value() as Array) result = ( CmdCommandHandler.new(_cmd_options) .register_cb("-help", Callable(self, "show_help")) @@ -412,7 +424,9 @@ class CLIRunner: # 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) + @warning_ignore("unsafe_property_access") test_suite.__is_skipped = true + @warning_ignore("unsafe_property_access") test_suite.__skip_reason = skip_reason else: # skip tests @@ -443,10 +457,8 @@ class CLIRunner: func _on_gdunit_event(event: GdUnitEvent) -> void: match event.type(): GdUnitEvent.INIT: - _report = GdUnitHtmlReport.new(_report_dir) + _report = GdUnitHtmlReport.new(_report_dir, _report_max) GdUnitEvent.STOP: - if _report == null: - _report = GdUnitHtmlReport.new(_report_dir) var report_path := _report.write() _report.delete_history(_report_max) JUnitXmlReport.new(_report._report_path, _report.iteration()).write(_report) @@ -464,45 +476,31 @@ class CLIRunner: Color.CORNFLOWER_BLUE ) GdUnitEvent.TESTSUITE_BEFORE: - _report.add_testsuite_report( - GdUnitTestSuiteReport.new(event.resource_path(), event.suite_name(), event.total_count()) - ) + _report.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) GdUnitEvent.TESTSUITE_AFTER: - _report.update_test_suite_report( + _report.add_testsuite_reports( event.resource_path(), - event.elapsed_time(), - event.is_error(), - event.is_failed(), - event.is_warning(), - event.is_skipped(), - event.skipped_count(), + event.error_count(), event.failed_count(), event.orphan_nodes(), + event.elapsed_time(), event.reports() ) GdUnitEvent.TESTCASE_BEFORE: - _report.add_testcase_report( - event.resource_path(), - GdUnitTestCaseReport.new( - event.resource_path(), - event.suite_name(), - event.test_name() - ) - ) + _report.add_testcase(event.resource_path(), event.suite_name(), event.test_name()) GdUnitEvent.TESTCASE_AFTER: - var test_report := GdUnitTestCaseReport.new( - event.resource_path(), - event.suite_name(), + _report.set_testcase_counters(event.resource_path(), event.test_name(), event.is_error(), - event.is_failed(), event.failed_count(), event.orphan_nodes(), event.is_skipped(), - event.reports(), - event.elapsed_time() - ) - _report.update_testcase_report(event.resource_path(), test_report) + event.is_flaky(), + event.elapsed_time()) + _report.add_testcase_reports(event.resource_path(), event.test_name(), event.reports()) + GdUnitEvent.TESTCASE_STATISTICS: + _report.update_testsuite_counters(event.resource_path(), event.is_error(), event.failed_count(), event.orphan_nodes(),\ + event.is_skipped(), event.is_flaky(), event.elapsed_time()) print_status(event) @@ -556,11 +554,12 @@ class CLIRunner: _print_failure_report(event.reports()) _print_status(event) _console.prints_color( - "Statistics: | %d tests cases | %d error | %d failed | %d skipped | %d orphans |\n" + "Statistics: | %d tests cases | %d error | %d failed | %d flaky | %d skipped | %d orphans |\n" % [ _report.test_count(), _report.error_count(), _report.failure_count(), + _report.flaky_count(), _report.skipped_count(), _report.orphan_count() ], @@ -587,14 +586,22 @@ class CLIRunner: func _print_status(event: GdUnitEvent) -> void: - if event.is_skipped(): + if event.is_flaky() and event.is_success(): + var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) + _console.print_color("FLAKY (%d retries)" % retries, Color.GREEN_YELLOW, CmdConsole.BOLD | CmdConsole.ITALIC) + elif event.is_success(): + _console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD) + elif event.is_skipped(): _console.print_color("SKIPPED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.ITALIC) elif event.is_failed() or event.is_error(): - _console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD) - elif event.orphan_nodes() > 0: - _console.print_color("PASSED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE) - else: - _console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD) + var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) + if retries > 1: + _console.print_color("FAILED (retry %d)" % retries, Color.FIREBRICK, CmdConsole.BOLD) + else: + _console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD) + elif event.is_warning(): + _console.print_color("WARNING", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE) + _console.prints_color( " %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE ) diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd index 2e037a69..084ac72d 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -4,23 +4,30 @@ extends MainLoop const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") # gdlint: disable=max-line-length -const NO_LOG_TEMPLATE = """ +const LOG_FRAME_TEMPLATE = """ - + - - - Logging - + + + Godot Logging + - -
-

No logging available!

-
-

For logging to occur, you must check Enable File Logging in Project Settings.

-

You can enable Logging Project Settings > Logging > File Logging > Enable File Logging in the Project Settings.

+ + +
+${content}
+ +""" + +const NO_LOG_MESSAGE = """ +

No logging available!

+
+

In order for logging to take place, you must activate the Activate file logging option in the project settings.

+

You can enable the logging under: +Project Settings > Debug > File Logging > Enable File Logging in the project settings.

""" #warning-ignore-all:return_value_discarded @@ -34,48 +41,65 @@ var _cmd_options := CmdOptions.new([ ) ]) + var _report_root_path: String +var _current_report_path: String +var _debug_cmd_args := PackedStringArray() func _init() -> void: - _report_root_path = GdUnitFileAccess.current_dir() + "reports" + set_report_directory(GdUnitFileAccess.current_dir() + "reports") + set_current_report_path() -func _process(_delta :float) -> bool: +func _process(_delta: float) -> bool: # check if reports exists if not reports_available(): prints("no reports found") return true - # scan for latest report path - var iteration := GdUnitFileAccess.find_last_path_index( - _report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX - ) - var report_path := "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration] + # only process if godot logging is enabled if not GdUnitSettings.is_log_enabled(): - _patch_report(report_path, "") + write_report(NO_LOG_MESSAGE, "") return true + # parse possible custom report path, var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") # ignore erros and exit quitly - if cmd_parser.parse(OS.get_cmdline_args(), true).is_error(): + if cmd_parser.parse(get_cmdline_args(), true).is_error(): return true CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory) - # scan for latest godot log and copy to report - var godot_log := _scan_latest_godot_log() - var result := _copy_and_pach(godot_log, report_path) + + var godot_log_file := scan_latest_godot_log() + var result := read_log_file_content(godot_log_file) if result.is_error(): - push_error(result.error_message()) + write_report(result.error_message(), godot_log_file) return true - _patch_report(report_path, godot_log) + write_report(result.value_as_string(), godot_log_file) return true +func set_current_report_path() -> void: + # scan for latest report directory + var iteration := GdUnitFileAccess.find_last_path_index( + _report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX + ) + _current_report_path = "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration] + + func set_report_directory(path: String) -> void: _report_root_path = path -func _scan_latest_godot_log() -> String: +func get_log_report_html() -> String: + return _current_report_path + "/godot_report_log.html" + + +func reports_available() -> bool: + return DirAccess.dir_exists_absolute(_report_root_path) + + +func scan_latest_godot_log() -> String: var path := GdUnitSettings.get_log_path().get_base_dir() var files_sorted := Array() for file in GdUnitFileAccess.scan_dir(path): @@ -83,59 +107,60 @@ func _scan_latest_godot_log() -> String: files_sorted.append(file_name) # sort by name, the name contains the timestamp so we sort at the end by timestamp files_sorted.sort() - return files_sorted[-1] - - -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) + return files_sorted.back() -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) +func read_log_file_content(log_file: String) -> GdUnitResult: + var file := FileAccess.open(log_file, FileAccess.READ) if file == null: return GdUnitResult.error( - "Can't find file '%s'. Error: %s" - % [from_file, error_string(FileAccess.get_open_error())] + "Can't find log file '%s'. Error: %s" + % [log_file, error_string(FileAccess.get_open_error())] ) - var content := file.get_as_text() + 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 = content\ .replace("", "")\ .replace(CmdConsole.CSI_BOLD, "")\ .replace(CmdConsole.CSI_ITALIC, "")\ .replace(CmdConsole.CSI_UNDERLINE, "") - var to_file := to_dir + "/" + from_file.get_file() - file = FileAccess.open(to_file, FileAccess.WRITE) + return GdUnitResult.success(content) + + +func write_report(content: String, godot_log_file: String) -> GdUnitResult: + var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE) if file == null: return GdUnitResult.error( "Can't open to write '%s'. Error: %s" - % [to_file, error_string(FileAccess.get_open_error())] + % [get_log_report_html(), error_string(FileAccess.get_open_error())] ) - file.store_string(content) - return GdUnitResult.empty() + var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content) + file.store_string(report_html) + _update_index_html(godot_log_file) + return GdUnitResult.success(file) -func reports_available() -> bool: - return DirAccess.dir_exists_absolute(_report_root_path) +func _update_index_html(godot_log_file: String) -> void: + var index_file := FileAccess.open("%s/index.html" % _current_report_path, FileAccess.READ_WRITE) + if index_file == null: + push_error( + "Can't add log path to index.html. Error: %s" + % error_string(FileAccess.get_open_error()) + ) + return + var content := index_file.get_as_text()\ + .replace("${log_report}", get_log_report_html())\ + .replace("${godot_log_file}", godot_log_file) + # overide it + index_file.seek(0) + index_file.store_string(content) + + +func get_cmdline_args() -> PackedStringArray: + if _debug_cmd_args.is_empty(): + return OS.get_cmdline_args() + return _debug_cmd_args diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg index cf7c92f1..6fdf5032 100644 --- a/addons/gdUnit4/plugin.cfg +++ b/addons/gdUnit4/plugin.cfg @@ -3,5 +3,5 @@ name="gdUnit4" description="Unit Testing Framework for Godot Scripts" author="Mike Schulze" -version="4.3.3" +version="4.4.1" script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index dd6d87da..b7887c8a 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -5,13 +5,14 @@ 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 _gd_console :Node +var _gd_inspector: Control +var _gd_console: Control var _guard: GdUnitTestDiscoverGuard func _enter_tree() -> void: if check_running_in_test_env(): + @warning_ignore("return_value_discarded") CmdConsole.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!") return if Engine.get_version_info().hex < 0x40200: @@ -23,6 +24,7 @@ func _enter_tree() -> void: add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector) # install the GdUnit Console _gd_console = load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn").instantiate() + @warning_ignore("return_value_discarded") add_control_to_bottom_panel(_gd_console, "gdUnitConsole") prints("Loading GdUnit4 Plugin success") if GdUnitSettings.is_update_notification_enabled(): @@ -32,6 +34,7 @@ func _enter_tree() -> void: prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) # connect to be notified for script changes to be able to discover new tests _guard = GdUnitTestDiscoverGuard.new() + @warning_ignore("return_value_discarded") resource_saved.connect(_on_resource_saved) @@ -56,4 +59,4 @@ func check_running_in_test_env() -> bool: func _on_resource_saved(resource: Resource) -> void: if resource is Script: - await _guard.discover(resource) + await _guard.discover(resource as Script) diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd index fe056d8b..6b354750 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -18,19 +18,19 @@ func is_not_null(): ## Verifies that the current value is equal to expected one. @warning_ignore("unused_parameter") @warning_ignore("untyped_declaration") -func is_equal(expected): +func is_equal(expected: Variant): return self ## Verifies that the current value is not equal to expected one. @warning_ignore("unused_parameter") @warning_ignore("untyped_declaration") -func is_not_equal(expected): +func is_not_equal(expected: Variant): return self @warning_ignore("untyped_declaration") -func test_fail(): +func do_fail(): return self diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd index fc2e487b..51385e88 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -1,8 +1,6 @@ class_name GdUnitAwaiter extends RefCounted -const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") - # Waits for a specified signal in an interval of 50ms sent from the , and terminates with an error after the specified timeout has elapsed. # source: the object from which the signal is emitted @@ -14,16 +12,19 @@ func await_signal_on(source :Object, signal_name :String, args :Array = [], time var assert_that := GdUnitAssertImpl.new(signal_name) var line_number := GdUnitAssertions.get_line_number() if not is_instance_valid(source): + @warning_ignore("return_value_discarded") assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) - return await Engine.get_main_loop().process_frame + return await (Engine.get_main_loop() as SceneTree).process_frame # fail fast if the given source instance invalid if not is_instance_valid(source): + @warning_ignore("return_value_discarded") assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) return await await_idle_frame() var awaiter := GdUnitSignalAwaiter.new(timeout_millis) var value :Variant = await awaiter.on_signal(source, signal_name, args) if awaiter.is_interrupted(): var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + @warning_ignore("return_value_discarded") assert_that.report_error(failure, line_number) return value @@ -37,6 +38,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array = var line_number := GdUnitAssertions.get_line_number() # fail fast if the given source instance invalid if not is_instance_valid(source): + @warning_ignore("return_value_discarded") GdUnitAssertImpl.new(signal_name)\ .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) return await await_idle_frame() @@ -44,6 +46,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array = var value :Variant = await awaiter.on_signal(source, signal_name, args) if awaiter.is_interrupted(): var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis] + @warning_ignore("return_value_discarded") GdUnitAssertImpl.new(signal_name).report_error(failure, line_number) return value @@ -56,7 +59,7 @@ func await_signal_idle_frames(source :Object, signal_name :String, args :Array = func await_millis(milliSec :int) -> void: var timer :Timer = Timer.new() timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id()) - Engine.get_main_loop().root.add_child(timer) + (Engine.get_main_loop() as SceneTree).root.add_child(timer) timer.add_to_group("GdUnitTimers") timer.set_one_shot(true) timer.start(milliSec / 1000.0) @@ -66,4 +69,4 @@ func await_millis(milliSec :int) -> void: # Waits until the next idle frame func await_idle_frame() -> void: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd index 8f24f561..6ce5f1e8 100644 --- a/addons/gdUnit4/src/GdUnitFloatAssert.gd +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd @@ -3,15 +3,15 @@ class_name GdUnitFloatAssert extends GdUnitAssert -## Verifies that the current value is equal to expected one. +## Verifies that the current String is equal to the given one. @warning_ignore("unused_parameter") -func is_equal(expected :float) -> GdUnitFloatAssert: +func is_equal(expected :Variant) -> GdUnitFloatAssert: return self -## Verifies that the current value is not equal to expected one. +## Verifies that the current String is not equal to the given one. @warning_ignore("unused_parameter") -func is_not_equal(expected :float) -> GdUnitFloatAssert: +func is_not_equal(expected :Variant) -> GdUnitFloatAssert: return self diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd index c5e0e5fc..75e8ebd6 100644 --- a/addons/gdUnit4/src/GdUnitFuncAssert.gd +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd @@ -5,39 +5,39 @@ extends GdUnitAssert ## Verifies that the current value is null. func is_null() -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that the current value is not null. func is_not_null() -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that the current value is equal to the given one. @warning_ignore("unused_parameter") func is_equal(expected :Variant) -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that the current value is not equal to the given one. @warning_ignore("unused_parameter") func is_not_equal(expected :Variant) -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that the current value is true. func is_true() -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that the current value is false. func is_false() -> GdUnitFuncAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd index d689625a..c5e9f863 100644 --- a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd @@ -9,7 +9,7 @@ extends GdUnitAssert ## await assert_error().is_success() ## [/codeblock] func is_success() -> GdUnitGodotErrorAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -20,7 +20,7 @@ func is_success() -> GdUnitGodotErrorAssert: ## [/codeblock] @warning_ignore("unused_parameter") func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -31,7 +31,7 @@ func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: ## [/codeblock] @warning_ignore("unused_parameter") func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -42,5 +42,5 @@ func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: ## [/codeblock] @warning_ignore("unused_parameter") func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd index 584788fc..d593edf9 100644 --- a/addons/gdUnit4/src/GdUnitIntAssert.gd +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd @@ -2,16 +2,15 @@ class_name GdUnitIntAssert extends GdUnitAssert - -## Verifies that the current value is equal to expected one. +## Verifies that the current String is equal to the given one. @warning_ignore("unused_parameter") -func is_equal(expected :int) -> GdUnitIntAssert: +func is_equal(expected :Variant) -> GdUnitIntAssert: return self -## Verifies that the current value is not equal to expected one. +## Verifies that the current String is not equal to the given one. @warning_ignore("unused_parameter") -func is_not_equal(expected :int) -> GdUnitIntAssert: +func is_not_equal(expected :Variant) -> GdUnitIntAssert: return self diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd index 5707a6ae..184be50a 100644 --- a/addons/gdUnit4/src/GdUnitSceneRunner.gd +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd @@ -1,44 +1,30 @@ -## The scene runner for GdUnit to simmulate scene interactions +## The Scene Runner is a tool used for simulating interactions on a scene. +## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames. +## This tool is typically used for integration testing a scene. class_name GdUnitSceneRunner extends RefCounted const NO_ARG = GdUnitConstants.NO_ARG -## Sets the mouse cursor to given position relative to the viewport. -@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: +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: +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: +func simulate_action_release(action: String) -> GdUnitSceneRunner: return self @@ -46,8 +32,14 @@ func simulate_action_release(action :String) -> GdUnitSceneRunner: ## [member key_code] : the key code e.g. [constant KEY_ENTER][br] ## [member shift_pressed] : false by default set to true if simmulate shift is press[br] ## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] +## [codeblock] +## func test_key_presssed(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## await runner.simulate_key_pressed(KEY_SPACE) +## [/codeblock] @warning_ignore("unused_parameter") -func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: +func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -56,7 +48,7 @@ func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := ## [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: +func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: return self @@ -65,14 +57,38 @@ func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := f ## [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: +func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: + return self + + +## Sets the mouse cursor to given position relative to the viewport. +## @deprecated: Use [set_mouse_position] instead. +@warning_ignore("unused_parameter") +func set_mouse_pos(position: Vector2) -> GdUnitSceneRunner: return self +## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br] +## [member position] : The absolute position in pixels as Vector2 +@warning_ignore("unused_parameter") +func set_mouse_position(position: Vector2) -> GdUnitSceneRunner: + return self + + +## Returns the mouse's position in this Viewport using the coordinate system of this Viewport. +func get_mouse_position() -> Vector2: + return Vector2.ZERO + + +## Gets the current global mouse position of the current window +func get_global_mouse_position() -> Vector2: + return Vector2.ZERO + + ## Simulates a mouse moved to final position.[br] -## [member pos] : The final mouse position +## [member position] : The final mouse position @warning_ignore("unused_parameter") -func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner: +func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner: return self @@ -89,7 +105,7 @@ func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner: ## [/codeblock] @warning_ignore("unused_parameter") func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -106,36 +122,149 @@ func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_ty ## [/codeblock] @warning_ignore("unused_parameter") func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Simulates a mouse button pressed.[br] -## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member double_click] : Set to true to simulate a double-click @warning_ignore("unused_parameter") -func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: +func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: return self ## Simulates a mouse button press (holding)[br] -## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member double_click] : Set to true to simulate a double-click @warning_ignore("unused_parameter") -func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: +func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: return self ## Simulates a mouse button released.[br] -## [member buttonIndex] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. +@warning_ignore("unused_parameter") +func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner: + return self + + +## Simulates a screen touch is pressed.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + return self + + +## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The position to touch the screen.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + return self + + +## Simulates a screen touch is released.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member double_tap] : If true, the touch's state is a double tab. +@warning_ignore("unused_parameter") +func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner: + return self + + +## Simulates a touch drag and drop event to a relative position.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member relative] : The relative position, indicating the drag&drop position offset.[br] +## [member time] : The time to move to the relative position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at final at 150,50 relative (50,50 + 100,0) +## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame + return self + + +## Simulates a touch screen drop to the absolute coordinates (offset).[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The final position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 +## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50)) +## # and drop it at 100,50 +## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50)) +## [/codeblock] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame + return self + + +## Simulates a complete drag and drop event from one position to another.[br] +## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br] +## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br] +## [br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The drag start position, indicating the drag position.[br] +## [member drop_position] : The drop position, indicating the drop position.[br] +## [member time] : The time to move to the final position in seconds (default is 1 second).[br] +## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br] +## [codeblock] +## func test_touch_drag_drop(): +## var runner = scene_runner("res://scenes/simple_scene.tscn") +## # start drag at position 50,50 and drop it at 100,50 +## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50)) +## [/codeblock] @warning_ignore("unused_parameter") -func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner: +func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame return self +## Simulates a touch screen drag event to given position.[br] +## [member index] : The touch index in the case of a multi-touch event.[br] +## [member position] : The drag start position, indicating the drag position.[br] +@warning_ignore("unused_parameter") +func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner: + return self + + +## Returns the actual position of the touchscreen drag position by given index. +## [member index] : The touch index in the case of a multi-touch event.[br] +@warning_ignore("unused_parameter") +func get_screen_touch_drag_position(index: int) -> Vector2: + return Vector2.ZERO + + ## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br] ## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life, ## whilst a value of 0.5 means the game moves at half the regular speed. + + +## Sets the time factor for the scene simulation. +## [member time_factor] : A float representing the simulation speed.[br] +## - Default is 1.0, meaning the simulation runs at normal speed.[br] +## - A value of 2.0 means the simulation runs twice as fast as real time.[br] +## - A value of 0.5 means the simulation runs at half the regular speed.[br] @warning_ignore("unused_parameter") -func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: +func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner: return self @@ -143,8 +272,8 @@ func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: ## [member frames] : amount of frames to process[br] ## [member delta_milli] : the time delta between a frame in milliseconds @warning_ignore("unused_parameter") -func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: - await Engine.get_main_loop().process_frame +func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -153,18 +282,18 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: ## [member args] : optional signal arguments to be matched for stop[br] @warning_ignore("unused_parameter") func simulate_until_signal( - signal_name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> GdUnitSceneRunner: - await Engine.get_main_loop().process_frame + signal_name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame return self @@ -174,64 +303,103 @@ func simulate_until_signal( ## [member args] : optional signal arguments to be matched for stop @warning_ignore("unused_parameter") func simulate_until_object_signal( - source :Object, - signal_name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> GdUnitSceneRunner: - await Engine.get_main_loop().process_frame + source: Object, + signal_name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> GdUnitSceneRunner: + await (Engine.get_main_loop() as SceneTree).process_frame return self -### Waits for all input events are processed +## Waits for all input events to be processed by flushing any buffered input events +## and then awaiting a full cycle of both the process and physics frames.[br] +## [br] +## This is typically used to ensure that any simulated or queued inputs are fully +## processed before proceeding with the next steps in the scene.[br] +## It's essential for reliable input simulation or when synchronizing logic based +## on inputs.[br] +## +## Usage Example: +## [codeblock] +## await await_input_processed() # Ensure all inputs are processed before continuing +## [/codeblock] func await_input_processed() -> void: - await Engine.get_main_loop().process_frame - await Engine.get_main_loop().physics_frame + if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame -## Waits for the function return value until specified timeout or fails.[br] -## [member args] : optional function arguments +## The await_func function pauses execution until a specified function in the scene returns a value.[br] +## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br] +## [member func_name] : The name of the function to wait for.[br] +## [member args] : Optional function arguments +## [br] +## Usage Example: +## [codeblock] +## # Waits for 'calculate_score' function and verifies the result is equal to 100. +## await_func("calculate_score").is_equal(100) +## [/codeblock] @warning_ignore("unused_parameter") -func await_func(func_name :String, args := []) -> GdUnitFuncAssert: +func await_func(func_name: String, args := []) -> GdUnitFuncAssert: return null -## Waits for the function return value of specified source until specified timeout or fails.[br] -## [member source : the object where implements the function[br] + +## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br] +## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br] +## [member source] : The object where implements the function.[br] +## [member func_name] : The name of the function to wait for.[br] ## [member args] : optional function arguments +## [br] +## Usage Example: +## [codeblock] +## # Waits for 'calculate_score' function and verifies the result is equal to 100. +## var my_instance := ScoreCalculator.new() +## await_func(my_instance, "calculate_score").is_equal(100) +## [/codeblock] @warning_ignore("unused_parameter") -func await_func_on(source :Object, func_name :String, args := []) -> GdUnitFuncAssert: +func await_func_on(source: Object, func_name: String, args := []) -> GdUnitFuncAssert: return null -## Waits for given signal is emited by the scene until a specified timeout to fail.[br] -## [member signal_name] : signal name[br] -## [member args] : the expected signal arguments as an array[br] -## [member timeout] : the timeout in ms, default is set to 2000ms +## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br] +## [member signal_name] : The name of the signal to wait for[br] +## [member args] : The signal arguments as an array[br] +## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing @warning_ignore("unused_parameter") -func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void: - await Engine.get_main_loop().process_frame +func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void: + await (Engine.get_main_loop() as SceneTree).process_frame pass -## Waits for given signal is emited by the until a specified timeout to fail.[br] +## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br] ## [member source] : the object from which the signal is emitted[br] -## [member signal_name] : signal name[br] -## [member args] : the expected signal arguments as an array[br] -## [member timeout] : the timeout in ms, default is set to 2000ms +## [member signal_name] : The name of the signal to wait for[br] +## [member args] : The signal arguments as an array[br] +## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing @warning_ignore("unused_parameter") -func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void: +func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void: pass -## maximizes the window to bring the scene visible +## Restores the scene window to a windowed mode and brings it to the foreground.[br] +## This ensures that the scene is visible and active during testing, making it easier to observe and interact with. +func move_window_to_foreground() -> GdUnitSceneRunner: + return self + + +## Restores the scene window to a windowed mode and brings it to the foreground.[br] +## This ensures that the scene is visible and active during testing, making it easier to observe and interact with. +## @deprecated: Use [move_window_to_foreground] instead. func maximize_view() -> GdUnitSceneRunner: return self @@ -240,7 +408,7 @@ func maximize_view() -> GdUnitSceneRunner: ## [member name] : name of property[br] ## [member return] : the value of the property @warning_ignore("unused_parameter") -func get_property(name :String) -> Variant: +func get_property(name: String) -> Variant: return null ## Set the value of the property with the name .[br] @@ -248,7 +416,7 @@ func get_property(name :String) -> Variant: ## [member value] : value of property[br] ## [member return] : true|false depending on valid property name. @warning_ignore("unused_parameter") -func set_property(name :String, value :Variant) -> bool: +func set_property(name: String, value: Variant) -> bool: return false @@ -258,17 +426,17 @@ func set_property(name :String, value :Variant) -> bool: ## [member return] : the function result @warning_ignore("unused_parameter") func invoke( - name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> Variant: + name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> Variant: return null @@ -277,7 +445,7 @@ func invoke( ## [member recursive] : enables/disables seraching recursive[br] ## [member return] : the node if find otherwise null @warning_ignore("unused_parameter") -func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node: +func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node: return null diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd index 8150df23..9dbc76d3 100644 --- a/addons/gdUnit4/src/GdUnitSignalAssert.gd +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd @@ -6,14 +6,14 @@ extends GdUnitAssert ## Verifies that given signal is emitted until waiting time @warning_ignore("unused_parameter") func is_emitted(name :String, args := []) -> GdUnitSignalAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self ## Verifies that given signal is NOT emitted until waiting time @warning_ignore("unused_parameter") func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return self diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd index f23cbd74..13241f4a 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -23,8 +23,6 @@ var __is_skipped := false var __skip_reason :String = "Unknow." var __active_test_case :String var __awaiter := __gdunit_awaiter() -# holds the actual execution context -var __execution_context :RefCounted ### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" @@ -100,25 +98,25 @@ func error_as_string(error_number :int) -> String: ## A litle helper to auto freeing your created objects after test execution func auto_free(obj :Variant) -> Variant: - if __execution_context != null: - return __execution_context.register_auto_free(obj) - else: - if is_instance_valid(obj): - obj.queue_free() - return obj + var execution_context := GdUnitThreadManager.get_current_context().get_execution_context() + + assert(execution_context != null, "INTERNAL ERROR: The current execution_context is null! Please report this as bug.") + return execution_context.register_auto_free(obj) @warning_ignore("native_method_override") func add_child(node :Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void: super.add_child(node, force_readable_name, internal) - if __execution_context != null: - __execution_context.orphan_monitor_start() + var execution_context := GdUnitThreadManager.get_current_context().get_execution_context() + if execution_context != null: + execution_context.orphan_monitor_start() ## Discard the error message triggered by a timeout (interruption).[br] ## By default, an interrupted test is reported as an error.[br] ## This function allows you to change the message to Success when an interrupted error is reported. func discard_error_interupted_by_timeout() -> void: + @warning_ignore("unsafe_method_access") __gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case) @@ -126,12 +124,14 @@ func discard_error_interupted_by_timeout() -> void: ## Useful for storing data during test execution. [br] ## The directory is automatically deleted after test suite execution func create_temp_dir(relative_path :String) -> String: + @warning_ignore("unsafe_method_access") return __gdunit_file_access().create_temp_dir(relative_path) ## Deletes the temporary base directory[br] ## Is called automatically after each execution of the test suite func clean_temp_dir() -> void: + @warning_ignore("unsafe_method_access") __gdunit_file_access().clear_tmp() @@ -139,28 +139,26 @@ func clean_temp_dir() -> void: ## with given name and given file (default = File.WRITE)[br] ## If success the returned File is automatically closed after the execution of the test suite func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: + @warning_ignore("unsafe_method_access") return __gdunit_file_access().create_temp_file(relative_path, file_name, mode) ## Reads a resource by given path into a PackedStringArray. func resource_as_array(resource_path :String) -> PackedStringArray: + @warning_ignore("unsafe_method_access") return __gdunit_file_access().resource_as_array(resource_path) ## Reads a resource by given path and returned the content as String. func resource_as_string(resource_path :String) -> String: + @warning_ignore("unsafe_method_access") return __gdunit_file_access().resource_as_string(resource_path) ## Reads a resource by given path and return Variand translated by str_to_var func resource_as_var(resource_path :String) -> Variant: - return str_to_var(__gdunit_file_access().resource_as_string(resource_path)) - - -## clears the debuger error list[br] -## PROTOTYPE!!!! Don't use it for now -func clear_push_errors() -> void: - __gdunit_tools().clear_push_errors() + @warning_ignore("unsafe_method_access", "unsafe_cast") + return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String) ## Waits for given signal is emited by the until a specified timeout to fail[br] @@ -169,11 +167,13 @@ func clear_push_errors() -> void: ## args: the expected signal arguments as an array[br] ## timeout: the timeout in ms, default is set to 2000ms func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> Variant: + @warning_ignore("unsafe_method_access") return await __awaiter.await_signal_on(source, signal_name, args, timeout) ## Waits until the next idle frame func await_idle_frame() -> void: + @warning_ignore("unsafe_method_access") await __awaiter.await_idle_frame() @@ -185,6 +185,7 @@ func await_idle_frame() -> void: ## [/codeblock][br] ## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out func await_millis(timeout :int) -> void: + @warning_ignore("unsafe_method_access") await __awaiter.await_millis(timeout) @@ -216,11 +217,13 @@ const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB ## Creates a mock for given class name func mock(clazz :Variant, mock_mode := RETURN_DEFAULTS) -> Variant: + @warning_ignore("unsafe_method_access") return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode) ## Creates a spy checked given object instance func spy(instance :Variant) -> Variant: + @warning_ignore("unsafe_method_access") return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance) @@ -236,21 +239,25 @@ func do_return(value :Variant) -> GdUnitMock: ## Verifies certain behavior happened at least once or exact number of times func verify(obj :Variant, times := 1) -> Variant: + @warning_ignore("unsafe_method_access") return __gdunit_object_interactions().verify(obj, times) ## Verifies no interactions is happen checked this mock or spy func verify_no_interactions(obj :Variant) -> GdUnitAssert: + @warning_ignore("unsafe_method_access") return __gdunit_object_interactions().verify_no_interactions(obj) ## Verifies the given mock or spy has any unverified interaction. func verify_no_more_interactions(obj :Variant) -> GdUnitAssert: + @warning_ignore("unsafe_method_access") return __gdunit_object_interactions().verify_no_more_interactions(obj) ## Resets the saved function call counters checked a mock or spy func reset(obj :Variant) -> void: + @warning_ignore("unsafe_method_access") __gdunit_object_interactions().reset(obj) @@ -267,6 +274,7 @@ func reset(obj :Variant) -> void: ## await assert_signal(emitter).is_emitted('my_signal') ## [/codeblock] func monitor_signals(source :Object, _auto_free := true) -> Object: + @warning_ignore("unsafe_method_access") __lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\ .get_current_context()\ .get_signal_collector()\ @@ -277,36 +285,43 @@ func monitor_signals(source :Object, _auto_free := true) -> Object: # === Argument matchers ======================================================== ## Argument matcher to match any argument func any() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().any() ## Argument matcher to match any boolean value func any_bool() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_BOOL) ## Argument matcher to match any integer value func any_int() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_INT) ## Argument matcher to match any float value func any_float() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_FLOAT) ## Argument matcher to match any string value func any_string() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_STRING) ## Argument matcher to match any Color value func any_color() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_COLOR) ## Argument matcher to match any Vector typed value func any_vector() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_types([ TYPE_VECTOR2, TYPE_VECTOR2I, @@ -319,141 +334,169 @@ func any_vector() -> GdUnitArgumentMatcher: ## Argument matcher to match any Vector2 value func any_vector2() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR2) ## Argument matcher to match any Vector2i value func any_vector2i() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I) ## Argument matcher to match any Vector3 value func any_vector3() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR3) ## Argument matcher to match any Vector3i value func any_vector3i() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I) ## Argument matcher to match any Vector4 value func any_vector4() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR4) ## Argument matcher to match any Vector3i value func any_vector4i() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I) ## Argument matcher to match any Rect2 value func any_rect2() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_RECT2) ## Argument matcher to match any Plane value func any_plane() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PLANE) ## Argument matcher to match any Quaternion value func any_quat() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_QUATERNION) ## Argument matcher to match any AABB value func any_aabb() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_AABB) ## Argument matcher to match any Basis value func any_basis() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_BASIS) ## Argument matcher to match any Transform2D value func any_transform_2d() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D) ## Argument matcher to match any Transform3D value func any_transform_3d() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D) ## Argument matcher to match any NodePath value func any_node_path() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH) ## Argument matcher to match any RID value func any_rid() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_RID) ## Argument matcher to match any Object value func any_object() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_OBJECT) ## Argument matcher to match any Dictionary value func any_dictionary() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY) ## Argument matcher to match any Array value func any_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_ARRAY) ## Argument matcher to match any PackedByteArray value func any_packed_byte_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY) ## Argument matcher to match any PackedInt32Array value func any_packed_int32_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY) ## Argument matcher to match any PackedInt64Array value func any_packed_int64_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY) ## Argument matcher to match any PackedFloat32Array value func any_packed_float32_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY) ## Argument matcher to match any PackedFloat64Array value func any_packed_float64_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY) ## Argument matcher to match any PackedStringArray value func any_packed_string_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY) ## Argument matcher to match any PackedVector2Array value func any_packed_vector2_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY) ## Argument matcher to match any PackedVector3Array value func any_packed_vector3_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY) ## Argument matcher to match any PackedColorArray value func any_packed_color_array() -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY) ## Argument matcher to match any instance of given class func any_class(clazz :Object) -> GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().any_class(clazz) @@ -492,13 +535,13 @@ func assert_that(current :Variant) -> GdUnitAssert: TYPE_STRING: return assert_str(current) TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I: - return assert_vector(current) + return assert_vector(current, false) TYPE_DICTIONARY: return assert_dict(current) TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\ TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\ TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY: - return assert_array(current) + return assert_array(current, false) TYPE_OBJECT, TYPE_NIL: return assert_object(current) _: @@ -531,13 +574,13 @@ func assert_float(current :Variant) -> GdUnitFloatAssert: ## [codeblock] ## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) ## [/codeblock] -func assert_vector(current :Variant) -> GdUnitVectorAssert: - return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current) +func assert_vector(current :Variant, type_check := true) -> GdUnitVectorAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current, type_check) ## An assertion tool to verify arrays. -func assert_array(current :Variant) -> GdUnitArrayAssert: - return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current) +func assert_array(current :Variant, type_check := true) -> GdUnitArrayAssert: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current, type_check) ## An assertion tool to verify dictionaries. @@ -577,6 +620,7 @@ func assert_signal(instance :Object) -> GdUnitSignalAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure(assertion :Callable) -> GdUnitFailureAssert: + @warning_ignore("unsafe_method_access") return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion) @@ -588,6 +632,7 @@ func assert_failure(assertion :Callable) -> GdUnitFailureAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: + @warning_ignore("unsafe_method_access") return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion) @@ -608,10 +653,12 @@ func assert_error(current :Callable) -> GdUnitGodotErrorAssert: func assert_not_yet_implemented() -> void: - __gdunit_assert().new(null).test_fail() + @warning_ignore("unsafe_method_access") + __gdunit_assert().new(null).do_fail() func fail(message :String) -> void: + @warning_ignore("unsafe_method_access") __gdunit_assert().new(null).report_error(message) diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd index 315a0a52..83c2892c 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -8,8 +8,12 @@ const SUB_COLOR := Color(1, 0, 0, .3) const ADD_COLOR := Color(0, 1, 0, .3) -static func format_dict(value :Dictionary) -> String: - if value.is_empty(): +static func format_dict(value :Variant) -> String: + if not value is Dictionary: + return str(value) + + var dict_value: Dictionary = value + if dict_value.is_empty(): return "{ }" var as_rows := var_to_str(value).split("\n") for index in range( 1, as_rows.size()-1): @@ -22,15 +26,22 @@ static func format_dict(value :Dictionary) -> String: static func input_event_as_text(event :InputEvent) -> String: var text := "" if event is InputEventKey: + var key_event := event as InputEventKey text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [ - event.as_text(), event.pressed, event.keycode, event.physical_keycode] + event.as_text(), key_event.pressed, key_event.keycode, key_event.physical_keycode] else: text += event.as_text() if event is InputEventMouse: - text += ", global_position %s" % event.global_position + var mouse_event := event as InputEventMouse + text += ", global_position %s" % mouse_event.global_position if event is InputEventWithModifiers: + var mouse_event := event as InputEventWithModifiers text += ", shift=%s, alt=%s, control=%s, meta=%s, command=%s" % [ - event.shift_pressed, event.alt_pressed, event.ctrl_pressed, event.meta_pressed, event.command_or_control_autoremap] + mouse_event.shift_pressed, + mouse_event.alt_pressed, + mouse_event.ctrl_pressed, + mouse_event.meta_pressed, + mouse_event.command_or_control_autoremap] return text @@ -51,9 +62,11 @@ static func colored_array_div(characters :PackedByteArray) -> String: match character: GdDiffTool.DIV_ADD: index += 1 + @warning_ignore("return_value_discarded") additional_chars.append(characters[index]) GdDiffTool.DIV_SUB: index += 1 + @warning_ignore("return_value_discarded") missing_chars.append(characters[index]) _: if not missing_chars.is_empty(): @@ -62,6 +75,7 @@ static func colored_array_div(characters :PackedByteArray) -> String: if not additional_chars.is_empty(): result.append_array(format_chars(additional_chars, ADD_COLOR)) additional_chars = PackedByteArray() + @warning_ignore("return_value_discarded") result.append(character) index += 1 @@ -95,7 +109,7 @@ static func _nerror(number :Variant) -> String: static func _colored_value(value :Variant) -> String: match typeof(value): TYPE_STRING, TYPE_STRING_NAME: - return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(value)] + return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(str(value))] TYPE_INT: return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value] TYPE_FLOAT: @@ -106,10 +120,12 @@ static func _colored_value(value :Variant) -> String: if value == null: return "'[color=%s][/color]'" % [VALUE_COLOR] if value is InputEvent: - return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(value)] - if value.has_method("_to_string"): + var ie: InputEvent = value + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(ie)] + var obj_value: Object = value + if obj_value.has_method("_to_string"): return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)] - return "[color=%s]<%s>[/color]" % [VALUE_COLOR, value.get_class()] + return "[color=%s]<%s>[/color]" % [VALUE_COLOR, obj_value.get_class()] TYPE_DICTIONARY: return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)] _: @@ -336,6 +352,7 @@ static func error_ends_with(current :Variant, expected :Variant) -> String: static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String: + @warning_ignore("unsafe_method_access") var current_length :Variant = current.length() if current != null else null match compare_operator: Comparator.EQUAL: @@ -361,48 +378,50 @@ static func error_has_length(current :Variant, expected: int, compare_operator : # - ArrayAssert specific messgaes --------------------------------------------------- -static func error_arr_contains(current :Variant, expected :Array, not_expect :Array, not_found :Array, by_reference :bool) -> String: +static func error_arr_contains(current: Variant, expected: Variant, not_expect: Variant, not_found: Variant, by_reference: bool) -> String: var failure_message := "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:" var error := "%s\n %s\n do contains (in any order)\n %s" % [ _error(failure_message), _colored_value(current), _colored_value(expected)] - if not not_expect.is_empty(): + if not is_empty(not_expect): error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) - if not not_found.is_empty(): - var prefix := "but" if not_expect.is_empty() else "and" + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] return error static func error_arr_contains_exactly( - current :Variant, - expected :Variant, - not_expect :Variant, - not_found :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String: + current: Variant, + expected: Variant, + not_expect: Variant, + not_found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String: var failure_message := ( "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting contains SAME exactly elements:" ) - if not_expect.is_empty() and not_found.is_empty(): - var diff := _find_first_diff(current, expected) + if is_empty(not_expect) and is_empty(not_found): + var arr_current: Array = current + var arr_expected: Array = expected + var diff := _find_first_diff(arr_current, arr_expected) return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [ _error(failure_message), _colored_value(current), _colored_value(expected), diff] var error := "%s\n %s\n do contains (in same order)\n %s" % [ _error(failure_message), _colored_value(current), _colored_value(expected)] - if not not_expect.is_empty(): + if not is_empty(not_expect): error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) - if not not_found.is_empty(): - var prefix := "but" if not_expect.is_empty() else "and" + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] return error static func error_arr_contains_exactly_in_any_order( - current :Variant, - expected :Array, - not_expect :Array, - not_found :Array, - compare_mode :GdObjects.COMPARE_MODE) -> String: + current: Variant, + expected: Variant, + not_expect: Variant, + not_found: Variant, + compare_mode: GdObjects.COMPARE_MODE) -> String: var failure_message := ( "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST @@ -410,19 +429,19 @@ static func error_arr_contains_exactly_in_any_order( ) var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [ _error(failure_message), _colored_value(current), _colored_value(expected)] - if not not_expect.is_empty(): + if not is_empty(not_expect): error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect) - if not not_found.is_empty(): - var prefix := "but" if not_expect.is_empty() else "and" + if not is_empty(not_found): + var prefix := "but" if is_empty(not_expect) else "and" error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)] return error -static func error_arr_not_contains(current :Array, expected :Array, found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String: +static func error_arr_not_contains(current: Variant, expected: Variant, found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String: var failure_message := "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:" var error := "%s\n %s\n do not contains\n %s" % [ _error(failure_message), _colored_value(current), _colored_value(expected)] - if not found.is_empty(): + if not is_empty(found): error += "\n but found elements:\n %s" % _colored_value(found) return error @@ -542,20 +561,25 @@ static func result_message(result :GdUnitResult) -> String: # - Spy|Mock specific errors ---------------------------------------------------- static func error_no_more_interactions(summary :Dictionary) -> String: var interactions := PackedStringArray() - for args :Variant in summary.keys(): + for args :Array in summary.keys(): var times :int = summary[args] + @warning_ignore("return_value_discarded") interactions.append(_format_arguments(args, times)) return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)] -static func error_validate_interactions(current_interactions :Dictionary, expected_interactions :Dictionary) -> String: - var interactions := PackedStringArray() - for args :Variant in current_interactions.keys(): - var times :int = current_interactions[args] - interactions.append(_format_arguments(args, times)) - var expected_interaction := _format_arguments(expected_interactions.keys()[0], expected_interactions.values()[0]) +static func error_validate_interactions(current_interactions: Dictionary, expected_interactions: Dictionary) -> String: + var collected_interactions := PackedStringArray() + for args: Array in current_interactions.keys(): + var times: int = current_interactions[args] + @warning_ignore("return_value_discarded") + collected_interactions.append(_format_arguments(args, times)) + + var arguments: Array = expected_interactions.keys()[0] + var interactions: int = expected_interactions.values()[0] + var expected_interaction := _format_arguments(arguments, interactions) return "%s\n%s\n%s\n%s" % [ - _error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(interactions)] + _error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(collected_interactions)] static func _format_arguments(args :Array, times :int) -> String: @@ -569,13 +593,15 @@ static func _format_arguments(args :Array, times :int) -> String: static func _to_typed_args(args :Array) -> PackedStringArray: var typed := PackedStringArray() for arg :Variant in args: + @warning_ignore("return_value_discarded") typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg))) return typed static func _format_arg(arg :Variant) -> String: if arg is InputEvent: - return input_event_as_text(arg) + var ie: InputEvent = arg + return input_event_as_text(ie) return str(arg) @@ -589,6 +615,7 @@ static func _find_first_diff(left :Array, right :Array) -> String: static func error_has_size(current :Variant, expected: int) -> String: + @warning_ignore("unsafe_method_access") var current_size :Variant = null if current == null else current.size() return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)] @@ -623,3 +650,8 @@ static func build_failure_message(failure :String, additional_failure_message: S %s [color=LIME_GREEN][b]Additional info:[/b][/color] %s""".dedent().trim_prefix("\n") % [message, additional_failure_message] + + +static func is_empty(value: Variant) -> bool: + var arry_value: Array = value + return arry_value != null and arry_value.is_empty() diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd index 1ac9e04b..06b72edb 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -51,5 +51,4 @@ static func current_failure() -> String: static func send_report(report :GdUnitReport) -> void: - var execution_context_id := GdUnitThreadManager.get_current_context().get_execution_context_id() - GdUnitSignals.instance().gdunit_report.emit(execution_context_id, report) + GdUnitThreadManager.get_current_context().get_execution_context().add_report(report) diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd index c2c3cefa..748e20e6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -1,21 +1,24 @@ +class_name GdUnitArrayAssertImpl extends GdUnitArrayAssert -var _base :GdUnitAssert -var _current_value_provider :ValueProvider +var _base: GdUnitAssertImpl +var _current_value_provider: ValueProvider +var _type_check: bool -func _init(current :Variant) -> void: +func _init(current: Variant, type_check := true) -> void: + _type_check = type_check _current_value_provider = DefaultValueProvider.new(current) - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _validate_value_type(current): + @warning_ignore("return_value_discarded") report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) -func _notification(event :int) -> void: +func _notification(event: int) -> void: if event == NOTIFICATION_PREDELETE: if _base != null: _base.notification(event) @@ -27,7 +30,7 @@ func report_success() -> GdUnitArrayAssert: return self -func report_error(error :String) -> GdUnitArrayAssert: +func report_error(error: String) -> GdUnitArrayAssert: _base.report_error(error) return self @@ -36,17 +39,17 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitArrayAssert: +func override_failure_message(message: String) -> GdUnitArrayAssert: _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitArrayAssert: +func append_failure_message(message: String) -> GdUnitArrayAssert: _base.append_failure_message(message) return self -func _validate_value_type(value :Variant) -> bool: +func _validate_value_type(value: Variant) -> bool: return value == null or GdArrayTools.is_array_type(value) @@ -54,15 +57,23 @@ func get_current_value() -> Variant: return _current_value_provider.get_value() -func max_length(left :Variant, right :Variant) -> int: +func max_length(left: Variant, right: Variant) -> int: var ls := str(left).length() var rs := str(right).length() return rs if ls < rs else ls -func _array_equals_div(current :Array, expected :Array, case_sensitive :bool = false) -> Array: - var current_value := PackedStringArray(current) - var expected_value := PackedStringArray(expected) +# gdlint: disable=function-name +func _toPackedStringArray(value: Variant) -> PackedStringArray: + if GdArrayTools.is_array_type(value): + @warning_ignore("unsafe_cast") + return PackedStringArray(value as Array) + return PackedStringArray([str(value)]) + + +func _array_equals_div(current: Variant, expected: Variant, case_sensitive: bool = false) -> Array[Array]: + var current_value := _toPackedStringArray(current) + var expected_value := _toPackedStringArray(expected) var index_report := Array() for index in current_value.size(): var c := current_value[index] @@ -72,25 +83,25 @@ func _array_equals_div(current :Array, expected :Array, case_sensitive :bool = f var length := max_length(c, e) current_value[index] = GdAssertMessages.format_invalid(c.lpad(length)) expected_value[index] = e.lpad(length) - index_report.push_back({"index" : index, "current" :c, "expected": e}) + index_report.push_back({"index": index, "current": c, "expected": e}) else: current_value[index] = GdAssertMessages.format_invalid(c) - index_report.push_back({"index" : index, "current" :c, "expected": ""}) + index_report.push_back({"index": index, "current": c, "expected": ""}) - for index in range(current.size(), expected_value.size()): + for index in range(current_value.size(), expected_value.size()): var value := expected_value[index] expected_value[index] = GdAssertMessages.format_invalid(value) - index_report.push_back({"index" : index, "current" : "", "expected": value}) + index_report.push_back({"index": index, "current": "", "expected": value}) return [current_value, expected_value, index_report] -func _array_div(compare_mode :GdObjects.COMPARE_MODE, left :Array[Variant], right :Array[Variant], _same_order := false) -> Array[Variant]: +func _array_div(compare_mode: GdObjects.COMPARE_MODE, left: Array[Variant], right: Array[Variant], _same_order := false) -> Array[Variant]: var not_expect := left.duplicate(true) var not_found := right.duplicate(true) for index_c in left.size(): - var c :Variant = left[index_c] + var c: Variant = left[index_c] for index_e in right.size(): - var e :Variant = right[index_e] + var e: Variant = right[index_e] if GdObjects.equals(c, e, false, compare_mode): GdArrayTools.erase_value(not_expect, e) GdArrayTools.erase_value(not_found, c) @@ -98,241 +109,262 @@ func _array_div(compare_mode :GdObjects.COMPARE_MODE, left :Array[Variant], righ return [not_expect, not_found] -func _contains(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: +func _contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null: return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], expected, by_reference)) - var diffs := _array_div(compare_mode, current_value, expected) + @warning_ignore("unsafe_cast") + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant]) #var not_expect := diffs[0] as Array - var not_found := diffs[1] as Array + var not_found: Array = diffs[1] if not not_found.is_empty(): return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], not_found, by_reference)) return report_success() -func _contains_exactly(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: +func _contains_exactly(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null: - return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], expected, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected, [], expected, compare_mode)) # has same content in same order - if GdObjects.equals(Array(current_value), Array(expected), false, compare_mode): + if _is_equal(current_value, expected, false, compare_mode): return report_success() # check has same elements but in different order - if GdObjects.equals_sorted(Array(current_value), Array(expected), false, compare_mode): + if _is_equals_sorted(current_value, expected, false, compare_mode): return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], [], compare_mode)) # find the difference - var diffs := _array_div(compare_mode, current_value, expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) - var not_expect := diffs[0] as Array[Variant] - var not_found := diffs[1] as Array[Variant] + @warning_ignore("unsafe_cast") + var diffs := _array_div(compare_mode, + current_value as Array[Variant], + expected as Array[Variant], + GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) + var not_expect: Array[Variant] = diffs[0] + var not_found: Array[Variant] = diffs[1] return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, not_expect, not_found, compare_mode)) -func _contains_exactly_in_any_order(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: +func _contains_exactly_in_any_order(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null: return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) # find the difference - var diffs := _array_div(compare_mode, current_value, expected, false) - var not_expect := diffs[0] as Array - var not_found := diffs[1] as Array + @warning_ignore("unsafe_cast") + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant], false) + var not_expect: Array[Variant] = diffs[0] + var not_found: Array[Variant] = diffs[1] if not_expect.is_empty() and not_found.is_empty(): return report_success() return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, not_expect, not_found, compare_mode)) -func _not_contains(expected :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: +func _not_contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null: return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) - var diffs := _array_div(compare_mode, current_value, expected) - var found := diffs[0] as Array - if found.size() == current_value.size(): + @warning_ignore("unsafe_cast") + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant]) + var found: Array[Variant] = diffs[0] + @warning_ignore("unsafe_cast") + if found.size() == (current_value as Array).size(): return report_success() - var diffs2 := _array_div(compare_mode, expected, diffs[1]) + @warning_ignore("unsafe_cast") + var diffs2 := _array_div(compare_mode, expected as Array[Variant], diffs[1] as Array[Variant]) return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected, diffs2[0], compare_mode)) func is_null() -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self # Verifies that the current String is equal to the given one. -func is_equal(expected :Variant) -> GdUnitArrayAssert: - if not _validate_value_type(expected): +func is_equal(expected: Variant) -> GdUnitArrayAssert: + if _type_check and not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null and expected != null: return report_error(GdAssertMessages.error_equal(null, expected)) - if not GdObjects.equals(current_value, expected): + if not _is_equal(current_value, expected): var diff := _array_equals_div(current_value, expected) var expected_as_list := GdArrayTools.as_string(diff[0], false) var current_as_list := GdArrayTools.as_string(diff[1], false) - var index_report :Variant = diff[2] + var index_report: Array = diff[2] return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report)) return report_success() # Verifies that the current Array is equal to the given one, ignoring case considerations. -func is_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert: - if not _validate_value_type(expected): +func is_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert: + if _type_check and not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() + var current_value: Variant = get_current_value() if current_value == null and expected != null: - return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected))) - if not GdObjects.equals(current_value, expected, true): - var diff := _array_equals_div(current_value, expected, true) + @warning_ignore("unsafe_cast") + return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected as Array))) + if not _is_equal(current_value, expected, true): + @warning_ignore("unsafe_cast") + var diff := _array_equals_div(current_value as Array[Variant], expected as Array[Variant], true) var expected_as_list := GdArrayTools.as_string(diff[0]) var current_as_list := GdArrayTools.as_string(diff[1]) - var index_report :Variant = diff[2] + var index_report: Array = diff[2] return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report)) return report_success() -func is_not_equal(expected :Variant) -> GdUnitArrayAssert: +func is_not_equal(expected: Variant) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() - if GdObjects.equals(current_value, expected): + var current_value: Variant = get_current_value() + if _is_equal(current_value, expected): return report_error(GdAssertMessages.error_not_equal(current_value, expected)) return report_success() -func is_not_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert: +func is_not_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current_value :Variant = get_current_value() - if GdObjects.equals(current_value, expected, true): - var c := GdArrayTools.as_string(current_value) - var e := GdArrayTools.as_string(expected) + var current_value: Variant = get_current_value() + if _is_equal(current_value, expected, true): + @warning_ignore("unsafe_cast") + var c := GdArrayTools.as_string(current_value as Array) + @warning_ignore("unsafe_cast") + var e := GdArrayTools.as_string(expected as Array) return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e)) return report_success() func is_empty() -> GdUnitArrayAssert: - var current_value :Variant = get_current_value() - if current_value == null or current_value.size() > 0: + var current_value: Variant = get_current_value() + @warning_ignore("unsafe_cast") + if current_value == null or (current_value as Array).size() > 0: return report_error(GdAssertMessages.error_is_empty(current_value)) return report_success() func is_not_empty() -> GdUnitArrayAssert: - var current_value :Variant = get_current_value() - if current_value != null and current_value.size() == 0: + var current_value: Variant = get_current_value() + @warning_ignore("unsafe_cast") + if current_value != null and (current_value as Array).size() == 0: return report_error(GdAssertMessages.error_is_not_empty()) return report_success() @warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_same(expected :Variant) -> GdUnitArrayAssert: +func is_same(expected: Variant) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current :Variant = get_current_value() + var current: Variant = get_current_value() if not is_same(current, expected): + @warning_ignore("return_value_discarded") report_error(GdAssertMessages.error_is_same(current, expected)) return self -func is_not_same(expected :Variant) -> GdUnitArrayAssert: +func is_not_same(expected: Variant) -> GdUnitArrayAssert: if not _validate_value_type(expected): return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) - var current :Variant = get_current_value() + var current: Variant = get_current_value() if is_same(current, expected): + @warning_ignore("return_value_discarded") report_error(GdAssertMessages.error_not_same(current, expected)) return self func has_size(expected: int) -> GdUnitArrayAssert: - var current_value :Variant= get_current_value() - if current_value == null or current_value.size() != expected: + var current_value: Variant = get_current_value() + @warning_ignore("unsafe_cast") + if current_value == null or (current_value as Array).size() != expected: return report_error(GdAssertMessages.error_has_size(current_value, expected)) return report_success() -func contains(expected :Variant) -> GdUnitArrayAssert: +func contains(expected: Variant) -> GdUnitArrayAssert: return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_exactly(expected :Variant) -> GdUnitArrayAssert: +func contains_exactly(expected: Variant) -> GdUnitArrayAssert: return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert: +func contains_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert: return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_same(expected :Variant) -> GdUnitArrayAssert: +func contains_same(expected: Variant) -> GdUnitArrayAssert: return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func contains_same_exactly(expected :Variant) -> GdUnitArrayAssert: +func contains_same_exactly(expected: Variant) -> GdUnitArrayAssert: return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func contains_same_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert: +func contains_same_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert: return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func not_contains(expected :Variant) -> GdUnitArrayAssert: +func not_contains(expected: Variant) -> GdUnitArrayAssert: return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func not_contains_same(expected :Variant) -> GdUnitArrayAssert: +func not_contains_same(expected: Variant) -> GdUnitArrayAssert: return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func is_instanceof(expected :Variant) -> GdUnitAssert: +func is_instanceof(expected: Variant) -> GdUnitAssert: + @warning_ignore("unsafe_method_access") _base.is_instanceof(expected) return self -func extract(func_name :String, args := Array()) -> GdUnitArrayAssert: +func extract(func_name: String, args := Array()) -> GdUnitArrayAssert: var extracted_elements := Array() - var extractor :GdUnitValueExtractor = ResourceLoader.load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd", - "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(func_name, args) - var current :Variant = get_current_value() + + var extractor := GdUnitFuncValueExtractor.new(func_name, args) + var current: Variant = get_current_value() if current == null: _current_value_provider = DefaultValueProvider.new(null) else: - for element :Variant in current: + for element: Variant in current: extracted_elements.append(extractor.extract_value(element)) _current_value_provider = DefaultValueProvider.new(extracted_elements) return self func extractv( - extr0 :GdUnitValueExtractor, - extr1 :GdUnitValueExtractor = null, - extr2 :GdUnitValueExtractor = null, - extr3 :GdUnitValueExtractor = null, - extr4 :GdUnitValueExtractor = null, - extr5 :GdUnitValueExtractor = null, - extr6 :GdUnitValueExtractor = null, - extr7 :GdUnitValueExtractor = null, - extr8 :GdUnitValueExtractor = null, - extr9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert: - var extractors :Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null) + extr0: GdUnitValueExtractor, + extr1: GdUnitValueExtractor = null, + extr2: GdUnitValueExtractor = null, + extr3: GdUnitValueExtractor = null, + extr4: GdUnitValueExtractor = null, + extr5: GdUnitValueExtractor = null, + extr6: GdUnitValueExtractor = null, + extr7: GdUnitValueExtractor = null, + extr8: GdUnitValueExtractor = null, + extr9: GdUnitValueExtractor = null) -> GdUnitArrayAssert: + var extractors: Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null) var extracted_elements := Array() - var current :Variant = get_current_value() + var current: Variant = get_current_value() if current == null: _current_value_provider = DefaultValueProvider.new(null) else: for element: Variant in current: - var ev :Array[Variant] = [ + var ev: Array[Variant] = [ GdUnitTuple.NO_ARG, GdUnitTuple.NO_ARG, GdUnitTuple.NO_ARG, @@ -344,12 +376,44 @@ func extractv( GdUnitTuple.NO_ARG, GdUnitTuple.NO_ARG ] - for index :int in extractors.size(): - var extractor :GdUnitValueExtractor = extractors[index] + @warning_ignore("unsafe_cast") + for index: int in (extractors as Array).size(): + var extractor: GdUnitValueExtractor = extractors[index] ev[index] = extractor.extract_value(element) - if extractors.size() > 1: + @warning_ignore("unsafe_cast") + if (extractors as Array).size() > 1: extracted_elements.append(GdUnitTuple.new(ev[0], ev[1], ev[2], ev[3], ev[4], ev[5], ev[6], ev[7], ev[8], ev[9])) else: extracted_elements.append(ev[0]) _current_value_provider = DefaultValueProvider.new(extracted_elements) return self + + +@warning_ignore("incompatible_ternary") +func _is_equal( + left: Variant, + right: Variant, + case_sensitive := false, + compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + + @warning_ignore("unsafe_cast") + return GdObjects.equals( + (left as Array) if GdArrayTools.is_array_type(left) else left, + (right as Array) if GdArrayTools.is_array_type(right) else right, + case_sensitive, + compare_mode + ) + + +func _is_equals_sorted( + left: Variant, + right: Variant, + case_sensitive := false, + compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + + @warning_ignore("unsafe_cast") + return GdObjects.equals_sorted( + left as Array, + right as Array, + case_sensitive, + compare_mode) diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd index 583461db..c08bc132 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -1,3 +1,4 @@ +class_name GdUnitAssertImpl extends GdUnitAssert @@ -15,8 +16,6 @@ func _init(current :Variant) -> void: - - func failure_message() -> String: return _current_failure_message @@ -39,7 +38,7 @@ func report_error(failure :String, failure_line_number: int = -1) -> GdUnitAsser return self -func test_fail() -> GdUnitAssert: +func do_fail() -> GdUnitAssert: return report_error(GdAssertMessages.error_not_implemented()) diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd index a1b12caf..897d63f4 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -3,6 +3,7 @@ class_name GdUnitAssertions extends RefCounted +@warning_ignore("return_value_discarded") func _init() -> void: # preload all gdunit assertions to speedup testsuite loading time # gdlint:disable=private-method-call diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd index b781e28d..5daebcec 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitBoolAssert -var _base: GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not GdUnitAssertions.validate_value_type(current, TYPE_BOOL): + @warning_ignore("return_value_discarded") report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -38,33 +38,39 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self # Verifies that the current value is null. func is_null() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self # Verifies that the current value is not null. func is_not_null() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self -func is_equal(expected :Variant) -> GdUnitBoolAssert: +func is_equal(expected: Variant) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitBoolAssert: +func is_not_equal(expected: Variant) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd index a4dae18a..17eba6ab 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitDictionaryAssert -var _base :GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not GdUnitAssertions.validate_value_type(current, TYPE_DICTIONARY): + @warning_ignore("return_value_discarded") report_error("GdUnitDictionaryAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -34,11 +34,13 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -48,11 +50,13 @@ func current_value() -> Variant: func is_null() -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self @@ -101,14 +105,16 @@ func is_not_same(expected :Variant) -> GdUnitDictionaryAssert: func is_empty() -> GdUnitDictionaryAssert: var current :Variant = current_value() - if current == null or not current.is_empty(): + @warning_ignore("unsafe_cast") + if current == null or not (current as Dictionary).is_empty(): return report_error(GdAssertMessages.error_is_empty(current)) return report_success() func is_not_empty() -> GdUnitDictionaryAssert: var current :Variant = current_value() - if current == null or current.is_empty(): + @warning_ignore("unsafe_cast") + if current == null or (current as Dictionary).is_empty(): return report_error(GdAssertMessages.error_is_not_empty()) return report_success() @@ -117,7 +123,8 @@ func has_size(expected: int) -> GdUnitDictionaryAssert: var current :Variant = current_value() if current == null: return report_error(GdAssertMessages.error_is_not_null()) - if current.size() != expected: + @warning_ignore("unsafe_cast") + if (current as Dictionary).size() != expected: return report_error(GdAssertMessages.error_has_size(current, expected)) return report_success() @@ -127,9 +134,11 @@ func _contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> Gd if current == null: return report_error(GdAssertMessages.error_is_not_null()) # find expected keys - var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode)) + @warning_ignore("unsafe_cast") + var keys_not_found :Array = expected.filter(_filter_by_key.bind((current as Dictionary).keys(), compare_mode)) if not keys_not_found.is_empty(): - return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode)) + @warning_ignore("unsafe_cast") + return report_error(GdAssertMessages.error_contains_keys((current as Dictionary).keys() as Array, expected, keys_not_found, compare_mode)) return report_success() @@ -138,11 +147,12 @@ func _contains_key_value(key :Variant, value :Variant, compare_mode :GdObjects.C var expected := [key] if current == null: return report_error(GdAssertMessages.error_is_not_null()) - var keys_not_found :Array = expected.filter(_filter_by_key.bind(current.keys(), compare_mode)) + var dict_current: Dictionary = current + var keys_not_found :Array = expected.filter(_filter_by_key.bind(dict_current.keys(), compare_mode)) if not keys_not_found.is_empty(): - return report_error(GdAssertMessages.error_contains_keys(current.keys(), expected, keys_not_found, compare_mode)) - if not GdObjects.equals(current[key], value, false, compare_mode): - return report_error(GdAssertMessages.error_contains_key_value(key, value, current[key], compare_mode)) + return report_error(GdAssertMessages.error_contains_keys(dict_current.keys() as Array, expected, keys_not_found, compare_mode)) + if not GdObjects.equals(dict_current[key], value, false, compare_mode): + return report_error(GdAssertMessages.error_contains_key_value(key, value, dict_current[key], compare_mode)) return report_success() @@ -150,9 +160,10 @@ func _not_contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) - var current :Variant = current_value() if current == null: return report_error(GdAssertMessages.error_is_not_null()) - var keys_found :Array = current.keys().filter(_filter_by_key.bind(expected, compare_mode, true)) + var dict_current: Dictionary = current + var keys_found :Array = dict_current.keys().filter(_filter_by_key.bind(expected, compare_mode, true)) if not keys_found.is_empty(): - return report_error(GdAssertMessages.error_not_contains_keys(current.keys(), expected, keys_found, compare_mode)) + return report_error(GdAssertMessages.error_not_contains_keys(dict_current.keys() as Array, expected, keys_found, compare_mode)) return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd index b25a675e..845d5fa9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -15,6 +15,7 @@ func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAs _set_do_expect_fail(true) var thread_context := GdUnitThreadManager.get_current_context() thread_context.set_assert(null) + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed) # execute the given assertion as callable if do_await: @@ -28,11 +29,13 @@ func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAs _is_failed = true _failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'" return self + @warning_ignore("unsafe_method_access") _failure_message = current_assert.failure_message() return self func execute(assertion :Callable) -> GdUnitFailureAssert: + @warning_ignore("return_value_discarded") execute_and_await(assertion, false) return self @@ -42,12 +45,12 @@ func _on_test_failed(value :bool) -> void: @warning_ignore("unused_parameter") -func is_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert: +func is_equal(_expected: Variant) -> GdUnitFailureAssert: return _report_error("Not implemented") @warning_ignore("unused_parameter") -func is_not_equal(_expected :GdUnitAssert) -> GdUnitFailureAssert: +func is_not_equal(_expected: Variant) -> GdUnitFailureAssert: return _report_error("Not implemented") @@ -79,13 +82,14 @@ func has_line(expected :int) -> GdUnitFailureAssert: func has_message(expected :String) -> GdUnitFailureAssert: + @warning_ignore("return_value_discarded") is_failed() var expected_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(expected)) var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) if current_error != expected_error: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages.colored_array_div(diffs[1]) - _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) return self @@ -95,7 +99,7 @@ func contains_message(expected :String) -> GdUnitFailureAssert: if not current_error.contains(expected_error): var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages.colored_array_div(diffs[1]) - _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) return self @@ -105,7 +109,7 @@ func starts_with_message(expected :String) -> GdUnitFailureAssert: if current_error.find(expected_error) != 0: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages.colored_array_div(diffs[1]) - _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) + return _report_error(GdAssertMessages.error_not_same_error(current, expected_error)) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd index 63683fce..f98bc933 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -2,15 +2,15 @@ extends GdUnitFileAssert const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -var _base: GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not GdUnitAssertions.validate_value_type(current, TYPE_STRING): + @warning_ignore("return_value_discarded") report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -22,7 +22,7 @@ func _notification(event :int) -> void: func current_value() -> String: - return _base.current_value() as String + return _base.current_value() func report_success() -> GdUnitFileAssert: @@ -40,21 +40,25 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_equal(expected :Variant) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self func is_not_equal(expected :Variant) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self @@ -84,17 +88,14 @@ func is_script() -> GdUnitFileAssert: return report_success() -func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: +func contains_exactly(expected_rows: Array) -> GdUnitFileAssert: var current := current_value() if FileAccess.open(current, FileAccess.READ) == null: return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()]) - var script := load(current) + var script: GDScript = load(current) if script is GDScript: - var instance :Variant = script.new() - var source_code := GdScriptParser.to_unix_format(instance.get_script().source_code) - GdUnitTools.free_instance(instance) + var source_code := GdScriptParser.to_unix_format(script.source_code) var rows := Array(source_code.split("\n")) - ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(rows).contains_exactly(expected_rows) + GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd index 27463fdb..05d05b84 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitFloatAssert -var _base: GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not GdUnitAssertions.validate_value_type(current, TYPE_FLOAT): + @warning_ignore("return_value_discarded") report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -38,31 +38,37 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_null() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self -func is_equal(expected :float) -> GdUnitFloatAssert: +func is_equal(expected :Variant) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :float) -> GdUnitFloatAssert: +func is_not_equal(expected :Variant) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self @@ -116,14 +122,16 @@ func is_not_negative() -> GdUnitFloatAssert: func is_zero() -> GdUnitFloatAssert: var current :Variant = current_value() - if current == null or not is_equal_approx(0.00000000, current): + @warning_ignore("unsafe_cast") + if current == null or not is_equal_approx(0.00000000, current as float): return report_error(GdAssertMessages.error_is_zero(current)) return report_success() func is_not_zero() -> GdUnitFloatAssert: var current :Variant = current_value() - if current == null or is_equal_approx(0.00000000, current): + @warning_ignore("unsafe_cast") + if current == null or is_equal_approx(0.00000000, current as float): return report_error(GdAssertMessages.error_is_not_zero()) return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd index c3992c76..b4a8038e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -22,6 +22,7 @@ func _init(instance :Object, func_name :String, args := Array()) -> void: GdUnitThreadManager.get_current_context().set_assert(self) # verify at first the function name exists if not instance.has_method(func_name): + @warning_ignore("return_value_discarded") report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance]) _interrupted = true else: @@ -33,7 +34,7 @@ func _notification(_what :int) -> void: _current_value_provider.dispose() _current_value_provider = null if is_instance_valid(_sleep_timer): - Engine.get_main_loop().root.remove_child(_sleep_timer) + (Engine.get_main_loop() as SceneTree).root.remove_child(_sleep_timer) _sleep_timer.stop() _sleep_timer.free() _sleep_timer = null @@ -54,10 +55,6 @@ func failure_message() -> String: return _current_failure_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 @@ -124,8 +121,10 @@ func _validate_callback(predicate :Callable, expected :Variant = null) -> void: var time_scale := Engine.get_time_scale() var timer := Timer.new() timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id()) - Engine.get_main_loop().root.add_child(timer) + var scene_tree := Engine.get_main_loop() as SceneTree + scene_tree.root.add_child(timer) timer.add_to_group("GdUnitTimers") + @warning_ignore("return_value_discarded") timer.timeout.connect(func do_interrupt() -> void: _interrupted = true , CONNECT_DEFERRED) @@ -133,7 +132,7 @@ func _validate_callback(predicate :Callable, expected :Variant = null) -> void: timer.start((_timeout/1000.0)*time_scale) _sleep_timer = Timer.new() _sleep_timer.set_name("gdunit_funcassert_sleep_timer_%d" % _sleep_timer.get_instance_id() ) - Engine.get_main_loop().root.add_child(_sleep_timer) + scene_tree.root.add_child(_sleep_timer) while true: var current :Variant = await next_current_value() @@ -145,13 +144,20 @@ func _validate_callback(predicate :Callable, expected :Variant = null) -> void: await _sleep_timer.timeout _sleep_timer.stop() - await Engine.get_main_loop().process_frame + await scene_tree.process_frame if _interrupted: # https://github.com/godotengine/godot/issues/73052 #var predicate_name = predicate.get_method() var predicate_name :String = str(predicate).split('::')[1] - report_error(GdAssertMessages.error_interrupted(predicate_name.strip_edges().trim_prefix("cb_"), expected, LocalTime.elapsed(_timeout))) + @warning_ignore("return_value_discarded") + report_error(GdAssertMessages.error_interrupted( + predicate_name.strip_edges().trim_prefix("cb_"), + expected, + LocalTime.elapsed(_timeout) + ) + ) else: + @warning_ignore("return_value_discarded") report_success() _sleep_timer.free() timer.free() diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd index f08da5bd..adf2a2eb 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -17,6 +17,7 @@ func _init(callable :Callable) -> void: func _execute() -> Array[ErrorLogEntry]: # execute the given code and monitor for runtime errors if _callable == null or not _callable.is_valid(): + @warning_ignore("return_value_discarded") _report_error("Invalid Callable '%s'" % _callable) else: await _callable.call() diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd index bfcc0317..1527cac5 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitIntAssert -var _base: GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not GdUnitAssertions.validate_value_type(current, TYPE_INT): + @warning_ignore("return_value_discarded") report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -38,31 +38,37 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_null() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self -func is_equal(expected :int) -> GdUnitIntAssert: +func is_equal(expected :Variant) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :int) -> GdUnitIntAssert: +func is_not_equal(expected :Variant) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd index 4ea12d91..ce78a186 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -1,11 +1,10 @@ extends GdUnitObjectAssert -var _base :GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if (current != null @@ -13,6 +12,7 @@ func _init(current :Variant) -> void: or GdUnitAssertions.validate_value_type(current, TYPE_INT) or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT) or GdUnitAssertions.validate_value_type(current, TYPE_STRING))): + @warning_ignore("return_value_discarded") report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -42,31 +42,37 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_equal(expected :Variant) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self func is_not_equal(expected :Variant) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self func is_null() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self @@ -75,19 +81,15 @@ func is_not_null() -> GdUnitObjectAssert: func is_same(expected :Variant) -> GdUnitObjectAssert: var current :Variant = current_value() if not is_same(current, expected): - report_error(GdAssertMessages.error_is_same(current, expected)) - return self - report_success() - return self + return report_error(GdAssertMessages.error_is_same(current, expected)) + return report_success() func is_not_same(expected :Variant) -> GdUnitObjectAssert: var current :Variant = current_value() if is_same(current, expected): - report_error(GdAssertMessages.error_not_same(current, expected)) - return self - report_success() - return self + return report_error(GdAssertMessages.error_not_same(current, expected)) + return report_success() func is_instanceof(type :Object) -> GdUnitObjectAssert: @@ -95,10 +97,8 @@ func is_instanceof(type :Object) -> GdUnitObjectAssert: if current == null or not is_instance_of(current, type): var result_expected: = GdObjects.extract_class_name(type) var result_current: = GdObjects.extract_class_name(current) - report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected)) - return self - report_success() - return self + return report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected)) + return report_success() func is_not_instanceof(type :Variant) -> GdUnitObjectAssert: @@ -106,9 +106,8 @@ func is_not_instanceof(type :Variant) -> GdUnitObjectAssert: if is_instance_of(current, type): var result: = GdObjects.extract_class_name(type) if result.is_success(): - report_error("Expected not be a instance of <%s>" % result.value()) - else: - push_error("Internal ERROR: %s" % result.error_message()) + return report_error("Expected not be a instance of <%s>" % str(result.value())) + + push_error("Internal ERROR: %s" % result.error_message()) return self - report_success() - return self + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index 6e734e0b..8b6c7f26 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitResultAssert -var _base :GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not validate_value_type(current): + @warning_ignore("return_value_discarded") report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -24,7 +24,7 @@ func validate_value_type(value :Variant) -> bool: func current_value() -> GdUnitResult: - return _base.current_value() as GdUnitResult + return _base.current_value() func report_success() -> GdUnitResultAssert: @@ -42,21 +42,25 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_null() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self @@ -64,63 +68,50 @@ func is_not_null() -> GdUnitResultAssert: func is_empty() -> GdUnitResultAssert: var result := current_value() if result == null or not result.is_empty(): - report_error(GdAssertMessages.error_result_is_empty(result)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_is_empty(result)) + return report_success() func is_success() -> GdUnitResultAssert: var result := current_value() if result == null or not result.is_success(): - report_error(GdAssertMessages.error_result_is_success(result)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_is_success(result)) + return report_success() func is_warning() -> GdUnitResultAssert: var result := current_value() if result == null or not result.is_warn(): - report_error(GdAssertMessages.error_result_is_warning(result)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_is_warning(result)) + return report_success() func is_error() -> GdUnitResultAssert: var result := current_value() if result == null or not result.is_error(): - report_error(GdAssertMessages.error_result_is_error(result)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_is_error(result)) + return report_success() func contains_message(expected :String) -> GdUnitResultAssert: var result := current_value() if result == null: - report_error(GdAssertMessages.error_result_has_message("", expected)) - return self + return report_error(GdAssertMessages.error_result_has_message("", expected)) if result.is_success(): - report_error(GdAssertMessages.error_result_has_message_on_success(expected)) - elif result.is_error() and result.error_message() != expected: - report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected)) - elif result.is_warn() and result.warn_message() != expected: - report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_has_message_on_success(expected)) + if result.is_error() and result.error_message() != expected: + return report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected)) + if result.is_warn() and result.warn_message() != expected: + return report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected)) + return report_success() func is_value(expected :Variant) -> GdUnitResultAssert: var result := current_value() var value :Variant = null if result == null else result.value() if not GdObjects.equals(value, expected): - report_error(GdAssertMessages.error_result_is_value(value, expected)) - else: - report_success() - return self + return report_error(GdAssertMessages.error_result_is_value(value, expected)) + return report_success() func is_equal(expected :Variant) -> GdUnitResultAssert: diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd index 0a1302dd..c3d5d5d6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -42,10 +42,6 @@ func failure_message() -> String: return _current_failure_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 @@ -58,6 +54,7 @@ func append_failure_message(message :String) -> GdUnitSignalAssert: func wait_until(timeout := 2000) -> GdUnitSignalAssert: if timeout <= 0: + @warning_ignore("return_value_discarded") report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!") _timeout = DEFAULT_TIMEOUT else: @@ -68,6 +65,7 @@ func wait_until(timeout := 2000) -> GdUnitSignalAssert: # Verifies the signal exists checked the emitter func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: if not _emitter.has_signal(signal_name): + @warning_ignore("return_value_discarded") report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()]) return self @@ -86,29 +84,30 @@ func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: func _wail_until_signal(signal_name :String, expected_args :Array, expect_not_emitted: bool) -> GdUnitSignalAssert: if _emitter == null: - report_error("Can't wait for signal checked a NULL object.") - return self + return report_error("Can't wait for signal checked a NULL object.") # first verify the signal is defined if not _emitter.has_signal(signal_name): - report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()]) - return self + return report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()]) _signal_collector.register_emitter(_emitter) var time_scale := Engine.get_time_scale() var timer := Timer.new() - Engine.get_main_loop().root.add_child(timer) + (Engine.get_main_loop() as SceneTree).root.add_child(timer) timer.add_to_group("GdUnitTimers") timer.set_one_shot(true) + @warning_ignore("return_value_discarded") timer.timeout.connect(func on_timeout() -> void: _interrupted = true) timer.start((_timeout/1000.0)*time_scale) var is_signal_emitted := false while not _interrupted and not is_signal_emitted: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame if is_instance_valid(_emitter): is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args) if is_signal_emitted and expect_not_emitted: + @warning_ignore("return_value_discarded") report_error(GdAssertMessages.error_signal_emitted(signal_name, expected_args, LocalTime.elapsed(int(_timeout-timer.time_left*1000)))) if _interrupted and not expect_not_emitted: + @warning_ignore("return_value_discarded") report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout))) timer.free() if is_instance_valid(_emitter): diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd index cf2c44be..0f15956c 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -1,14 +1,14 @@ extends GdUnitStringAssert -var _base :GdUnitAssert +var _base: GdUnitAssertImpl func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if current != null and typeof(current) != TYPE_STRING and typeof(current) != TYPE_STRING_NAME: + @warning_ignore("return_value_discarded") report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) @@ -38,21 +38,25 @@ func report_error(error :String) -> GdUnitStringAssert: func override_failure_message(message :String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_null() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self @@ -95,49 +99,56 @@ func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert: func is_empty() -> GdUnitStringAssert: var current :Variant = current_value() - if current == null or not current.is_empty(): + @warning_ignore("unsafe_cast") + if current == null or not (current as String).is_empty(): return report_error(GdAssertMessages.error_is_empty(current)) return report_success() func is_not_empty() -> GdUnitStringAssert: var current :Variant = current_value() - if current == null or current.is_empty(): + @warning_ignore("unsafe_cast") + if current == null or (current as String).is_empty(): return report_error(GdAssertMessages.error_is_not_empty()) return report_success() func contains(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() - if current == null or current.find(expected) == -1: + @warning_ignore("unsafe_cast") + if current == null or (current as String).find(expected) == -1: return report_error(GdAssertMessages.error_contains(current, expected)) return report_success() func not_contains(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() - if current != null and current.find(expected) != -1: + @warning_ignore("unsafe_cast") + if current != null and (current as String).find(expected) != -1: return report_error(GdAssertMessages.error_not_contains(current, expected)) return report_success() func contains_ignoring_case(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() - if current == null or current.findn(expected) == -1: + @warning_ignore("unsafe_cast") + if current == null or (current as String).findn(expected) == -1: return report_error(GdAssertMessages.error_contains_ignoring_case(current, expected)) return report_success() func not_contains_ignoring_case(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() - if current != null and current.findn(expected) != -1: + @warning_ignore("unsafe_cast") + if current != null and (current as String).findn(expected) != -1: return report_error(GdAssertMessages.error_not_contains_ignoring_case(current, expected)) return report_success() func starts_with(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() - if current == null or current.find(expected) != 0: + @warning_ignore("unsafe_cast") + if current == null or (current as String).find(expected) != 0: return report_error(GdAssertMessages.error_starts_with(current, expected)) return report_success() @@ -146,8 +157,10 @@ func ends_with(expected :String) -> GdUnitStringAssert: var current :Variant = current_value() if current == null: return report_error(GdAssertMessages.error_ends_with(current, expected)) - var find :int = current.length() - expected.length() - if current.rfind(expected) != find: + @warning_ignore("unsafe_cast") + var find :int = (current as String).length() - expected.length() + @warning_ignore("unsafe_cast") + if (current as String).rfind(expected) != find: return report_error(GdAssertMessages.error_ends_with(current, expected)) return report_success() @@ -157,22 +170,23 @@ func has_length(expected :int, comparator := Comparator.EQUAL) -> GdUnitStringAs var current :Variant = current_value() if current == null: return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + var str_current: String = current match comparator: Comparator.EQUAL: - if current.length() != expected: - return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + if str_current.length() != expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) Comparator.LESS_THAN: - if current.length() >= expected: - return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + if str_current.length() >= expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) Comparator.LESS_EQUAL: - if current.length() > expected: - return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + if str_current.length() > expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) Comparator.GREATER_THAN: - if current.length() <= expected: - return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + if str_current.length() <= expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) Comparator.GREATER_EQUAL: - if current.length() < expected: - return report_error(GdAssertMessages.error_has_length(current, expected, comparator)) + if str_current.length() < expected: + return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator)) _: return report_error("Comparator '%d' not implemented!" % comparator) return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd index 0b35ed73..7b10d6bb 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -1,15 +1,16 @@ extends GdUnitVectorAssert -var _base: GdUnitAssert -var _current_type :int +var _base: GdUnitAssertImpl +var _current_type: int +var _type_check: bool - -func _init(current :Variant) -> void: - _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", - ResourceLoader.CACHE_MODE_REUSE).new(current) +func _init(current: Variant, type_check := true) -> void: + _type_check = type_check + _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _validate_value_type(current): + @warning_ignore("return_value_discarded") report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current)) _current_type = typeof(current) @@ -39,6 +40,7 @@ func _validate_is_vector_type(value :Variant) -> bool: var type := typeof(value) if type == _current_type or _current_type == TYPE_NIL: return true + @warning_ignore("return_value_discarded") report_error(GdAssertMessages.error_is_wrong_type(_current_type, type)) return false @@ -62,35 +64,41 @@ func failure_message() -> String: func override_failure_message(message :String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message :String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self func is_null() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.is_null() return self func is_not_null() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.is_not_null() return self -func is_equal(expected :Variant) -> GdUnitVectorAssert: - if not _validate_is_vector_type(expected): +func is_equal(expected: Variant) -> GdUnitVectorAssert: + if _type_check and not _validate_is_vector_type(expected): return self + @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitVectorAssert: - if not _validate_is_vector_type(expected): +func is_not_equal(expected: Variant) -> GdUnitVectorAssert: + if _type_check and not _validate_is_vector_type(expected): return self + @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd b/addons/gdUnit4/src/asserts/ValueProvider.gd index a94aa91d..be01f70b 100644 --- a/addons/gdUnit4/src/asserts/ValueProvider.gd +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd @@ -4,3 +4,7 @@ extends RefCounted func get_value() -> Variant: return null + + +func dispose() -> void: + pass diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd index 1abe67ff..aa023194 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd @@ -40,22 +40,23 @@ func options() -> CmdOptions: return _options -func _parse_cmd_arguments(option :CmdOption, args :Array) -> int: +func _parse_cmd_arguments(option: CmdOption, args: Array) -> int: var command_name := option.short_command() - var command :CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name)) + var command: CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name)) if option.has_argument(): if not option.is_argument_optional() and args.is_empty(): return -1 if _is_next_value_argument(args): - command.add_argument(args.pop_front()) + var value: String = args.pop_front() + command.add_argument(value) elif not option.is_argument_optional(): return -1 _parsed_commands[command_name] = command return 0 -func _is_next_value_argument(args :Array) -> bool: +func _is_next_value_argument(args: PackedStringArray) -> bool: if args.is_empty(): return false return _options.get_option(args[0]) == null diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd b/addons/gdUnit4/src/cmd/CmdCommand.gd index 58f09155..92e8c1fe 100644 --- a/addons/gdUnit4/src/cmd/CmdCommand.gd +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd @@ -19,6 +19,7 @@ func arguments() -> PackedStringArray: func add_argument(arg :String) -> void: + @warning_ignore("return_value_discarded") _arguments.append(arg) diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd index 5b3b1cdc..a11c7bcf 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd @@ -57,14 +57,17 @@ func _validate() -> GdUnitResult: if _command_cbs[cmd_name][CB_SINGLE_ARG] else _command_cbs[cmd_name][CB_MULTI_ARGS]) if cb != NO_CB and not cb.is_valid(): + @warning_ignore("return_value_discarded") errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name) if _cmd_options.get_option(cmd_name) == null: + @warning_ignore("return_value_discarded") errors.append("The command '%s' is unknown, verify your CmdOptions!" % cmd_name) # verify for multiple registered command callbacks if _enhanced_fr_test and cb != NO_CB: var cb_method: = cb.get_method() if registered_cbs.has(cb_method): var already_registered_cmd :String = registered_cbs[cb_method] + @warning_ignore("return_value_discarded") errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd]) else: registered_cbs[cb_method] = cmd_name @@ -95,7 +98,8 @@ func execute(commands :Array[CmdCommand]) -> GdUnitResult: # we need to find the method and determin the arguments to call the right function for m in cb_m.get_object().get_method_list(): if m["name"] == cb_m.get_method(): - if m["args"].size() > 1: + @warning_ignore("unsafe_cast") + if (m["args"] as Array).size() > 1: cb_m.callv(arguments) break else: diff --git a/addons/gdUnit4/src/cmd/CmdConsole.gd b/addons/gdUnit4/src/cmd/CmdConsole.gd index 62a2949d..a10c73e7 100644 --- a/addons/gdUnit4/src/cmd/CmdConsole.gd +++ b/addons/gdUnit4/src/cmd/CmdConsole.gd @@ -23,14 +23,14 @@ var _color_mode := COLOR_TABLE func color(p_color :Color) -> CmdConsole: # using color table 16 - 231 a 6 x 6 x 6 RGB color cube (16 + R * 36 + G * 6 + B) - if _color_mode == COLOR_TABLE: - @warning_ignore("integer_division") - var c2 := 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42) - if _debug_show_color_codes: - printraw("%6d" % [c2]) - printraw("[38;5;%dm" % c2 ) - else: - printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] ) + #if _color_mode == COLOR_TABLE: + # @warning_ignore("integer_division") + # var c2 := 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42) + # if _debug_show_color_codes: + # printraw("%6d" % [c2]) + # printraw("[38;5;%dm" % c2 ) + #else: + printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] ) return self @@ -59,6 +59,7 @@ func scroll_area(from :int, to :int) -> CmdConsole: return self +@warning_ignore("return_value_discarded") func progress_bar(p_progress :int, p_color :Color = Color.POWDER_BLUE) -> CmdConsole: if p_progress < 0: p_progress = 0 @@ -123,6 +124,7 @@ func print_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole: .end_color() +@warning_ignore("return_value_discarded") func print_color_table() -> void: prints_color("Color Table 6x6x6", Color.ANTIQUE_WHITE) _debug_show_color_codes = true diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd index 3e3d3a9f..19cbad0d 100644 --- a/addons/gdUnit4/src/core/GdArrayTools.gd +++ b/addons/gdUnit4/src/core/GdArrayTools.gd @@ -28,10 +28,11 @@ static func is_type_array(type :int) -> bool: ## Filters an array by given value[br] ## If the given value not an array it returns null, will remove all occurence of given value. -static func filter_value(array :Variant, value :Variant) -> Variant: +@warning_ignore("unsafe_method_access") +static func filter_value(array: Variant, value: Variant) -> Variant: if not is_array_type(array): return null - var filtered_array :Variant = array.duplicate() + var filtered_array: Variant = array.duplicate() var index :int = filtered_array.find(value) while index != -1: filtered_array.remove_at(index) @@ -72,13 +73,12 @@ static func scan_typed(array :Array) -> int: ## # will result in PackedString(["a", "b"]) ## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN)) ## [/codeblock] -static func as_string(elements :Variant, encode_value := true) -> String: - if not is_array_type(elements): - return "ERROR: Not an Array Type!" +static func as_string(elements: Variant, encode_value := true) -> String: var delemiter := ", " if elements == null: return "" - if elements.is_empty(): + @warning_ignore("unsafe_cast") + if (elements as Array).is_empty(): return "" var prefix := _typeof_as_string(elements) if encode_value else "" var formatted := "" diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd index a918a990..16e567b0 100644 --- a/addons/gdUnit4/src/core/GdDiffTool.gd +++ b/addons/gdUnit4/src/core/GdDiffTool.gd @@ -7,7 +7,7 @@ const DIV_ADD :int = 214 const DIV_SUB :int = 215 -static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff: Array, rdiff: Array) -> void: +static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array[Array], ldiff: Array, rdiff: Array) -> void: var loffset := lb.size() var roffset := rb.size() @@ -39,17 +39,19 @@ static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array, ldiff # lookup[i][j] stores the length of LCS of substring X[0..i-1], Y[0..j-1] -static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array: - var lookup := Array() +static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]: + var lookup: Array[Array] = [] + @warning_ignore("return_value_discarded") lookup.resize(lb.size() + 1) for i in lookup.size(): var x := [] + @warning_ignore("return_value_discarded") x.resize(rb.size() + 1) lookup[i] = x return lookup -static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array: +static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]: var lookup := _createLookUp(lb, rb) # first column of the lookup table will be all 0 for i in lookup.size(): @@ -105,6 +107,7 @@ static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStri var lcsResultList := PackedStringArray(); while (i < text1WordCount && j < text2WordCount): if text1Words[i] == text2Words[j]: + @warning_ignore("return_value_discarded") lcsResultList.append(text2Words[j]) i += 1 j += 1 diff --git a/addons/gdUnit4/src/core/GdFunctionDoubler.gd b/addons/gdUnit4/src/core/GdFunctionDoubler.gd index ade86539..99a522f2 100644 --- a/addons/gdUnit4/src/core/GdFunctionDoubler.gd +++ b/addons/gdUnit4/src/core/GdFunctionDoubler.gd @@ -82,7 +82,9 @@ static func get_enum_default(value :String) -> Variant: return %s.values()[0] """.dedent() % value + @warning_ignore("return_value_discarded") script.reload() + @warning_ignore("unsafe_method_access") return script.new().call("get_enum_default") @@ -113,20 +115,19 @@ func _init(push_errors :bool = false) -> void: @warning_ignore("unused_parameter") -func get_template(return_type :Variant, is_vararg :bool) -> String: - push_error("Must be implemented!") +func get_template(return_type: GdFunctionDescriptor, is_callable: bool) -> String: + assert(false, "'get_template' must be implemented!") return "" -func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray: - var func_signature := func_descriptor.typeless() + +func double(func_descriptor: GdFunctionDescriptor, is_callable: bool = false) -> PackedStringArray: var is_static := func_descriptor.is_static() - var is_vararg := func_descriptor.is_vararg() var is_coroutine := func_descriptor.is_coroutine() var func_name := func_descriptor.name() var args := func_descriptor.args() var varargs := func_descriptor.varargs() var return_value := GdFunctionDoubler.default_return_value(func_descriptor) - var arg_names := extract_arg_names(args) + var arg_names := extract_arg_names(args, true) var vararg_names := extract_arg_names(varargs) # save original constructor arguments @@ -135,17 +136,15 @@ func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray: var constructor := "func _init(%s) -> void:\n super(%s)\n pass\n" % [constructor_args, ", ".join(arg_names)] return constructor.split("\n") - var double_src := "" - double_src += '@warning_ignore("untyped_declaration")\n' if Engine.get_version_info().hex >= 0x40200 else '\n' + var double_src := "@warning_ignore('shadowed_variable', 'untyped_declaration', 'unsafe_call_argument', 'unsafe_method_access')\n" if func_descriptor.is_engine(): double_src += '@warning_ignore("native_method_override")\n' if func_descriptor.return_type() == GdObjects.TYPE_ENUM: double_src += '@warning_ignore("int_as_enum_without_match")\n' double_src += '@warning_ignore("int_as_enum_without_cast")\n' - double_src += '@warning_ignore("shadowed_variable")\n' - double_src += func_signature + double_src += GdFunctionDoubler.extract_func_signature(func_descriptor) # fix to unix format, this is need when the template is edited under windows than the template is stored with \r\n - var func_template := get_template(func_descriptor.return_type(), is_vararg).replace("\r\n", "\n") + var func_template := get_template(func_descriptor, is_callable).replace("\r\n", "\n") double_src += func_template\ .replace("$(arguments)", ", ".join(arg_names))\ .replace("$(varargs)", ", ".join(vararg_names))\ @@ -161,25 +160,54 @@ func double(func_descriptor :GdFunctionDescriptor) -> PackedStringArray: return double_src.split("\n") -func extract_arg_names(argument_signatures :Array[GdFunctionArgument]) -> PackedStringArray: +func extract_arg_names(argument_signatures: Array[GdFunctionArgument], add_suffix := false) -> PackedStringArray: var arg_names := PackedStringArray() for arg in argument_signatures: - arg_names.append(arg._name) + @warning_ignore("return_value_discarded") + arg_names.append(arg._name + ("_" if add_suffix else "")) return arg_names static func extract_constructor_args(args :Array[GdFunctionArgument]) -> PackedStringArray: var constructor_args := PackedStringArray() for arg in args: - var arg_name := arg._name + var arg_name := arg._name + "_" var default_value := get_default(arg) if default_value == "null": + @warning_ignore("return_value_discarded") constructor_args.append(arg_name + ":Variant=" + default_value) else: + @warning_ignore("return_value_discarded") constructor_args.append(arg_name + ":=" + default_value) return constructor_args +static func extract_func_signature(descriptor: GdFunctionDescriptor) -> String: + var func_signature := "" + if descriptor._return_type == TYPE_NIL: + func_signature = "func %s(%s) -> void:" % [descriptor.name(), typeless_args(descriptor)] + elif descriptor._return_type == GdObjects.TYPE_VARIANT: + func_signature = "func %s(%s):" % [descriptor.name(), typeless_args(descriptor)] + else: + func_signature = "func %s(%s) -> %s:" % [descriptor.name(), typeless_args(descriptor), descriptor.return_type_as_string()] + return "static " + func_signature if descriptor.is_static() else func_signature + + +static func typeless_args(descriptor: GdFunctionDescriptor) -> String: + var collect := PackedStringArray() + for arg in descriptor.args(): + if arg.has_default(): + @warning_ignore("return_value_discarded") + collect.push_back(arg.name() + "_" + "=" + arg.value_as_string()) + else: + @warning_ignore("return_value_discarded") + collect.push_back(arg.name() + "_") + for arg in descriptor.varargs(): + @warning_ignore("return_value_discarded") + collect.push_back(arg.name() + "=" + arg.value_as_string()) + return ", ".join(collect) + + static func get_default(arg :GdFunctionArgument) -> String: if arg.has_default(): return arg.value_as_string() diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index 5a2eb5c2..2a780ce8 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -8,17 +8,16 @@ const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") # introduced with Godot 4.3.beta1 const TYPE_PACKED_VECTOR4_ARRAY = 38 #TYPE_PACKED_VECTOR4_ARRAY -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 +const TYPE_VOID = 1000 +const TYPE_VARARG = 1001 +const TYPE_VARIANT = 1002 +const TYPE_FUNC = 1003 +const TYPE_FUZZER = 1004 # missing Godot types -const TYPE_CONTROL = TYPE_MAX + 2002 -const TYPE_CANVAS = TYPE_MAX + 2003 -const TYPE_ENUM = TYPE_MAX + 2004 +const TYPE_NODE = 2001 +const TYPE_CONTROL = 2002 +const TYPE_CANVAS = 2003 +const TYPE_ENUM = 2004 # used as default value for varargs @@ -146,7 +145,8 @@ enum COMPARE_MODE { # prototype of better object to dictionary -static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary: +@warning_ignore("unsafe_cast") +static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary: if obj == null: return {} var clazz_name := obj.get_class() @@ -154,13 +154,20 @@ static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary: var clazz_path := "" if is_instance_valid(obj) and obj.get_script() != null: - var d := inst_to_dict(obj) - clazz_path = d["@path"] - if d["@subpath"] != NodePath(""): - clazz_name = d["@subpath"] - dict["@inner_class"] = true + var script: Script = obj.get_script() + # handle build-in scripts + if script.resource_path != null and script.resource_path.contains(".tscn"): + var path_elements := script.resource_path.split(".tscn") + clazz_name = path_elements[0].get_file() + clazz_path = script.resource_path else: - clazz_name = clazz_path.get_file().replace(".gd", "") + var d := inst_to_dict(obj) + clazz_path = d["@path"] + if d["@subpath"] != NodePath(""): + clazz_name = d["@subpath"] + dict["@inner_class"] = true + else: + clazz_name = clazz_path.get_file().replace(".gd", "") dict["@path"] = clazz_path for property in obj.get_property_list(): @@ -178,11 +185,14 @@ static func obj2dict(obj :Object, hashed_objects := Dictionary()) -> Dictionary: dict[property_name] = str(property_value) continue hashed_objects[obj] = true - dict[property_name] = obj2dict(property_value, hashed_objects) + dict[property_name] = obj2dict(property_value as Object, hashed_objects) else: dict[property_name] = property_value - if obj.has_method("get_children"): - var childrens :Array = obj.get_children() + if obj is Node: + var childrens :Array = (obj as Node).get_children() + dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects)) + if obj is TreeItem: + var childrens :Array = (obj as TreeItem).get_children() dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects)) return {"%s" % clazz_name : dict} @@ -192,14 +202,15 @@ static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0) -static func equals_sorted(obj_a :Array, obj_b :Array, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: - var a := obj_a.duplicate() - var b := obj_b.duplicate() +static func equals_sorted(obj_a: Array[Variant], obj_b: Array[Variant], case_sensitive: bool = false, compare_mode: COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool: + var a: Array[Variant] = obj_a.duplicate() + var b: Array[Variant] = obj_b.duplicate() a.sort() b.sort() return equals(a, b, case_sensitive, compare_mode) +@warning_ignore("unsafe_method_access", "unsafe_cast") static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool: var type_a := typeof(obj_a) var type_b := typeof(obj_b) @@ -239,8 +250,8 @@ static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compar return false if obj_a.get_class() != obj_b.get_class(): return false - var a := obj2dict(obj_a) - var b := obj2dict(obj_b) + var a := obj2dict(obj_a as Object) + var b := obj2dict(obj_b as Object) return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth) return obj_a == obj_b @@ -298,6 +309,7 @@ static func to_pascal_case(value :String) -> String: return value.capitalize().replace(" ", "") +@warning_ignore("return_value_discarded") static func to_snake_case(value :String) -> String: var result := PackedStringArray() for ch in value: @@ -318,6 +330,8 @@ static func is_snake_case(value :String) -> bool: static func type_as_string(type :int) -> String: + if type < TYPE_MAX: + return type_string(type) return TYPE_AS_STRING_MAPPINGS.get(type, "Variant") @@ -350,10 +364,13 @@ static func _is_type_equivalent(type_a :int, type_b :int) -> bool: or type_a == type_b) -static func is_engine_type(value :Object) -> bool: +static func is_engine_type(value :Variant) -> bool: if value is GDScript or value is ScriptExtension: return false - return value.is_class("GDScriptNativeClass") + var obj: Object = value + if is_instance_valid(obj) and obj.has_method("is_class"): + return obj.is_class("GDScriptNativeClass") + return false static func is_type(value :Variant) -> bool: @@ -364,6 +381,7 @@ static func is_type(value :Variant) -> bool: if is_engine_type(value): return true # is a custom class type + @warning_ignore("unsafe_cast") if value is GDScript and (value as GDScript).can_instantiate(): return true return false @@ -377,7 +395,8 @@ static func _is_same(left :Variant, right :Variant) -> bool: if left_type != right_type: return false if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT: - return left.get_instance_id() == right.get_instance_id() + @warning_ignore("unsafe_cast") + return (left as Object).get_instance_id() == (right as Object).get_instance_id() return equals(left, right) @@ -402,7 +421,8 @@ static func is_scene(value :Variant) -> bool: static func is_scene_resource_path(value :Variant) -> bool: - return value is String and value.ends_with(".tscn") + @warning_ignore("unsafe_cast") + return value is String and (value as String).ends_with(".tscn") static func is_gd_script(script :Script) -> bool: @@ -418,8 +438,8 @@ static func is_gd_testsuite(script :Script) -> bool: if is_gd_script(script): var stack := [script] while not stack.is_empty(): - var current := stack.pop_front() as Script - var base := current.get_base_script() as Script + var current: Script = stack.pop_front() + var base: Script = current.get_base_script() if base != null: if base.resource_path.find("GdUnitTestSuite") != -1: return true @@ -427,11 +447,12 @@ static func is_gd_testsuite(script :Script) -> bool: return false -static func is_singleton(value :Variant) -> bool: +static func is_singleton(value: Variant) -> bool: if not is_instance_valid(value) or is_native_class(value): return false for name in Engine.get_singleton_list(): - if value.is_class(name): + @warning_ignore("unsafe_cast") + if (value as Object).is_class(name): return true return false @@ -439,17 +460,19 @@ static func is_singleton(value :Variant) -> bool: static func is_instance(value :Variant) -> bool: if not is_instance_valid(value) or is_native_class(value): return false + @warning_ignore("unsafe_cast") if is_script(value) and (value as Script).get_instance_base_type() == "": return true if is_scene(value): return true - return not value.has_method('new') and not value.has_method('instance') + @warning_ignore("unsafe_cast") + return not (value as Object).has_method('new') and not (value as Object).has_method('instance') # only object form type Node and attached filename static func is_instance_scene(instance :Variant) -> bool: if instance is Node: - var node := instance as Node + var node: Node = instance return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty() return false @@ -457,7 +480,8 @@ static func is_instance_scene(instance :Variant) -> bool: static func can_be_instantiate(obj :Variant) -> bool: if not obj or is_engine_type(obj): return false - return obj.has_method("new") + @warning_ignore("unsafe_cast") + return (obj as Object).has_method("new") static func create_instance(clazz :Variant) -> GdUnitResult: @@ -466,48 +490,54 @@ static func create_instance(clazz :Variant) -> GdUnitResult: # test is given clazz already an instance if is_instance(clazz): return GdUnitResult.success(clazz) + @warning_ignore("unsafe_method_access") return GdUnitResult.success(clazz.new()) TYPE_STRING: - if ClassDB.class_exists(clazz): - if Engine.has_singleton(clazz): - return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz) - if not ClassDB.can_instantiate(clazz): - return GdUnitResult.error("Can't instance Engine class '%s'." % clazz) - return GdUnitResult.success(ClassDB.instantiate(clazz)) + var clazz_name: String = clazz + if ClassDB.class_exists(clazz_name): + if Engine.has_singleton(clazz_name): + return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz_name) + if not ClassDB.can_instantiate(clazz_name): + return GdUnitResult.error("Can't instance Engine class '%s'." % clazz_name) + return GdUnitResult.success(ClassDB.instantiate(clazz_name)) else: - var clazz_path :String = extract_class_path(clazz)[0] + var clazz_path :String = extract_class_path(clazz_name)[0] if not FileAccess.file_exists(clazz_path): - return GdUnitResult.error("Class '%s' not found." % clazz) - var script := load(clazz_path) + return GdUnitResult.error("Class '%s' not found." % clazz_name) + var script: GDScript = load(clazz_path) if script != null: return GdUnitResult.success(script.new()) else: - return GdUnitResult.error("Can't create instance for '%s'." % clazz) - return GdUnitResult.error("Can't create instance for class '%s'." % clazz) + return GdUnitResult.error("Can't create instance for '%s'." % clazz_name) + return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz)) +@warning_ignore("return_value_discarded") static func extract_class_path(clazz :Variant) -> PackedStringArray: var clazz_path := PackedStringArray() if clazz is String: - clazz_path.append(clazz) + @warning_ignore("unsafe_cast") + clazz_path.append(clazz as String) return clazz_path if is_instance(clazz): # is instance a script instance? - var script := clazz.script as GDScript + var script: GDScript = clazz.script if script != null: return extract_class_path(script) return clazz_path if clazz is GDScript: - if not clazz.resource_path.is_empty(): - clazz_path.append(clazz.resource_path) + var script: GDScript = clazz + if not script.resource_path.is_empty(): + clazz_path.append(script.resource_path) return clazz_path # if not found we go the expensive way and extract the path form the script by creating an instance - var arg_list := build_function_default_arguments(clazz, "_init") - var instance :Variant = clazz.callv("new", arg_list) + var arg_list := build_function_default_arguments(script, "_init") + var instance: Object = script.callv("new", arg_list) var clazz_info := inst_to_dict(instance) GdUnitTools.free_instance(instance) - clazz_path.append(clazz_info["@path"]) + @warning_ignore("unsafe_cast") + clazz_path.append(clazz_info["@path"] as String) if clazz_info.has("@subpath"): var sub_path :String = clazz_info["@subpath"] if not sub_path.is_empty(): @@ -534,33 +564,38 @@ static func extract_class_name(clazz :Variant) -> GdUnitResult: if is_instance(clazz): # is instance a script instance? - var script := clazz.script as GDScript + var script: GDScript = clazz.script if script != null: return extract_class_name(script) + @warning_ignore("unsafe_cast") return GdUnitResult.success((clazz as Object).get_class()) # extract name form full qualified class path if clazz is String: - if ClassDB.class_exists(clazz): - return GdUnitResult.success(clazz) - var source_sript :Script = load(clazz) - var clazz_name :String = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript) + var clazz_name: String = clazz + if ClassDB.class_exists(clazz_name): + return GdUnitResult.success(clazz_name) + var source_script :GDScript = load(clazz_name) + clazz_name = GdScriptParser.new().get_class_name(source_script) return GdUnitResult.success(to_pascal_case(clazz_name)) if is_primitive_type(clazz): return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) if is_script(clazz): - if clazz.resource_path.is_empty(): + @warning_ignore("unsafe_cast") + if (clazz as Script).resource_path.is_empty(): var class_path := extract_class_name_from_class_path(extract_class_path(clazz)) return GdUnitResult.success(class_path); return extract_class_name(clazz.resource_path) # need to create an instance for a class typ the extract the class name + @warning_ignore("unsafe_method_access") var instance :Variant = clazz.new() if instance == null: - return GdUnitResult.error("Can't create a instance for class '%s'" % clazz) + return GdUnitResult.error("Can't create a instance for class '%s'" % str(clazz)) var result := extract_class_name(instance) + @warning_ignore("return_value_discarded") GdUnitTools.free_instance(instance) return result @@ -576,6 +611,7 @@ static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStr var value :Variant = map.get(key) if value is GDScript: var class_path := extract_class_path(value) + @warning_ignore("return_value_discarded") inner_classes.append(class_path[1]) return inner_classes @@ -654,6 +690,7 @@ static func default_value_by_type(type :int) -> Variant: TYPE_NODE_PATH: return NodePath() TYPE_RID: return RID() TYPE_OBJECT: return null + TYPE_CALLABLE: return Callable() TYPE_ARRAY: return [] TYPE_DICTIONARY: return {} TYPE_PACKED_BYTE_ARRAY: return PackedByteArray() diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd index 5918353d..777eb92c 100644 --- a/addons/gdUnit4/src/core/GdUnit4Version.gd +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd @@ -16,6 +16,7 @@ func _init(major :int, minor :int, patch :int) -> void: static func parse(value :String) -> GdUnit4Version: var regex := RegEx.new() + @warning_ignore("return_value_discarded") regex.compile("[a-zA-Z:,-]+") var cleaned := regex.sub(value, "", true) var parts := cleaned.split(".") @@ -27,8 +28,10 @@ static func parse(value :String) -> GdUnit4Version: static func current() -> GdUnit4Version: var config := ConfigFile.new() + @warning_ignore("return_value_discarded") config.load('addons/gdUnit4/plugin.cfg') - return parse(config.get_value('plugin', 'version')) + @warning_ignore("unsafe_cast") + return parse(config.get_value('plugin', 'version') as String) func equals(other :GdUnit4Version) -> bool: @@ -45,12 +48,13 @@ func is_greater(other :GdUnit4Version) -> bool: static func init_version_label(label :Control) -> void: var config := ConfigFile.new() + @warning_ignore("return_value_discarded") config.load('addons/gdUnit4/plugin.cfg') var version :String = config.get_value('plugin', 'version') if label is RichTextLabel: - label.text = VERSION_PATTERN.replace('${version}', version) + (label as RichTextLabel).text = VERSION_PATTERN.replace('${version}', version) else: - label.text = "gdUnit4 " + version + (label as Label).text = "gdUnit4 " + version func _to_string() -> String: diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd index 4f0d2bbc..8a96a29b 100644 --- a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd +++ b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd @@ -37,12 +37,15 @@ static func check_leaked_instances() -> void: # class_info = { "class_name": <>, "class_path" : <>} static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray: # store instance id + var clazz_name: String = class_info.get("class_name") var source_code := template\ .replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\ - .replace("${source_class}", class_info.get("class_name")) + .replace("${source_class}", clazz_name) var lines := GdScriptParser.to_unix_format(source_code).split("\n") # replace template class_name with Doubled name and extends form source class - lines.insert(0, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) + @warning_ignore("return_value_discarded") + lines.insert(0, "class_name Doubled%s" % clazz_name.replace(".", "_")) + @warning_ignore("return_value_discarded") lines.insert(1, extends_clazz(class_info)) # append Object interactions stuff lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n")) @@ -74,16 +77,14 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P push_error(result.error_message()) return PackedStringArray() var class_descriptor :GdClassDescriptor = result.value() - while class_descriptor != null: - for func_descriptor in class_descriptor.functions(): - if instance != null and not instance.has_method(func_descriptor.name()): - #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) - continue - if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): - continue - doubled_source += func_doubler.double(func_descriptor) - functions.append(func_descriptor.name()) - class_descriptor = class_descriptor.parent() + for func_descriptor in class_descriptor.functions(): + if instance != null and not instance.has_method(func_descriptor.name()): + #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) + continue + if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): + continue + doubled_source += func_doubler.double(func_descriptor, instance is CallableDoubler) + functions.append(func_descriptor.name()) # double regular class functions var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path) @@ -103,7 +104,7 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) continue functions.append(func_descriptor.name()) - doubled_source.append_array(func_doubler.double(func_descriptor)) + doubled_source.append_array(func_doubler.double(func_descriptor, instance is CallableDoubler)) return doubled_source diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd index 01022ddb..e73216d0 100644 --- a/addons/gdUnit4/src/core/GdUnitFileAccess.gd +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd @@ -23,6 +23,7 @@ static func create_temp_file(relative_path :String, file_name :String, mode := F static func temp_dir() -> String: if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) return GDUNIT_TEMP @@ -30,6 +31,7 @@ static func temp_dir() -> String: static func create_temp_dir(folder_name :String) -> String: var new_folder := temp_dir() + "/" + folder_name if not DirAccess.dir_exists_absolute(new_folder): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(new_folder) return new_folder @@ -61,6 +63,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f var source_dir := DirAccess.open(from_dir) var dest_dir := DirAccess.open(to_dir) if source_dir != null: + @warning_ignore("return_value_discarded") source_dir.list_dir_begin() var next := "." @@ -72,6 +75,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f var dest := dest_dir.get_current_dir() + "/" + next if source_dir.current_is_dir(): if recursive: + @warning_ignore("return_value_discarded") copy_directory(source + "/", dest, recursive) continue var err := source_dir.copy(source, dest) @@ -88,6 +92,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f static func delete_directory(path :String, only_content := false) -> void: var dir := DirAccess.open(path) if dir != null: + @warning_ignore("return_value_discarded") dir.list_dir_begin() var file_name := "." while file_name != "": @@ -113,6 +118,7 @@ static func delete_path_index_lower_equals_than(path :String, prefix :String, in if dir == null: return 0 var deleted := 0 + @warning_ignore("return_value_discarded") dir.list_dir_begin() var next := "." while next != "": @@ -134,6 +140,7 @@ static func find_last_path_index(path :String, prefix :String) -> int: if dir == null: return 0 var last_iteration := 0 + @warning_ignore("return_value_discarded") dir.list_dir_begin() var next := "." while next != "": @@ -152,12 +159,14 @@ static func scan_dir(path :String) -> PackedStringArray: if dir == null or not dir.dir_exists(path): return PackedStringArray() var content := PackedStringArray() + @warning_ignore("return_value_discarded") dir.list_dir_begin() var next := "." while next != "": next = dir.get_next() if next.is_empty() or next == "." or next == "..": continue + @warning_ignore("return_value_discarded") content.append(next) return content @@ -169,6 +178,7 @@ static func resource_as_array(resource_path :String) -> PackedStringArray: return PackedStringArray() var file_content := PackedStringArray() while not file.eof_reached(): + @warning_ignore("return_value_discarded") file_content.append(file.get_line()) return file_content @@ -203,9 +213,11 @@ static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult: for zip_entry in zip_entries: var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") if zip_entry.ends_with("/"): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(new_file_path) continue var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) file.store_buffer(zip.read_file(zip_entry)) + @warning_ignore("return_value_discarded") zip.close() return GdUnitResult.success(dest_path) diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd index 36930fc7..c3ea021a 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd @@ -5,13 +5,15 @@ extends RefCounted static func verify(interaction_object :Object, interactions_times :int) -> Variant: if not _is_mock_or_spy(interaction_object, "__verify"): return interaction_object + @warning_ignore("unsafe_method_access") return interaction_object.__do_verify_interactions(interactions_times) static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert: - var __gd_assert :GdUnitAssert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") + var __gd_assert := GdUnitAssertImpl.new("") if not _is_mock_or_spy(interaction_object, "__verify"): return __gd_assert.report_success() + @warning_ignore("unsafe_method_access") var __summary :Dictionary = interaction_object.__verify_no_interactions() if __summary.is_empty(): return __gd_assert.report_success() @@ -19,9 +21,10 @@ static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert: static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAssert: - var __gd_assert :GdUnitAssert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") + var __gd_assert := GdUnitAssertImpl.new("") if not _is_mock_or_spy(interaction_object, "__verify_no_more_interactions"): return __gd_assert + @warning_ignore("unsafe_method_access") var __summary :Dictionary = interaction_object.__verify_no_more_interactions() if __summary.is_empty(): return __gd_assert @@ -31,12 +34,14 @@ static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAss static func reset(interaction_object :Object) -> Object: if not _is_mock_or_spy(interaction_object, "__reset"): return interaction_object + @warning_ignore("unsafe_method_access") interaction_object.__reset_interactions() return interaction_object static func _is_mock_or_spy(interaction_object :Object, mock_function_signature :String) -> bool: - if interaction_object is GDScript and not interaction_object.get_script().has_script_method(mock_function_signature): + @warning_ignore("unsafe_cast") + if interaction_object is GDScript and not (interaction_object.get_script() as GDScript).has_method(mock_function_signature): push_error("Error: You try to use a non mock or spy!") return false return true diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd index c06b1d4d..94c435f4 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd @@ -1,4 +1,3 @@ -const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") var __expected_interactions :int = -1 var __saved_interactions := Dictionary() @@ -47,8 +46,10 @@ func __verify_interactions(function_args :Array[Variant]) -> void: __error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) else: __error_message = GdAssertMessages.error_validate_interactions(__summary, __expected_summary) + @warning_ignore("return_value_discarded") __gd_assert.report_error(__error_message) else: + @warning_ignore("return_value_discarded") __gd_assert.report_success() __expected_interactions = -1 diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd b/addons/gdUnit4/src/core/GdUnitProperty.gd index 6e338a3f..138dd9f7 100644 --- a/addons/gdUnit4/src/core/GdUnitProperty.gd +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd @@ -31,6 +31,10 @@ func value() -> Variant: return _value +func value_as_string() -> String: + return _value + + func value_set() -> PackedStringArray: return _value_set @@ -44,11 +48,11 @@ func set_value(p_value :Variant) -> void: TYPE_STRING: _value = str(p_value) TYPE_BOOL: - _value = bool(p_value) + _value = convert(p_value, TYPE_BOOL) TYPE_INT: - _value = int(p_value) + _value = convert(p_value, TYPE_INT) TYPE_FLOAT: - _value = float(p_value) + _value = convert(p_value, TYPE_FLOAT) _: _value = p_value diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd b/addons/gdUnit4/src/core/GdUnitResult.gd index f2d297f9..c7187d48 100644 --- a/addons/gdUnit4/src/core/GdUnitResult.gd +++ b/addons/gdUnit4/src/core/GdUnitResult.gd @@ -8,7 +8,7 @@ enum { EMPTY } -var _state :Variant +var _state: int var _warn_message := "" var _error_message := "" var _value :Variant = null @@ -66,6 +66,10 @@ func value() -> Variant: return _value +func value_as_string() -> String: + return _value + + func or_else(p_value :Variant) -> Variant: if not is_success(): return p_value @@ -97,7 +101,8 @@ static func serialize(result :GdUnitResult) -> Dictionary: static func deserialize(config :Dictionary) -> GdUnitResult: var result := GdUnitResult.new() - result._value = str_to_var(config.get("value", "")) + var cfg_value: String = config.get("value", "") + result._value = str_to_var(cfg_value) result._warn_message = config.get("warn_msg", null) result._error_message = config.get("err_msg", null) result._state = config.get("state") diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd index 68aadee3..65ae140e 100644 --- a/addons/gdUnit4/src/core/GdUnitRunner.gd +++ b/addons/gdUnit4/src/core/GdUnitRunner.gd @@ -36,7 +36,9 @@ func _ready() -> void: push_error(config_result.error_message()) _state = EXIT return + @warning_ignore("return_value_discarded") _client.connect("connection_failed", _on_connection_failed) + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) var result := _client.start("127.0.0.1", _config.server_port()) if result.is_error(): @@ -75,11 +77,14 @@ func _process(_delta :float) -> void: # process next test suite set_process(false) var test_suite :Node = _test_suites_to_process.pop_front() + @warning_ignore("unsafe_method_access") if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + @warning_ignore("unsafe_method_access") _cs_executor.Execute(test_suite) + @warning_ignore("unsafe_property_access") await _cs_executor.ExecutionCompleted else: - await _executor.execute(test_suite) + await _executor.execute(test_suite as GdUnitTestSuite) set_process(true) STOP: _state = EXIT @@ -133,6 +138,7 @@ func _do_filter_test_case(test_suite :Node, test_case :Node, included_tests :Pac # we have a paremeterized test selection if test_meta.size() > 1: var test_param_index := test_meta[1] + @warning_ignore("unsafe_method_access") test_case.set_test_parameter_index(test_param_index.to_int()) return # the test is filtered out diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd index 173b536d..820f2482 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -38,6 +38,7 @@ func server_port() -> int: return _config.get(SERVER_PORT, -1) +@warning_ignore("return_value_discarded") func self_test() -> GdUnitRunnerConfig: add_test_suite("res://addons/gdUnit4/test/") add_test_suite("res://addons/gdUnit4/mono/test/") @@ -52,6 +53,7 @@ func add_test_suite(p_resource_path :String) -> GdUnitRunnerConfig: func add_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig: for resource_path_ in resource_paths: + @warning_ignore("return_value_discarded") add_test_suite(resource_path_) return self @@ -60,8 +62,10 @@ func add_test_case(p_resource_path :String, test_name :StringName, test_param_in var to_execute_ := to_execute() var test_cases :PackedStringArray = to_execute_.get(p_resource_path, PackedStringArray()) if test_param_index != -1: + @warning_ignore("return_value_discarded") test_cases.append("%s:%d" % [test_name, test_param_index]) else: + @warning_ignore("return_value_discarded") test_cases.append(test_name) to_execute_[p_resource_path] = test_cases return self @@ -72,18 +76,22 @@ func add_test_case(p_resource_path :String, test_name :StringName, test_param_in # '/path/path', res://path/path', 'res://path/path/testsuite.gd' or 'testsuite' # 'res://path/path/testsuite.gd:test_case' or 'testsuite:test_case' func skip_test_suite(value :StringName) -> GdUnitRunnerConfig: - var parts :Array = GdUnitFileAccess.make_qualified_path(value).rsplit(":") + var parts: PackedStringArray = GdUnitFileAccess.make_qualified_path(value).rsplit(":") if parts[0] == "res": - parts.pop_front() + parts.remove_at(0) parts[0] = GdUnitFileAccess.make_qualified_path(parts[0]) match parts.size(): - 1: skipped()[parts[0]] = PackedStringArray() - 2: skip_test_case(parts[0], parts[1]) + 1: + skipped()[parts[0]] = PackedStringArray() + 2: + @warning_ignore("return_value_discarded") + skip_test_case(parts[0], parts[1]) return self func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig: for resource_path_ in resource_paths: + @warning_ignore("return_value_discarded") skip_test_suite(resource_path_) return self @@ -91,6 +99,7 @@ func skip_test_suites(resource_paths :PackedStringArray) -> GdUnitRunnerConfig: func skip_test_case(p_resource_path :String, test_name :StringName) -> GdUnitRunnerConfig: var to_ignore := skipped() var test_cases :PackedStringArray = to_ignore.get(p_resource_path, PackedStringArray()) + @warning_ignore("return_value_discarded") test_cases.append(test_name) to_ignore[p_resource_path] = test_cases return self @@ -129,19 +138,20 @@ func load_config(path :String = CONFIG_FILE) -> GdUnitResult: var error := test_json_conv.parse(content) if error != OK: return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) - _config = test_json_conv.get_data() as Dictionary + _config = test_json_conv.get_data() if not _config.has(VERSION): return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) fix_value_types() return GdUnitResult.success(path) +@warning_ignore("unsafe_cast") func fix_value_types() -> void: # fix float value to int json stores all numbers as float var server_port_ :int = _config.get(SERVER_PORT, -1) _config[SERVER_PORT] = server_port_ - convert_Array_to_PackedStringArray(_config[INCLUDED]) - convert_Array_to_PackedStringArray(_config[SKIPPED]) + convert_Array_to_PackedStringArray(_config[INCLUDED] as Dictionary) + convert_Array_to_PackedStringArray(_config[SKIPPED] as Dictionary) func convert_Array_to_PackedStringArray(data :Dictionary) -> void: diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index bd2da3c8..c70be022 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -3,7 +3,7 @@ class_name GdUnitSceneRunnerImpl extends GdUnitSceneRunner -var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) +var GdUnitFuncAssertImpl: GDScript = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) # mapping of mouse buttons and his masks @@ -19,29 +19,36 @@ const MAP_MOUSE_BUTTON_MASKS := { } var _is_disposed := false -var _current_scene :Node = null -var _awaiter :GdUnitAwaiter = GdUnitAwaiter.new() -var _verbose :bool -var _simulate_start_time :LocalTime -var _last_input_event :InputEvent = null +var _current_scene: Node = null +var _awaiter: GdUnitAwaiter = GdUnitAwaiter.new() +var _verbose: bool +var _simulate_start_time: LocalTime +var _last_input_event: InputEvent = null var _mouse_button_on_press := [] var _key_on_press := [] var _action_on_press := [] -var _curent_mouse_position :Vector2 +var _curent_mouse_position: Vector2 +# holds the touch position for each touch index +# { index: int = position: Vector2} +var _current_touch_position: Dictionary = {} +# holds the curretn touch drag position +var _current_touch_drag_position: Vector2 = Vector2.ZERO # time factor settings var _time_factor := 1.0 -var _saved_iterations_per_second :float +var _saved_iterations_per_second: float var _scene_auto_free := false -func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> void: +func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> void: _verbose = p_verbose _saved_iterations_per_second = Engine.get_physics_ticks_per_second() + @warning_ignore("return_value_discarded") set_time_factor(1) # handle scene loading by resource path if typeof(p_scene) == TYPE_STRING: - if !ResourceLoader.exists(p_scene): + @warning_ignore("unsafe_cast") + if !ResourceLoader.exists(p_scene as String): if not p_hide_push_errors: push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene) return @@ -49,7 +56,8 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo if not p_hide_push_errors: push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene) return - _current_scene = load(p_scene).instantiate() + @warning_ignore("unsafe_cast") + _current_scene = (load(p_scene as String) as PackedScene).instantiate() _scene_auto_free = true else: # verify we have a node instance @@ -62,10 +70,14 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo if not p_hide_push_errors: push_error("GdUnitSceneRunner: Scene must be not null!") return + _scene_tree().root.add_child(_current_scene) # do finally reset all open input events when the scene is removed + @warning_ignore("return_value_discarded") _scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void: if child == _current_scene: + # we need to disable the processing to avoid input flush buffer errors + _current_scene.process_mode = Node.PROCESS_MODE_DISABLED _reset_input_to_default() ) _simulate_start_time = LocalTime.now() @@ -78,7 +90,7 @@ func _init(p_scene :Variant, p_verbose :bool, p_hide_push_errors := false) -> vo max_iteration_to_wait += 1 -func _notification(what :int) -> void: +func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE and is_instance_valid(self): # reset time factor to normal __deactivate_time_factor() @@ -89,45 +101,52 @@ func _notification(what :int) -> void: _current_scene.free() _is_disposed = true _current_scene = null - # we hide the scene/main window after runner is finished - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) func _scene_tree() -> SceneTree: return Engine.get_main_loop() as SceneTree -func simulate_action_pressed(action :String) -> GdUnitSceneRunner: +@warning_ignore("return_value_discarded") +func simulate_action_pressed(action: String) -> GdUnitSceneRunner: simulate_action_press(action) simulate_action_release(action) return self -func simulate_action_press(action :String) -> GdUnitSceneRunner: +func simulate_action_press(action: String) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventAction.new() event.pressed = true event.action = action + if Engine.get_version_info().hex >= 0x40300: + @warning_ignore("unsafe_property_access") + event.event_index = InputMap.get_actions().find(action) _action_on_press.append(action) return _handle_input_event(event) -func simulate_action_release(action :String) -> GdUnitSceneRunner: +func simulate_action_release(action: String) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventAction.new() event.pressed = false event.action = action + if Engine.get_version_info().hex >= 0x40300: + @warning_ignore("unsafe_property_access") + event.event_index = InputMap.get_actions().find(action) _action_on_press.erase(action) return _handle_input_event(event) -func simulate_key_pressed(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: +@warning_ignore("return_value_discarded") +func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: simulate_key_press(key_code, shift_pressed, ctrl_pressed) + await _scene_tree().process_frame simulate_key_release(key_code, shift_pressed, ctrl_pressed) return self -func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: +func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventKey.new() event.pressed = true @@ -141,7 +160,7 @@ func simulate_key_press(key_code :int, shift_pressed := false, ctrl_pressed := f return _handle_input_event(event) -func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: +func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventKey.new() event.pressed = false @@ -155,7 +174,11 @@ func simulate_key_release(key_code :int, shift_pressed := false, ctrl_pressed := return _handle_input_event(event) -func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner: +func set_mouse_pos(pos: Vector2) -> GdUnitSceneRunner: + return set_mouse_position(pos) + + +func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner: var event := InputEventMouseMotion.new() event.position = pos event.global_position = get_global_mouse_position() @@ -165,7 +188,7 @@ func set_mouse_pos(pos :Vector2) -> GdUnitSceneRunner: func get_mouse_position() -> Vector2: if _last_input_event is InputEventMouse: - return _last_input_event.position + return (_last_input_event as InputEventMouse).position var current_scene := scene() if current_scene != null: return current_scene.get_viewport().get_mouse_position() @@ -173,19 +196,20 @@ func get_mouse_position() -> Vector2: func get_global_mouse_position() -> Vector2: - return Engine.get_main_loop().root.get_mouse_position() + return (Engine.get_main_loop() as SceneTree).root.get_mouse_position() -func simulate_mouse_move(pos :Vector2) -> GdUnitSceneRunner: +func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner: var event := InputEventMouseMotion.new() - event.position = pos - event.relative = pos - get_mouse_position() + event.position = position + event.relative = position - get_mouse_position() event.global_position = get_global_mouse_position() _apply_input_mouse_mask(event) _apply_input_modifiers(event) return _handle_input_event(event) +@warning_ignore("return_value_discarded") func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: var tween := _scene_tree().create_tween() _curent_mouse_position = get_mouse_position() @@ -199,6 +223,7 @@ func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_ty return self +@warning_ignore("return_value_discarded") func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: var tween := _scene_tree().create_tween() _curent_mouse_position = get_mouse_position() @@ -211,36 +236,166 @@ func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_ty return self -func simulate_mouse_button_pressed(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: - simulate_mouse_button_press(buttonIndex, double_click) - simulate_mouse_button_release(buttonIndex) +@warning_ignore("return_value_discarded") +func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: + simulate_mouse_button_press(button_index, double_click) + simulate_mouse_button_release(button_index) return self -func simulate_mouse_button_press(buttonIndex :MouseButton, double_click := false) -> GdUnitSceneRunner: +func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: var event := InputEventMouseButton.new() - event.button_index = buttonIndex + event.button_index = button_index event.pressed = true event.double_click = double_click _apply_input_mouse_position(event) _apply_input_mouse_mask(event) _apply_input_modifiers(event) - _mouse_button_on_press.append(buttonIndex) + _mouse_button_on_press.append(button_index) return _handle_input_event(event) -func simulate_mouse_button_release(buttonIndex :MouseButton) -> GdUnitSceneRunner: +func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner: var event := InputEventMouseButton.new() - event.button_index = buttonIndex + event.button_index = button_index event.pressed = false _apply_input_mouse_position(event) _apply_input_mouse_mask(event) _apply_input_modifiers(event) - _mouse_button_on_press.erase(buttonIndex) + _mouse_button_on_press.erase(button_index) return _handle_input_event(event) -func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: +@warning_ignore("return_value_discarded") +func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + simulate_screen_touch_press(index, position, double_tap) + simulate_screen_touch_release(index) + return self + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the touch the mouse events + set_mouse_pos(position) + simulate_mouse_button_press(MOUSE_BUTTON_LEFT) + # push touch press event at position + var event := InputEventScreenTouch.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = position + event.double_tap = double_tap + event.pressed = true + _current_scene.get_viewport().push_input(event) + # save current drag position by index + _current_touch_position[index] = position + return self + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the touch the mouse events + simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + # push touch release event at position + var event := InputEventScreenTouch.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = get_screen_touch_drag_position(index) + event.pressed = false + event.double_tap = (_last_input_event as InputEventScreenTouch).double_tap if _last_input_event is InputEventScreenTouch else double_tap + _current_scene.get_viewport().push_input(event) + return self + + +func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + var current_position: Vector2 = _current_touch_position[index] + return await _do_touch_drag_at(index, current_position + relative, time, trans_type) + + +func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + return await _do_touch_drag_at(index, position, time, trans_type) + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: + simulate_screen_touch_press(index, position) + return await _do_touch_drag_at(index, drop_position, time, trans_type) + + +@warning_ignore("return_value_discarded") +func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner: + if is_emulate_mouse_from_touch(): + simulate_mouse_move(position) + var event := InputEventScreenDrag.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = position + event.relative = _get_screen_touch_drag_position_or_default(index, position) - position + event.velocity = event.relative / _scene_tree().root.get_process_delta_time() + event.pressure = 1.0 + _current_touch_position[index] = position + _current_scene.get_viewport().push_input(event) + return self + + +func get_screen_touch_drag_position(index: int) -> Vector2: + if _current_touch_position.has(index): + return _current_touch_position[index] + push_error("No touch drag position for index '%d' is set!" % index) + return Vector2.ZERO + + +func is_emulate_mouse_from_touch() -> bool: + return ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch", true) + + +func _get_screen_touch_drag_position_or_default(index: int, default_position: Vector2) -> Vector2: + if _current_touch_position.has(index): + return _current_touch_position[index] + return default_position + + +@warning_ignore("return_value_discarded") +func _do_touch_drag_at(index: int, drag_position: Vector2, time: float, trans_type: Tween.TransitionType) -> GdUnitSceneRunner: + # start draging + var event := InputEventScreenDrag.new() + event.window_id = scene().get_window().get_window_id() + event.index = index + event.position = get_screen_touch_drag_position(index) + event.pressure = 1.0 + _current_touch_drag_position = event.position + + var tween := _scene_tree().create_tween() + tween.tween_property(self, "_current_touch_drag_position", drag_position, time).set_trans(trans_type) + tween.play() + + while not _current_touch_drag_position.is_equal_approx(drag_position): + if is_emulate_mouse_from_touch(): + # we need to simulate in addition to the drag the mouse move events + simulate_mouse_move(event.position) + # send touche drag event to new position + event.relative = _current_touch_drag_position - event.position + event.velocity = event.relative / _scene_tree().root.get_process_delta_time() + event.position = _current_touch_drag_position + _current_scene.get_viewport().push_input(event) + await _scene_tree().process_frame + + # finaly drop it + if is_emulate_mouse_from_touch(): + simulate_mouse_move(drag_position) + simulate_mouse_button_release(MOUSE_BUTTON_LEFT) + var touch_drop_event := InputEventScreenTouch.new() + touch_drop_event.window_id = event.window_id + touch_drop_event.index = event.index + touch_drop_event.position = drag_position + touch_drop_event.pressed = false + _current_scene.get_viewport().push_input(touch_drop_event) + await _scene_tree().process_frame + return self + + +func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner: _time_factor = min(9.0, time_factor) __activate_time_factor() __print("set time factor: %f" % _time_factor) @@ -248,7 +403,7 @@ func set_time_factor(time_factor := 1.0) -> GdUnitSceneRunner: return self -func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: +func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner: var time_shift_frames :int = max(1, frames / _time_factor) for frame in time_shift_frames: if delta_milli == -1: @@ -259,74 +414,73 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: func simulate_until_signal( - signal_name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> GdUnitSceneRunner: - var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + signal_name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> GdUnitSceneRunner: + var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000) return self func simulate_until_object_signal( - source :Object, - signal_name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> GdUnitSceneRunner: - var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + source: Object, + signal_name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> GdUnitSceneRunner: + var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) return self -func await_func(func_name :String, args := []) -> GdUnitFuncAssert: +func await_func(func_name: String, args := []) -> GdUnitFuncAssert: return GdUnitFuncAssertImpl.new(scene(), func_name, args) -func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFuncAssert: +func await_func_on(instance: Object, func_name: String, args := []) -> GdUnitFuncAssert: return GdUnitFuncAssertImpl.new(instance, func_name, args) -func await_signal(signal_name :String, args := [], timeout := 2000 ) -> void: +func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void: await _awaiter.await_signal_on(scene(), signal_name, args, timeout) -func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ) -> void: +func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void: await _awaiter.await_signal_on(source, signal_name, args, timeout) -# maximizes the window to bring the scene visible func maximize_view() -> GdUnitSceneRunner: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) DisplayServer.window_move_to_foreground() return self -func _property_exists(name :String) -> bool: +func _property_exists(name: String) -> bool: return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name) -func get_property(name :String) -> Variant: +func get_property(name: String) -> Variant: if not _property_exists(name): return "The property '%s' not exist checked loaded scene." % name return scene().get(name) -func set_property(name :String, value :Variant) -> bool: +func set_property(name: String, value: Variant) -> bool: if not _property_exists(name): push_error("The property named '%s' cannot be set, it does not exist!" % name) return false; @@ -335,24 +489,24 @@ func set_property(name :String, value :Variant) -> bool: func invoke( - name :String, - arg0 :Variant = NO_ARG, - arg1 :Variant = NO_ARG, - arg2 :Variant = NO_ARG, - arg3 :Variant = NO_ARG, - arg4 :Variant = NO_ARG, - arg5 :Variant = NO_ARG, - arg6 :Variant = NO_ARG, - arg7 :Variant = NO_ARG, - arg8 :Variant = NO_ARG, - arg9 :Variant = NO_ARG) -> Variant: - var args :Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) + name: String, + arg0: Variant = NO_ARG, + arg1: Variant = NO_ARG, + arg2: Variant = NO_ARG, + arg3: Variant = NO_ARG, + arg4: Variant = NO_ARG, + arg5: Variant = NO_ARG, + arg6: Variant = NO_ARG, + arg7: Variant = NO_ARG, + arg8: Variant = NO_ARG, + arg9: Variant = NO_ARG) -> Variant: + var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) if scene().has_method(name): return scene().callv(name, args) return "The method '%s' not exist checked loaded scene." % name -func find_child(name :String, recursive :bool = true, owned :bool = false) -> Node: +func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node: return scene().find_child(name, recursive, owned) @@ -377,37 +531,40 @@ func __deactivate_time_factor() -> void: # copy over current active modifiers -func _apply_input_modifiers(event :InputEvent) -> void: +func _apply_input_modifiers(event: InputEvent) -> void: if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers: - event.meta_pressed = event.meta_pressed or _last_input_event.meta_pressed - event.alt_pressed = event.alt_pressed or _last_input_event.alt_pressed - event.shift_pressed = event.shift_pressed or _last_input_event.shift_pressed - event.ctrl_pressed = event.ctrl_pressed or _last_input_event.ctrl_pressed + var last_input_event := _last_input_event as InputEventWithModifiers + var _event := event as InputEventWithModifiers + _event.meta_pressed = _event.meta_pressed or last_input_event.meta_pressed + _event.alt_pressed = _event.alt_pressed or last_input_event.alt_pressed + _event.shift_pressed = _event.shift_pressed or last_input_event.shift_pressed + _event.ctrl_pressed = _event.ctrl_pressed or last_input_event.ctrl_pressed # this line results into reset the control_pressed state!!! #event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap # copy over current active mouse mask and combine with curren mask -func _apply_input_mouse_mask(event :InputEvent) -> void: +func _apply_input_mouse_mask(event: InputEvent) -> void: # first apply last mask if _last_input_event is InputEventMouse and event is InputEventMouse: - event.button_mask |= _last_input_event.button_mask + (event as InputEventMouse).button_mask |= (_last_input_event as InputEventMouse).button_mask if event is InputEventMouseButton: - var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(event.get_button_index(), 0) - if event.is_pressed(): - event.button_mask |= button_mask + var _event := event as InputEventMouseButton + var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(_event.get_button_index(), 0) + if _event.is_pressed(): + _event.button_mask |= button_mask else: - event.button_mask ^= button_mask + _event.button_mask ^= button_mask # copy over last mouse position if need -func _apply_input_mouse_position(event :InputEvent) -> void: +func _apply_input_mouse_position(event: InputEvent) -> void: if _last_input_event is InputEventMouse and event is InputEventMouseButton: - event.position = _last_input_event.position + (event as InputEventMouseButton).position = (_last_input_event as InputEventMouse).position ## handle input action via Input modifieres -func _handle_actions(event :InputEventAction) -> bool: +func _handle_actions(event: InputEventAction) -> bool: if not InputMap.event_is_action(event, event.action, true): return false __print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()]) @@ -419,20 +576,23 @@ func _handle_actions(event :InputEventAction) -> bool: # for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work -func _handle_input_event(event :InputEvent) -> GdUnitSceneRunner: +@warning_ignore("return_value_discarded") +func _handle_input_event(event: InputEvent) -> GdUnitSceneRunner: if event is InputEventMouse: - Input.warp_mouse(event.position) + Input.warp_mouse((event as InputEventMouse).position as Vector2) Input.parse_input_event(event) if event is InputEventAction: - _handle_actions(event) + _handle_actions(event as InputEventAction) - Input.flush_buffered_events() var current_scene := scene() if is_instance_valid(current_scene): + # do not flush events if node processing disabled otherwise we run into errors at tree removed + if _current_scene.process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() __print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()]) if(current_scene.has_method("_gui_input")): - current_scene._gui_input(event) + (current_scene as Control)._gui_input(event) if(current_scene.has_method("_unhandled_input")): current_scene._unhandled_input(event) current_scene.get_viewport().set_input_as_handled() @@ -442,6 +602,7 @@ func _handle_input_event(event :InputEvent) -> GdUnitSceneRunner: return self +@warning_ignore("return_value_discarded") func _reset_input_to_default() -> void: # reset all mouse button to inital state if need for m_button :int in _mouse_button_on_press.duplicate(): @@ -459,11 +620,12 @@ func _reset_input_to_default() -> void: simulate_action_release(action) _action_on_press.clear() - Input.flush_buffered_events() + if is_instance_valid(_current_scene) and _current_scene.process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() _last_input_event = null -func __print(message :String) -> void: +func __print(message: String) -> void: if _verbose: prints(message) diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd index cbb6abc9..e4bf4781 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -14,8 +14,10 @@ const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes" const GROUP_TEST = COMMON_SETTINGS + "/test" const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds" const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" -const TEST_SITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" +const TEST_SUITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" const TEST_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery" +const TEST_FLAKY_CHECK = GROUP_TEST + "/flaky_check_enable" +const TEST_FLAKY_MAX_RETRIES = GROUP_TEST + "/flaky_max_retries" # Report Setiings @@ -81,7 +83,7 @@ const DEFAULT_TEST_TIMEOUT :int = 60*5 const DEFAULT_TEST_LOOKUP_FOLDER := "test" # help texts -const HELP_TEST_LOOKUP_FOLDER := "Sets the subfolder for the search/creation of test suites. (leave empty to use source folder)" +const HELP_TEST_LOOKUP_FOLDER := "Subfolder where test suites are located (or empty to use source folder directly)" enum NAMING_CONVENTIONS { AUTO_DETECT, @@ -90,29 +92,36 @@ enum NAMING_CONVENTIONS { } +const _VALUE_SET_SEPARATOR = "\f" # ASCII Form-feed character (AKA page break) + + static func setup() -> void: - create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Enables/Disables the update notification checked startup.") - create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Sets the server connection timeout in minutes.") - create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Sets the test case runtime timeout in seconds.") + create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Show notification if new gdUnit4 version is found") + # test settings + create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Server connection timeout in minutes") + create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Test case runtime timeout in seconds") create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER) - create_property_if_need(TEST_SITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Sets test-suite genrate script name convention.", NAMING_CONVENTIONS.keys()) - create_property_if_need(TEST_DISCOVER_ENABLED, false, "Enables/Disables the automatic detection of tests by finding tests in test lookup folders at runtime.") - create_property_if_need(REPORT_PUSH_ERRORS, false, "Enables/Disables report of push_error() as failure!") - create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Enables/Disables report of script errors as failure!") - create_property_if_need(REPORT_ORPHANS, true, "Enables/Disables orphan reporting.") - create_property_if_need(REPORT_ASSERT_ERRORS, true, "Enables/Disables error reporting checked asserts.") - create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Enables/Disables warning reporting checked asserts") - create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Enabled/disabled number values will be compared strictly by type. (real vs int)") + create_property_if_need(TEST_SUITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention to use when generating testsuites", NAMING_CONVENTIONS.keys()) + create_property_if_need(TEST_DISCOVER_ENABLED, false, "Automatically detect new tests in test lookup folders at runtime") + create_property_if_need(TEST_FLAKY_CHECK, false, "Rerun tests on failure and mark them as FLAKY") + create_property_if_need(TEST_FLAKY_MAX_RETRIES, 3, "Sets the number of retries for rerunning a flaky test") + # report settings + create_property_if_need(REPORT_PUSH_ERRORS, false, "Report push_error() as failure") + create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Report script errors as failure") + create_property_if_need(REPORT_ORPHANS, true, "Report orphaned nodes after tests finish") + create_property_if_need(REPORT_ASSERT_ERRORS, true, "Report assertion failures as errors") + create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Report assertion failures as warnings") + create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Compare number values strictly by type (real vs int)") # inspector create_property_if_need(INSPECTOR_NODE_COLLAPSE, true, - "Enables/Disables that the testsuite node is closed after a successful test run.") + "Close testsuite node after a successful test run.") create_property_if_need(INSPECTOR_TREE_VIEW_MODE, GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE, - "Sets the inspector panel presentation.", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys()) + "Inspector panel presentation mode", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys()) create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED, - "Sets the inspector panel presentation.", GdUnitInspectorTreeConstants.SORT_MODE.keys()) + "Inspector panel sorting mode", GdUnitInspectorTreeConstants.SORT_MODE.keys()) create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false, - "Shows/Hides the 'Run overall Tests' button in the inspector toolbar.") - create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Defines the test suite template") + "Show 'Run overall Tests' button in the inspector toolbar") + create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use") create_shortcut_properties_if_need() migrate_properties() @@ -129,17 +138,17 @@ static func migrate_properties() -> void: static func create_shortcut_properties_if_need() -> void: # inspector - create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun of the last tests performed.") - create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun of the last tests performed (Debug).") - create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug).") - create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stops the current test execution.") + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun the most recently executed tests") + create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun the most recently executed tests (Debug mode)") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug mode)") + create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stop the current test execution") # script editor - create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Runs the currently selected test.") - create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Runs the currently selected test (Debug).") - create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Creates a new test case for the currently selected function.") + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Run the currently selected test") + create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Run the currently selected test (Debug mode).") + create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Create a new test case for the currently selected function") # filesystem - create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file.") - create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Runs all test suites on the selected folder or file (Debug).") + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file") + create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file (Debug)") static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void: @@ -148,7 +157,7 @@ static func create_property_if_need(name :String, default :Variant, help :="", v ProjectSettings.set_setting(name, default) ProjectSettings.set_initial_value(name, default) - help += "" if value_set.is_empty() else " %s" % value_set + help = help if value_set.is_empty() else "%s%s%s" % [help, _VALUE_SET_SEPARATOR, value_set] set_help(name, default, help) @@ -175,6 +184,7 @@ static func is_update_notification_enabled() -> bool: static func set_update_notification(enable :bool) -> void: ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable) + @warning_ignore("return_value_discarded") ProjectSettings.save() @@ -185,6 +195,7 @@ static func get_log_path() -> String: static func set_log_path(path :String) -> void: ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true) ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path) + @warning_ignore("return_value_discarded") ProjectSettings.save() @@ -261,6 +272,14 @@ static func is_test_discover_enabled() -> bool: return get_setting(TEST_DISCOVER_ENABLED, false) +static func is_test_flaky_check_enabled() -> bool: + return get_setting(TEST_FLAKY_CHECK, false) + + +static func get_flaky_max_retries() -> int: + return get_setting(TEST_FLAKY_MAX_RETRIES, 3) + + static func set_test_discover_enabled(enable :bool) -> void: var property := get_property(TEST_DISCOVER_ENABLED) property.set_value(enable) @@ -271,29 +290,34 @@ static func is_log_enabled() -> bool: return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE) -static func list_settings(category :String) -> Array[GdUnitProperty]: - var settings :Array[GdUnitProperty] = [] +static func list_settings(category: String) -> Array[GdUnitProperty]: + var settings: Array[GdUnitProperty] = [] for property in ProjectSettings.get_property_list(): var property_name :String = property["name"] if property_name.begins_with(category): - var value :Variant = ProjectSettings.get_setting(property_name) - var default :Variant = ProjectSettings.property_get_revert(property_name) - var help :String = property["hint_string"] - var value_set := extract_value_set_from_help(help) - settings.append(GdUnitProperty.new(property_name, property["type"], value, default, help, value_set)) + settings.append(build_property(property_name, property)) return settings static func extract_value_set_from_help(value :String) -> PackedStringArray: + var split_value := value.split(_VALUE_SET_SEPARATOR) + if not split_value.size() > 1: + return PackedStringArray() + var regex := RegEx.new() + @warning_ignore("return_value_discarded") regex.compile("\\[(.+)\\]") - var matches := regex.search_all(value) + var matches := regex.search_all(split_value[1]) if matches.is_empty(): return PackedStringArray() - var values :String = matches[0].get_string(1) + var values: String = matches[0].get_string(1) return values.replacen(" ", "").replacen("\"", "").split(",", false) +static func extract_help_text(value :String) -> String: + return value.split(_VALUE_SET_SEPARATOR)[0] + + static func update_property(property :GdUnitProperty) -> Variant: var current_value :Variant = ProjectSettings.get_setting(property.name()) if current_value != property.value(): @@ -315,7 +339,7 @@ static func reset_property(property :GdUnitProperty) -> void: static func validate_property_value(property :GdUnitProperty) -> Variant: match property.name(): TEST_LOOKUP_FOLDER: - return validate_lookup_folder(property.value()) + return validate_lookup_folder(property.value_as_string()) _: return null @@ -349,14 +373,19 @@ static func get_property(name :String) -> GdUnitProperty: for property in ProjectSettings.get_property_list(): var property_name :String = property["name"] if property_name == name: - var value :Variant = ProjectSettings.get_setting(property_name) - var default :Variant = ProjectSettings.property_get_revert(property_name) - var help :String = property["hint_string"] - var value_set := extract_value_set_from_help(help) - return GdUnitProperty.new(property_name, property["type"], value, default, help, value_set) + return build_property(name, property) return null +static func build_property(property_name: String, property: Dictionary) -> GdUnitProperty: + var value: Variant = ProjectSettings.get_setting(property_name) + var value_type: int = property["type"] + var default: Variant = ProjectSettings.property_get_revert(property_name) + var help: String = property["hint_string"] + var value_set := extract_value_set_from_help(help) + return GdUnitProperty.new(property_name, value_type, value, default, extract_help_text(help), value_set) + + static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void: var property := get_property(old_property) if property == null: @@ -371,8 +400,10 @@ static func migrate_property(old_property :String, new_property :String, default static func dump_to_tmp() -> void: + @warning_ignore("return_value_discarded") ProjectSettings.save_custom("user://project_settings.godot") static func restore_dump_from_tmp() -> void: + @warning_ignore("return_value_discarded") DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot") diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd index b2bb8348..ccc013fb 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd @@ -41,12 +41,15 @@ func elapsed_time() -> float: func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant: # register checked signal to wait for + @warning_ignore("return_value_discarded") source.connect(signal_name, _on_signal_emmited) # install timeout timer + var scene_tree := Engine.get_main_loop() as SceneTree var timer := Timer.new() - Engine.get_main_loop().root.add_child(timer) + scene_tree.root.add_child(timer) timer.add_to_group("GdUnitTimers") timer.set_one_shot(true) + @warning_ignore("return_value_discarded") timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED) timer.start(_timeout_millis * 0.001 * Engine.get_time_scale()) @@ -61,12 +64,13 @@ func on_signal(source :Object, signal_name :String, expected_signal_args :Array) value = [value] if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args): break - await Engine.get_main_loop().process_frame + await scene_tree.process_frame source.disconnect(signal_name, _on_signal_emmited) _time_left = timer.time_left - await Engine.get_main_loop().process_frame - if value is Array and value.size() == 1: + await scene_tree.process_frame + @warning_ignore("unsafe_cast") + if value is Array and (value as Array).size() == 1: return value[0] return value diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd index 12e22b0e..10bacc4a 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -30,8 +30,8 @@ func register_emitter(emitter :Object) -> void: return _collected_signals[emitter] = Dictionary() # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. - if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): - emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) + if emitter is Node and !(emitter as Node).tree_exiting.is_connected(unregister_emitter): + (emitter as Node).tree_exiting.connect(unregister_emitter.bind(emitter)) # connect to all signals of the emitter we want to collect for signal_def in emitter.get_signal_list(): var signal_name :String = signal_def["name"] @@ -54,6 +54,7 @@ func unregister_emitter(emitter :Object) -> void: var signal_name :String = signal_def["name"] if emitter.is_connected(signal_name, _on_signal_emmited): emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + @warning_ignore("return_value_discarded") _collected_signals.erase(emitter) @@ -77,7 +78,8 @@ func _on_signal_emmited( var emitter :Object = signal_args.pop_back() #prints("_on_signal_emmited:", emitter, signal_name, signal_args) if is_signal_collecting(emitter, signal_name): - _collected_signals[emitter][signal_name].append(signal_args) + @warning_ignore("unsafe_cast") + (_collected_signals[emitter][signal_name] as Array).append(signal_args) func reset_received_signals(emitter :Object, signal_name: String, signal_args :Array) -> void: @@ -85,12 +87,14 @@ func reset_received_signals(emitter :Object, signal_name: String, signal_args :A if _collected_signals.has(emitter): var signals_by_emitter :Dictionary = _collected_signals[emitter] if signals_by_emitter.has(signal_name): - _collected_signals[emitter][signal_name].erase(signal_args) + @warning_ignore("unsafe_cast") + (_collected_signals[emitter][signal_name] as Array).erase(signal_args) #_debug_signal_list("after claer"); func is_signal_collecting(emitter :Object, signal_name :String) -> bool: - return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) + @warning_ignore("unsafe_cast") + return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name) func match(emitter :Object, signal_name :String, args :Array) -> bool: diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd index 652b7af6..b67d7a19 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -17,8 +17,6 @@ signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) @warning_ignore("unused_signal") signal gdunit_message(message :String) @warning_ignore("unused_signal") -signal gdunit_report(execution_context_id :int, report :GdUnitReport) -@warning_ignore("unused_signal") signal gdunit_set_test_failed(is_failed :bool) @warning_ignore("unused_signal") signal gdunit_settings_changed(property :GdUnitProperty) @@ -38,7 +36,10 @@ static func dispose() -> void: var signals := instance() # cleanup connected signals for signal_ in signals.get_signal_list(): - for connection in signals.get_signal_connection_list(signal_["name"]): - connection["signal"].disconnect(connection["callable"]) + @warning_ignore("unsafe_cast") + for connection in signals.get_signal_connection_list(signal_["name"] as StringName): + var _signal: Signal = connection["signal"] + var _callable: Callable = connection["callable"] + _signal.disconnect(_callable) signals = null Engine.remove_meta(META_KEY) diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd index ed6103e7..83a186f3 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -17,12 +17,14 @@ static func instance(name :String, clazz :Callable) -> Variant: return Engine.get_meta(name) var singleton :Variant = clazz.call() if is_instance_of(singleton, RefCounted): - push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, singleton.get_class()]) + @warning_ignore("unsafe_cast") + push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()]) return Engine.set_meta(name, singleton) GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton]) var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + @warning_ignore("return_value_discarded") singletons.append(name) Engine.set_meta(MEATA_KEY, singletons) return singleton @@ -36,6 +38,7 @@ static func unregister(p_singleton :String, use_call_deferred :bool = false) -> singletons.remove_at(index) var instance_ :Object = Engine.get_meta(p_singleton) GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) + @warning_ignore("return_value_discarded") GdUnitTools.free_instance(instance_, use_call_deferred) Engine.remove_meta(p_singleton) GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) @@ -44,7 +47,7 @@ static func unregister(p_singleton :String, use_call_deferred :bool = false) -> static func dispose(use_call_deferred :bool = false) -> void: # use a copy because unregister is modify the singletons array - var singletons := PackedStringArray(Engine.get_meta(MEATA_KEY, PackedStringArray())) + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) GdUnitTools.prints_verbose("----------------------------------------------------------------") GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons) for singleton in singletons: diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd index 6cd2ee4b..aabbac75 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -5,7 +5,9 @@ extends RefCounted static func create(source :Script, line_number :int) -> GdUnitResult: var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) # we need to save and close the testsuite and source if is current opened before modify + @warning_ignore("return_value_discarded") ScriptEditorControls.save_an_open_script(source.resource_path) + @warning_ignore("return_value_discarded") ScriptEditorControls.save_an_open_script(test_suite_path, true) if GdObjects.is_cs_script(source): return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd index 4ae3ea66..395958c3 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -30,8 +30,10 @@ func prescan_testsuite_classes() -> void: var base_class :String = script_meta["base"] var resource_path :String = script_meta["path"] if base_class == "GdUnitTestSuite": + @warning_ignore("return_value_discarded") _included_resources.append(resource_path) elif ClassDB.class_exists(base_class): + @warning_ignore("return_value_discarded") _excluded_resources.append(resource_path) @@ -54,6 +56,7 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N if exclude_scan_directories.has(dir.get_current_dir()): return collected_suites prints("Scanning for test suites in:", dir.get_current_dir()) + @warning_ignore("return_value_discarded") dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 var file_name := dir.get_next() while file_name != "": @@ -61,6 +64,7 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N if dir.current_is_dir(): var sub_dir := DirAccess.open(resource_path) if sub_dir != null: + @warning_ignore("return_value_discarded") _scan_test_suites(sub_dir, collected_suites) else: var time := LocalTime.now() @@ -91,7 +95,7 @@ func _parse_is_test_suite(resource_path :String) -> Node: return null # Check in the global class cache whether the GdUnitTestSuite class has been extended. if _included_resources.has(resource_path): - return _parse_test_suite(ResourceLoader.load(resource_path)) + return _parse_test_suite(GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path)) # Otherwise we need to scan manual, we need to exclude classes where direct extends form Godot classes # the resource loader can fail to load e.g. plugin classes with do preload other scripts @@ -100,10 +104,25 @@ func _parse_is_test_suite(resource_path :String) -> Node: if extends_from.is_empty() or ClassDB.class_exists(extends_from): return null # Finally, we need to load the class to determine it is a test suite - var script := ResourceLoader.load(resource_path) + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) if not GdObjects.is_test_suite(script): return null - return _parse_test_suite(ResourceLoader.load(resource_path)) + return _parse_test_suite(script) + + +# We load the test suites with disabled unsafe_method_access to avoid spamming loading errors +# `unsafe_method_access` will happen when using `assert_that` +static func load_with_disabled_warnings(resource_path: String) -> GDScript: + # grap current level + var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") + + # disable and load the script + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) + var script: GDScript = ResourceLoader.load(resource_path) + + # restore + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) + return script static func _is_script_format_supported(resource_path :String) -> bool: @@ -113,7 +132,7 @@ static func _is_script_format_supported(resource_path :String) -> bool: return GdUnit4CSharpApiLoader.is_csharp_file(resource_path) -func _parse_test_suite(script :Script) -> GdUnitTestSuite: +func _parse_test_suite(script: Script) -> GdUnitTestSuite: if not GdObjects.is_test_suite(script): return null @@ -122,61 +141,49 @@ func _parse_test_suite(script :Script) -> GdUnitTestSuite: return GdUnit4CSharpApiLoader.parse_test_suite(script.resource_path) # Do pares as GDScript - var test_suite :GdUnitTestSuite = script.new() + var test_suite: GdUnitTestSuite = (script as GDScript).new() test_suite.set_name(GdUnitTestSuiteScanner.parse_test_suite_name(script)) # add test cases to test suite and parse test case line nummber - var test_case_names := _extract_test_case_names(script) - _parse_and_add_test_cases(test_suite, script, test_case_names) - # not all test case parsed? - # we have to scan the base class to - if not test_case_names.is_empty(): - var base_script :GDScript = test_suite.get_script().get_base_script() - while base_script is GDScript: - # do not parse testsuite itself - if base_script.resource_path.find("GdUnitTestSuite") == -1: - _parse_and_add_test_cases(test_suite, base_script, test_case_names) - base_script = base_script.get_base_script() + var test_case_names := _extract_test_case_names(script as GDScript) + _parse_and_add_test_cases(test_suite, script as GDScript, test_case_names) return test_suite func _extract_test_case_names(script :GDScript) -> PackedStringArray: - var names := PackedStringArray() - for method in script.get_script_method_list(): - var funcName :String = method["name"] - if funcName.begins_with("test"): - names.append(funcName) - return names + return script.get_script_method_list()\ + .map(func(descriptor: Dictionary) -> String: return descriptor["name"])\ + .filter(func(func_name: String) -> bool: return func_name.begins_with("test")) static func parse_test_suite_name(script :Script) -> String: return script.resource_path.get_file().replace(".gd", "") -func _handle_test_suite_arguments(test_suite :Node, script :GDScript, fd :GdFunctionDescriptor) -> void: +func _handle_test_suite_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void: for arg in fd.args(): match arg.name(): _TestCase.ARGUMENT_SKIP: - var result :Variant = _expression_runner.execute(script, arg.value_as_string()) + var result: Variant = _expression_runner.execute(script, arg.plain_value()) if result is bool: test_suite.__is_skipped = result else: - push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string()) + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) _TestCase.ARGUMENT_SKIP_REASON: - test_suite.__skip_reason = arg.value_as_string() + test_suite.__skip_reason = arg.plain_value() _: push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path]) -func _handle_test_case_arguments(test_suite :Node, script :GDScript, fd :GdFunctionDescriptor) -> void: +func _handle_test_case_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void: var timeout := _TestCase.DEFAULT_TIMEOUT var iterations := Fuzzer.ITERATION_DEFAULT_COUNT var seed_value := -1 var is_skipped := false var skip_reason := "Unknown." - var fuzzers :Array[GdFunctionArgument] = [] + var fuzzers: Array[GdFunctionArgument] = [] var test := _TestCase.new() - for arg in fd.args(): + for arg: GdFunctionArgument in fd.args(): # verify argument is allowed # is test using fuzzers? if arg.type() == GdObjects.TYPE_FUZZER: @@ -186,31 +193,33 @@ func _handle_test_case_arguments(test_suite :Node, script :GDScript, fd :GdFunct _TestCase.ARGUMENT_TIMEOUT: timeout = arg.default() _TestCase.ARGUMENT_SKIP: - var result :Variant = _expression_runner.execute(script, arg.value_as_string()) + var result :Variant = _expression_runner.execute(script, arg.plain_value()) if result is bool: is_skipped = result else: - push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.value_as_string()) + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) _TestCase.ARGUMENT_SKIP_REASON: - skip_reason = arg.value_as_string() + skip_reason = arg.plain_value() Fuzzer.ARGUMENT_ITERATIONS: iterations = arg.default() Fuzzer.ARGUMENT_SEED: seed_value = arg.default() # create new test - test.configure(fd.name(), fd.line_number(), script.resource_path, timeout, fuzzers, iterations, seed_value) + @warning_ignore("return_value_discarded") + test.configure(fd.name(), fd.line_number(), fd.source_path(), timeout, fuzzers, iterations, seed_value) test.set_function_descriptor(fd) test.skip(is_skipped, skip_reason) _validate_argument(fd, test) test_suite.add_child(test) -func _parse_and_add_test_cases(test_suite :Node, script :GDScript, test_case_names :PackedStringArray) -> void: +func _parse_and_add_test_cases(test_suite: GdUnitTestSuite, script: GDScript, test_case_names: PackedStringArray) -> void: var test_cases_to_find := Array(test_case_names) var functions_to_scan := test_case_names.duplicate() + @warning_ignore("return_value_discarded") functions_to_scan.append("before") - var source := _script_parser.load_source_code(script, [script.resource_path]) - var function_descriptors := _script_parser.parse_functions(source, "", [script.resource_path], functions_to_scan) + + var function_descriptors := _script_parser.get_function_descriptors(script, functions_to_scan) for fd in function_descriptors: if fd.name() == "before": _handle_test_suite_arguments(test_suite, script, fd) @@ -231,7 +240,7 @@ func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void: # converts given file name by configured naming convention static func _to_naming_convention(file_name :String) -> String: - var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SITE_NAMING_CONVENTION, 0) + var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, 0) match nc: GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT: if GdObjects.is_snake_case(file_name): @@ -296,16 +305,15 @@ static func create_test_suite(test_suite_path :String, source_path :String) -> G static func get_test_case_line_number(resource_path :String, func_name :String) -> int: var file := FileAccess.open(resource_path, FileAccess.READ) if file != null: - var script_parser := GdScriptParser.new() var line_number := 0 while not file.eof_reached(): - var row := GdScriptParser.clean_up_row(file.get_line()) + var row := file.get_line() line_number += 1 # ignore comments and empty lines and not test functions - if row.begins_with("#") || row.length() == 0 || row.find("functest") == -1: + if row.begins_with("#") || row.length() == 0 || row.find("func test_") == -1: continue # abort if test case name found - if script_parser.parse_func_name(row) == "test_" + func_name: + if row.find("func") != -1 and row.find("test_" + func_name) != -1: return line_number return -1 @@ -328,7 +336,7 @@ func get_extends_classname(resource_path :String) -> String: static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult: - var script := load(resource_path) as GDScript + var script := load_with_disabled_warnings(resource_path) # count all exiting lines and add two as space to add new test case var line_number := count_lines(script) + 2 var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name) @@ -355,7 +363,7 @@ static func test_suite_exists(test_suite_path :String) -> bool: static func test_case_exists(test_suite_path :String, func_name :String) -> bool: if not test_suite_exists(test_suite_path): return false - var script := ResourceLoader.load(test_suite_path) as GDScript + var script := load_with_disabled_warnings(test_suite_path) for f in script.get_script_method_list(): if f["name"] == "test_" + func_name: return true diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index b3782716..2015fb8c 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -27,11 +27,13 @@ static func prints_verbose(message :String) -> void: prints(message) +@warning_ignore("unsafe_cast") static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool: if instance is Array: for element :Variant in instance: + @warning_ignore("return_value_discarded") free_instance(element) - instance.clear() + (instance as Array).clear() return true # do not free an already freed instance if not is_instance_valid(instance): @@ -41,37 +43,39 @@ static func free_instance(instance :Variant, use_call_deferred :bool = false, is return false if is_stdout_verbose: print_verbose("GdUnit4:gc():free instance ", instance) - release_double(instance) + release_double(instance as Object) if instance is RefCounted: (instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE) + # If scene runner freed we explicit await all inputs are processed + if instance is GdUnitSceneRunnerImpl: + await (instance as GdUnitSceneRunnerImpl).await_input_processed() return true else: - # is instance already freed? - #if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): - # return false - #release_connections(instance) if instance is Timer: - instance.stop() + var timer := instance as Timer + timer.stop() if use_call_deferred: - instance.call_deferred("free") + timer.call_deferred("free") else: - instance.free() - await Engine.get_main_loop().process_frame + timer.free() + await (Engine.get_main_loop() as SceneTree).process_frame return true - if instance is Node and instance.get_parent() != null: + + if instance is Node and (instance as Node).get_parent() != null: + var node := instance as Node if is_stdout_verbose: - print_verbose("GdUnit4:gc():remove node from parent ", instance.get_parent(), instance) + print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node) if use_call_deferred: - instance.get_parent().remove_child.call_deferred(instance) + node.get_parent().remove_child.call_deferred(node) #instance.call_deferred("set_owner", null) else: - instance.get_parent().remove_child(instance) + node.get_parent().remove_child(node) if is_stdout_verbose: print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance) if use_call_deferred: - instance.call_deferred("free") + (instance as Object).call_deferred("free") else: - instance.free() + (instance as Object).free() return !is_instance_valid(instance) @@ -92,13 +96,14 @@ static func _release_connections(instance :Object) -> void: static func release_timers() -> void: # we go the new way to hold all gdunit timers in group 'GdUnitTimers' - if Engine.get_main_loop().root == null: + var scene_tree := Engine.get_main_loop() as SceneTree + if scene_tree.root == null: return - for node :Node in Engine.get_main_loop().root.get_children(): + for node :Node in scene_tree.root.get_children(): if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): if is_instance_valid(node): - Engine.get_main_loop().root.remove_child.call_deferred(node) - node.stop() + scene_tree.root.remove_child.call_deferred(node) + (node as Timer).stop() node.queue_free() @@ -115,12 +120,6 @@ static func release_double(instance :Object) -> void: instance.call("__release_double") -static func clear_push_errors() -> void: - var runner :Node = Engine.get_meta("GdUnitRunner") - if runner != null: - runner.clear_push_errors() - - static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: - var test_case :Node = test_suite.find_child(test_case_name, false, false) + var test_case: _TestCase = test_suite.find_child(test_case_name, false, false) test_case.expect_to_interupt() diff --git a/addons/gdUnit4/src/core/LocalTime.gd b/addons/gdUnit4/src/core/LocalTime.gd index 64b9ba00..cf6c22c8 100644 --- a/addons/gdUnit4/src/core/LocalTime.gd +++ b/addons/gdUnit4/src/core/LocalTime.gd @@ -61,6 +61,7 @@ func plus(time_unit :TimeUnit, value :int) -> LocalTime: addValue = value * MILLIS_PER_MINUTE TimeUnit.HOUR: addValue = value * MILLIS_PER_HOUR + @warning_ignore("return_value_discarded") _init(_time + addValue) return self diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd index 31be7810..b8d9273d 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -22,7 +22,6 @@ var _expect_to_interupt := false var _timer: Timer var _interupted: bool = false var _failed := false -var _report: GdUnitReport = null var _parameter_set_resolver: GdUnitTestParameterSetResolver var _is_disposed := false @@ -80,14 +79,13 @@ func dispose() -> void: stop_timer() _remove_failure_handler() _fuzzers.clear() - _report = null @warning_ignore("shadowed_variable_base_class", "redundant_await") func _execute_test_case(name: String, test_parameter: Array) -> void: # needs at least on await otherwise it breaks the awaiting chain await get_parent().callv(name, test_parameter) - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame completed.emit() @@ -104,22 +102,30 @@ func set_timeout() -> void: _timer = Timer.new() add_child(_timer) _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) - _timer.timeout.connect(func do_interrupt() -> void: - if is_fuzzed(): - _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout")) - else: - _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout)) - _interupted = true - completed.emit() - , CONNECT_DEFERRED) + @warning_ignore("return_value_discarded") + _timer.timeout.connect(do_interrupt, CONNECT_DEFERRED) _timer.set_one_shot(true) _timer.set_wait_time(time) _timer.set_autostart(false) _timer.start() +func do_interrupt() -> void: + _interupted = true + if not is_expect_interupted(): + var execution_context:= GdUnitThreadManager.get_current_context().get_execution_context() + if is_fuzzed(): + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout"))) + else: + execution_context.add_report(GdUnitReport.new()\ + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout))) + completed.emit() + + func _set_failure_handler() -> void: if not GdUnitSignals.instance().gdunit_set_test_failed.is_connected(_failure_received): + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_set_test_failed.connect(_failure_received) @@ -164,10 +170,6 @@ func is_skipped() -> bool: return _skipped -func report() -> GdUnitReport: - return _report - - func skip_info() -> String: return _skip_reason diff --git a/addons/gdUnit4/src/core/assets/touch-button.png b/addons/gdUnit4/src/core/assets/touch-button.png new file mode 100644 index 00000000..23f46eff Binary files /dev/null and b/addons/gdUnit4/src/core/assets/touch-button.png differ diff --git a/addons/gdUnit4/src/core/assets/touch-button.png.import b/addons/gdUnit4/src/core/assets/touch-button.png.import new file mode 100644 index 00000000..a84ce13b --- /dev/null +++ b/addons/gdUnit4/src/core/assets/touch-button.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://btx5kcrsngasl" +path="res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gdUnit4/src/core/assets/touch-button.png" +dest_files=["res://.godot/imported/touch-button.png-2fff40c8520d8e97a57db1b2b043f641.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd index 339fb8cd..6f4c1251 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -50,6 +50,7 @@ static func instance() -> GdUnitCommandHandler: return GdUnitSingleton.instance("GdUnitCommandHandler", func() -> GdUnitCommandHandler: return GdUnitCommandHandler.new()) +@warning_ignore("return_value_discarded") func _init() -> void: assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING) @@ -58,6 +59,7 @@ func _init() -> void: GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) # preload previous test execution + @warning_ignore("return_value_discarded") _runner_config.load_config() init_shortcuts() @@ -75,7 +77,8 @@ func _init() -> void: # schedule discover tests if enabled and running inside the editor if Engine.is_editor_hint() and GdUnitSettings.is_test_discover_enabled(): - var timer :SceneTreeTimer = Engine.get_main_loop().create_timer(5) + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(5) + @warning_ignore("return_value_discarded") timer.timeout.connect(cmd_discover_tests) @@ -119,7 +122,7 @@ func init_shortcuts() -> void: register_shortcut(shortcut, inputEvent) -func create_shortcut_input_even(key_codes : PackedInt32Array) -> InputEventKey: +func create_shortcut_input_even(key_codes: PackedInt32Array) -> InputEventKey: var inputEvent :InputEventKey = InputEventKey.new() inputEvent.pressed = true for key_code in key_codes: @@ -204,7 +207,8 @@ func cmd_run(debug :bool) -> void: if _is_running: return # save current selected excution config - var result := _runner_config.set_server_port(Engine.get_meta("gdunit_server_port")).save_config() + var server_port: int = Engine.get_meta("gdunit_server_port") + var result := _runner_config.set_server_port(server_port).save_config() if result.is_error(): push_error(result.error_message()) return @@ -239,6 +243,7 @@ func cmd_editor_run_test(debug :bool) -> void: var cursor_line := active_base_editor().get_caret_line() #run test case? var regex := RegEx.new() + @warning_ignore("return_value_discarded") regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") var result := regex.search(active_base_editor().get_line(cursor_line)) if result: @@ -259,8 +264,10 @@ func cmd_create_test() -> void: # show error dialog push_error("Failed to create test case: %s" % result.error_message()) return - var info := result.value() as Dictionary - ScriptEditorControls.edit_script(info.get("path"), info.get("line")) + var info: Dictionary = result.value() + var script_path: String = info.get("path") + var script_line: int = info.get("line") + ScriptEditorControls.edit_script(script_path, script_line) func cmd_discover_tests() -> void: @@ -276,8 +283,10 @@ static func scan_test_directorys(base_directory :String, test_directory: String, if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): continue if match_test_directory(directory, test_directory): + @warning_ignore("return_value_discarded") test_suite_paths.append(current_directory) else: + @warning_ignore("return_value_discarded") scan_test_directorys(current_directory, test_directory, test_suite_paths) return test_suite_paths @@ -339,11 +348,13 @@ func _on_run_overall_pressed(_debug := false) -> void: func _on_settings_changed(property :GdUnitProperty) -> void: if SETTINGS_SHORTCUT_MAPPING.has(property.name()): var shortcut :GdUnitShortcut.ShortCut = SETTINGS_SHORTCUT_MAPPING.get(property.name()) - var input_event := create_shortcut_input_even(property.value()) + var value: PackedInt32Array = property.value() + var input_event := create_shortcut_input_even(value) prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()]) register_shortcut(shortcut, input_event) if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED: - var timer :SceneTreeTimer = Engine.get_main_loop().create_timer(3) + var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3) + @warning_ignore("return_value_discarded") timer.timeout.connect(cmd_discover_tests) diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd index 793d5144..438ddedf 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd @@ -8,6 +8,7 @@ var _discover_cache := {} func _init() -> void: # Register for discovery events to sync the cache + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_add_test_suite.connect(sync_cache) @@ -45,6 +46,7 @@ func discover(script: Script) -> void: var tests_removed := PackedStringArray() for test_case in discovered_test_cases: if not script_test_cases.has(test_case): + @warning_ignore("return_value_discarded") tests_removed.append(test_case) # second detect new added tests var tests_added :Array[String] = [] @@ -80,7 +82,8 @@ func extract_test_functions(test_suite :Node) -> PackedStringArray: func rebuild_project(script: Script) -> void: var class_path := ProjectSettings.globalize_path(script.resource_path) print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard: CSharpScript change detected on: '%s' [/color]" % class_path) - await Engine.get_main_loop().process_frame + var scene_tree := Engine.get_main_loop() as SceneTree + await scene_tree.process_frame var output := [] var exit_code := OS.execute("dotnet", ["--version"], output) @@ -95,4 +98,4 @@ func rebuild_project(script: Script) -> void: print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]") for out:Variant in output: print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) - await Engine.get_main_loop().process_frame + await scene_tree.process_frame diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd index 7e8dc81e..22b36d15 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd @@ -5,10 +5,11 @@ extends RefCounted static func run() -> void: prints("Running test discovery ..") GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) - await Engine.get_main_loop().create_timer(.5).timeout + await (Engine.get_main_loop() as SceneTree).create_timer(.5).timeout # We run the test discovery in an extra thread so that the main thread is not blocked var t:= Thread.new() + @warning_ignore("return_value_discarded") t.start(func () -> void: var test_suite_directories :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), []) var scanner := GdUnitTestSuiteScanner.new() @@ -18,7 +19,7 @@ static func run() -> void: _test_suites_to_process.append_array(scanner.scan(test_suite_dir)) # Do sync the main thread before emit the discovered test suites to the inspector - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame var test_case_count :int = 0 for test_suite in _test_suites_to_process: test_case_count += test_suite.get_child_count() @@ -32,6 +33,6 @@ static func run() -> void: ) # wait unblocked to the tread is finished while t.is_alive(): - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame # needs finally to wait for finish await t.wait_to_finish() diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd index 0f8ad7d1..0fd2af2f 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -3,6 +3,7 @@ extends Resource const WARNINGS = "warnings" const FAILED = "failed" +const FLAKY = "flaky" const ERRORS = "errors" const SKIPPED = "skipped" const ELAPSED_TIME = "elapsed_time" @@ -10,6 +11,7 @@ const ORPHAN_NODES = "orphan_nodes" const ERROR_COUNT = "error_count" const FAILED_COUNT = "failed_count" const SKIPPED_COUNT = "skipped_count" +const RETRY_COUNT = "retry_count" enum { INIT, @@ -18,6 +20,7 @@ enum { TESTSUITE_AFTER, TESTCASE_BEFORE, TESTCASE_AFTER, + TESTCASE_STATISTICS, DISCOVER_START, DISCOVER_END, DISCOVER_SUITE_ADDED, @@ -71,6 +74,15 @@ func test_after(p_resource_path :String, p_suite_name :String, p_test_name :Stri return self +func test_statistics(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}) -> GdUnitEvent: + _event_type = TESTCASE_STATISTICS + _resource_path = p_resource_path + _suite_name = p_suite_name + _test_name = p_test_name + _statistics = p_statistics + return self + + func type() -> int: return _event_type @@ -135,6 +147,10 @@ func is_error() -> bool: return _statistics.get(ERRORS, false) +func is_flaky() -> bool: + return _statistics.get(FLAKY, false) + + func is_skipped() -> bool: return _statistics.get(SKIPPED, false) @@ -170,7 +186,8 @@ func deserialize(serialized :Dictionary) -> GdUnitEvent: if serialized.has("reports"): # needs this workaround to copy typed values in the array var reports_to_deserializ :Array[Dictionary] = [] - reports_to_deserializ.append_array(serialized.get("reports")) + @warning_ignore("unsafe_cast") + reports_to_deserializ.append_array(serialized.get("reports") as Array) _reports = _deserialize_reports(reports_to_deserializ) return self diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd index 25d69b09..904da013 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -2,50 +2,65 @@ ## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor class_name GdUnitExecutionContext -var _parent_context :GdUnitExecutionContext -var _sub_context :Array[GdUnitExecutionContext] = [] -var _orphan_monitor :GdUnitOrphanNodesMonitor -var _memory_observer :GdUnitMemoryObserver -var _report_collector :GdUnitTestReportCollector -var _timer :LocalTime +var _parent_context: GdUnitExecutionContext +var _sub_context: Array[GdUnitExecutionContext] = [] +var _orphan_monitor: GdUnitOrphanNodesMonitor +var _memory_observer: GdUnitMemoryObserver +var _report_collector: GdUnitTestReportCollector +var _timer: LocalTime var _test_case_name: StringName -var _name :String - - -var error_monitor : GodotGdErrorMonitor = null: - set (value): - error_monitor = value +var _test_case_parameter_set: Array +var _name: String +var _test_execution_iteration: int = 0 +var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled() +var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries() + + +# execution states +var _is_calculated := false +var _is_success: bool +var _is_flaky: bool +var _is_skipped: bool +var _has_warnings: bool +var _has_failures: bool +var _has_errors: bool +var _failure_count := 0 +var _orphan_count := 0 +var _error_count := 0 +var _skipped_count := 0 + + +var error_monitor: GodotGdErrorMonitor = null: get: if _parent_context != null: return _parent_context.error_monitor + if error_monitor == null: + error_monitor = GodotGdErrorMonitor.new() return error_monitor -var test_suite : GdUnitTestSuite = null: - set (value): - test_suite = value +var test_suite: GdUnitTestSuite = null: get: if _parent_context != null: return _parent_context.test_suite return test_suite -var test_case : _TestCase = null: +var test_case: _TestCase = null: get: - if _test_case_name.is_empty(): - return null - return test_suite.find_child(_test_case_name, false, false) + if test_case == null and _parent_context != null: + return _parent_context.test_case + return test_case -func _init(name :String, parent_context :GdUnitExecutionContext = null) -> void: +func _init(name: StringName, parent_context: GdUnitExecutionContext = null) -> void: _name = name _parent_context = parent_context _timer = LocalTime.now() _orphan_monitor = GdUnitOrphanNodesMonitor.new(name) _orphan_monitor.start() _memory_observer = GdUnitMemoryObserver.new() - error_monitor = GodotGdErrorMonitor.new() - _report_collector = GdUnitTestReportCollector.new(get_instance_id()) + _report_collector = GdUnitTestReportCollector.new() if parent_context != null: parent_context._sub_context.append(self) @@ -58,40 +73,55 @@ func dispose() -> void: _parent_context = null test_suite = null test_case = null + dispose_sub_contexts() + + +func dispose_sub_contexts() -> void: for context in _sub_context: context.dispose() _sub_context.clear() -func set_active() -> void: - test_suite.__execution_context = self - GdUnitThreadManager.get_current_context().set_execution_context(self) +static func of(pe: GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context._test_execution_iteration = pe._test_execution_iteration + return context -static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext: - assert(test_suite_, "test_suite is null") - var context := GdUnitExecutionContext.new(test_suite_.get_name()) - context.test_suite = test_suite_ - context.set_active() +static func of_test_suite(p_test_suite: GdUnitTestSuite) -> GdUnitExecutionContext: + assert(p_test_suite, "test_suite is null") + var context := GdUnitExecutionContext.new(p_test_suite.get_name()) + context.test_suite = p_test_suite return context -static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext: +static func of_test_case(pe: GdUnitExecutionContext, p_test_case: _TestCase) -> GdUnitExecutionContext: + assert(p_test_case, "test_case is null") + var context := GdUnitExecutionContext.new(p_test_case.get_name(), pe) + context.test_case = p_test_case + return context + + +static func of_parameterized_test(pe: GdUnitExecutionContext, test_case_name: String, test_case_parameter_set: Array) -> GdUnitExecutionContext: var context := GdUnitExecutionContext.new(test_case_name, pe) context._test_case_name = test_case_name - context.set_active() + context._test_case_parameter_set = test_case_parameter_set return context -static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext: - var context := GdUnitExecutionContext.new(pe._test_case_name, pe) - context._test_case_name = pe._test_case_name - context.set_active() - return context +func get_test_suite_path() -> String: + return test_suite.get_script().resource_path + + +func get_test_suite_name() -> StringName: + return test_suite.get_name() -func test_failed() -> bool: - return has_failures() or has_errors() +func get_test_case_name() -> StringName: + if _test_case_name.is_empty(): + return test_case.get_name() + return _test_case_name func error_monitor_start() -> void: @@ -102,7 +132,7 @@ func error_monitor_stop() -> void: await error_monitor.scan() for error_report in error_monitor.to_reports(): if error_report.is_error(): - _report_collector._reports.append(error_report) + _report_collector.push_back(error_report) func orphan_monitor_start() -> void: @@ -113,45 +143,164 @@ func orphan_monitor_stop() -> void: _orphan_monitor.stop() +func add_report(report: GdUnitReport) -> void: + _report_collector.push_back(report) + + func reports() -> Array[GdUnitReport]: return _report_collector.reports() -func build_report_statistics(orphans :int, recursive := true) -> Dictionary: +func collect_reports(recursive: bool) -> Array[GdUnitReport]: + if not recursive: + return reports() + var current_reports := reports() + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + for sub_context in _sub_context: + current_reports.append_array(sub_context.reports()) + # needs finally to clean the test reports to avoid counting twice + sub_context.reports().clear() + return current_reports + + +func collect_orphans(p_reports: Array[GdUnitReport]) -> int: + var orphans := 0 + if not _sub_context.is_empty(): + orphans += collect_testcase_orphan_reports(_sub_context[0], p_reports) + orphans += collect_teststage_orphan_reports(p_reports) + return orphans + + +func collect_testcase_orphan_reports(context: GdUnitExecutionContext, p_reports: Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + p_reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) + return orphans + + +func collect_teststage_orphan_reports(p_reports: Array[GdUnitReport]) -> int: + var orphans := count_orphans() + if orphans > 0: + p_reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) + return orphans + + +func build_reports(recursive:= true) -> Array[GdUnitReport]: + var collected_reports: Array[GdUnitReport] = collect_reports(recursive) + if recursive: + _orphan_count = collect_orphans(collected_reports) + else: + _orphan_count = count_orphans() + if _orphan_count > 0: + collected_reports.push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(_orphan_count))) + _is_skipped = is_skipped() + _skipped_count = count_skipped(recursive) + _is_success = is_success() + _is_flaky = is_flaky() + _has_warnings = has_warnings() + _has_errors = has_errors() + _error_count = count_errors(recursive) + if !_is_success: + _has_failures = has_failures() + _failure_count = count_failures(recursive) + _is_calculated = true + return collected_reports + + +# Evaluates the actual test case status by validate latest execution state (cold be more based on flaky max retry count) +func evaluate_test_retry_status() -> bool: + # get latest test execution status + var last_test_status :GdUnitExecutionContext = _sub_context.back() + _is_skipped = last_test_status.is_skipped() + _skipped_count = last_test_status.count_skipped(false) + _is_success = last_test_status.is_success() + # if success but it have more than one sub contexts the test was rerurn becouse of failures and will be marked as flaky + _is_flaky = _is_success and _sub_context.size() > 1 + _has_warnings = last_test_status.has_warnings() + _has_errors = last_test_status.has_errors() + _error_count = last_test_status.count_errors(false) + _has_failures = last_test_status.has_failures() + _failure_count = last_test_status.count_failures(false) + _orphan_count = last_test_status.collect_orphans(collect_reports(false)) + _is_calculated = true + # finally cleanup the retry execution contexts + dispose_sub_contexts() + return _is_success + + +func get_execution_statistics() -> Dictionary: return { - GdUnitEvent.ORPHAN_NODES: orphans, + GdUnitEvent.RETRY_COUNT: _test_execution_iteration, + GdUnitEvent.ORPHAN_NODES: _orphan_count, GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(), - GdUnitEvent.FAILED: has_failures(), - GdUnitEvent.ERRORS: has_errors(), - GdUnitEvent.WARNINGS: has_warnings(), - GdUnitEvent.SKIPPED: has_skipped(), - GdUnitEvent.FAILED_COUNT: count_failures(recursive), - GdUnitEvent.ERROR_COUNT: count_errors(recursive), - GdUnitEvent.SKIPPED_COUNT: count_skipped(recursive) + GdUnitEvent.FAILED: !_is_success, + GdUnitEvent.ERRORS: _has_errors, + GdUnitEvent.WARNINGS: _has_warnings, + GdUnitEvent.FLAKY: _is_flaky, + GdUnitEvent.SKIPPED: _is_skipped, + GdUnitEvent.FAILED_COUNT: _failure_count, + GdUnitEvent.ERROR_COUNT: _error_count, + GdUnitEvent.SKIPPED_COUNT: _skipped_count } func has_failures() -> bool: - return _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c.has_failures()) or _report_collector.has_failures() + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c._has_failures if c._is_calculated else c.has_failures()) + or _report_collector.has_failures() + ) func has_errors() -> bool: - return _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c.has_errors()) or _report_collector.has_errors() + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c._has_errors if c._is_calculated else c.has_errors()) + or _report_collector.has_errors() + ) func has_warnings() -> bool: - return _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c.has_warnings()) or _report_collector.has_warnings() + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c._has_warnings if c._is_calculated else c.has_warnings()) + or _report_collector.has_warnings() + ) + + +func is_flaky() -> bool: + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c._is_flaky if c._is_calculated else c.is_flaky()) + or _test_execution_iteration > 1 + ) -func has_skipped() -> bool: - return _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c.has_skipped()) or _report_collector.has_skipped() +func is_success() -> bool: + if _sub_context.is_empty(): + return not has_failures() + var failed_context := _sub_context.filter(func(c :GdUnitExecutionContext) -> bool: + return !(c._is_success if c._is_calculated else c.is_success())) + return failed_context.is_empty() and not has_failures() -func count_failures(recursive :bool) -> int: + +func is_skipped() -> bool: + return ( + _sub_context.any(func(c :GdUnitExecutionContext) -> bool: + return c._is_skipped if c._is_calculated else c.is_skipped()) + or test_case.is_skipped() if test_case != null else false + ) + + +func is_interupted() -> bool: + return false if test_case == null else test_case.is_interupted() + + +func count_failures(recursive: bool) -> int: if not recursive: return _report_collector.count_failures() return _sub_context\ @@ -159,7 +308,7 @@ func count_failures(recursive :bool) -> int: return c.count_failures(recursive)).reduce(sum, _report_collector.count_failures()) -func count_errors(recursive :bool) -> int: +func count_errors(recursive: bool) -> int: if not recursive: return _report_collector.count_errors() return _sub_context\ @@ -167,7 +316,7 @@ func count_errors(recursive :bool) -> int: return c.count_errors(recursive)).reduce(sum, _report_collector.count_errors()) -func count_skipped(recursive :bool) -> int: +func count_skipped(recursive: bool) -> int: if not recursive: return _report_collector.count_skipped() return _sub_context\ @@ -182,11 +331,18 @@ func count_orphans() -> int: return _orphan_monitor.orphan_nodes() - orphans -func sum(accum :int, number :int) -> int: +func sum(accum: int, number: int) -> int: return accum + number -func register_auto_free(obj :Variant) -> Variant: +func retry_execution() -> bool: + var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries + if retry: + _test_execution_iteration += 1 + return retry + + +func register_auto_free(obj: Variant) -> Variant: return _memory_observer.register_auto_free(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd index 69b15f84..274b66f4 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -18,6 +18,7 @@ func register_auto_free(obj :Variant) -> Variant: if not is_instance_valid(obj): return obj # do not register on GDScriptNativeClass + @warning_ignore("unsafe_cast") if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : return obj #if obj is GDScript or obj is ScriptExtension: @@ -41,6 +42,7 @@ static func _is_instance_guard_enabled() -> bool: return false +@warning_ignore("unsafe_method_access") static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: if not _show_debug: return @@ -54,7 +56,7 @@ static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: prints(name, obj, obj.get_class(), obj.get_name()) -static func guard_instance(obj :Object) -> Object: +static func guard_instance(obj :Object) -> void: if not _is_instance_guard_enabled(): return var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) @@ -62,7 +64,6 @@ static func guard_instance(obj :Object) -> Object: return debug_observe("Gard on instance", obj) Engine.set_meta(tag, obj) - return obj static func unguard_instance(obj :Object, verbose := true) -> void: @@ -78,7 +79,7 @@ static func unguard_instance(obj :Object, verbose := true) -> void: static func gc_guarded_instance(name :String, instance :Object) -> void: if not _is_instance_guard_enabled(): return - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame unguard_instance(instance, false) if is_instance_valid(instance) and instance is RefCounted: # finally do this very hacky stuff @@ -90,8 +91,8 @@ static func gc_guarded_instance(name :String, instance :Object) -> void: # if base_script: # base_script.unreference() debug_observe(name, instance) - instance.unreference() - await Engine.get_main_loop().process_frame + (instance as RefCounted).unreference() + await (Engine.get_main_loop() as SceneTree).process_frame static func gc_on_guarded_instances() -> void: @@ -106,7 +107,7 @@ static func gc_on_guarded_instances() -> void: # store the object into global store aswell to be verified by 'is_marked_auto_free' func _tag_object(obj :Variant) -> void: - var tagged_object := Engine.get_meta(TAG_AUTO_FREE, []) as Array + var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, []) tagged_object.append(obj) Engine.set_meta(TAG_AUTO_FREE, tagged_object) @@ -116,16 +117,18 @@ func gc() -> void: if _store.is_empty(): return # give engine time to free objects to process objects marked by queue_free() - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame if _is_stdout_verbose: print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size()) - var tagged_objects := Engine.get_meta(TAG_AUTO_FREE, []) as Array + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) while not _store.is_empty(): var value :Variant = _store.pop_front() tagged_objects.erase(value) await GdUnitTools.free_instance(value, _is_stdout_verbose) + assert(_store.is_empty(), "The memory observer has still entries in the store!") ## Checks whether the specified object is registered for automatic release -static func is_marked_auto_free(obj :Object) -> bool: - return Engine.get_meta(TAG_AUTO_FREE, []).has(obj) +static func is_marked_auto_free(obj: Variant) -> bool: + var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, []) + return tagged_objects.has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd index 8484f0d1..62c4b02f 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -3,7 +3,6 @@ class_name GdUnitTestReportCollector extends RefCounted -var _execution_context_id :int var _reports :Array[GdUnitReport] = [] @@ -23,11 +22,6 @@ static func __filter_is_skipped(report :GdUnitReport) -> bool: return report.is_skipped() -func _init(execution_context_id :int) -> void: - _execution_context_id = execution_context_id - GdUnitSignals.instance().gdunit_report.connect(on_reports) - - func count_failures() -> int: return _reports.filter(__filter_is_failure).size() @@ -64,7 +58,5 @@ func reports() -> Array[GdUnitReport]: return _reports -# Consumes reports emitted by tests -func on_reports(execution_context_id :int, report :GdUnitReport) -> void: - if execution_context_id == _execution_context_id: - _reports.append(report) +func push_back(report :GdUnitReport) -> void: + _reports.push_back(report) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd index c9194694..0dd0ba0f 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -5,7 +5,7 @@ class_name GdUnitTestSuiteExecutor # preload all asserts here @warning_ignore("unused_private_class_variable") var _assertions := GdUnitAssertions.new() -var _executeStage :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new() +var _executeStage := GdUnitTestSuiteExecutionStage.new() func _init(debug_mode :bool = false) -> void: @@ -17,8 +17,8 @@ func execute(test_suite :GdUnitTestSuite) -> void: if not orphan_detection_enabled: prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") - Engine.get_main_loop().root.call_deferred("add_child", test_suite) - await Engine.get_main_loop().process_frame + (Engine.get_main_loop() as SceneTree).root.call_deferred("add_child", test_suite) + await (Engine.get_main_loop() as SceneTree).process_frame await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd index 50699752..9b78f8a6 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -4,16 +4,16 @@ class_name GdUnitTestCaseAfterStage extends IGdUnitExecutionStage -var _test_name :StringName = "" -var _call_stage :bool +var _call_stage: bool func _init(call_stage := true) -> void: _call_stage = call_stage -func _execute(context :GdUnitExecutionContext) -> void: +func _execute(context: GdUnitExecutionContext) -> void: var test_suite := context.test_suite + if _call_stage: @warning_ignore("redundant_await") await test_suite.after_test() @@ -21,69 +21,22 @@ func _execute(context :GdUnitExecutionContext) -> void: GdUnitThreadManager.get_current_context().set_assert(null) await context.gc() await context.error_monitor_stop() - if context.test_case.is_skipped(): - fire_test_skipped(context) - else: - fire_test_ended(context) - if is_instance_valid(context.test_case): - context.test_case.dispose() - - -func set_test_name(test_name :StringName) -> void: - _test_name = test_name - - -func fire_test_ended(context :GdUnitExecutionContext) -> void: - var test_suite := context.test_suite - var test_name := context._test_case_name if _test_name.is_empty() else _test_name - var reports := collect_reports(context) - var orphans := collect_orphans(context, reports) - - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports)) - -func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: - var orphans := 0 - if not context._sub_context.is_empty(): - orphans += add_orphan_report_test(context._sub_context[0], reports) - orphans += add_orphan_report_teststage(context, reports) - return orphans + var reports := context.build_reports() - -func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]: - var reports := context.reports() - var test_case := context.test_case - if test_case.is_interupted() and not test_case.is_expect_interupted() and test_case.report() != null: - reports.push_back(test_case.report()) - # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` - if not context._sub_context.is_empty(): - reports.append_array(context._sub_context[0].reports()) - # needs finally to clean the test reports to avoid counting twice - context._sub_context[0].reports().clear() - return reports - - -func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: - var orphans := context.count_orphans() - if orphans > 0: - reports.push_front(GdUnitReport.new()\ - .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) - return orphans - - -func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: - var orphans := context.count_orphans() - if orphans > 0: - reports.push_front(GdUnitReport.new()\ - .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) - return orphans + if context.is_skipped(): + fire_test_skipped(context) + else: + fire_event(GdUnitEvent.new() \ + .test_after(context.get_test_suite_path(), + context.get_test_suite_name(), + context.get_test_case_name(), + context.get_execution_statistics(), + reports)) -func fire_test_skipped(context :GdUnitExecutionContext) -> void: - var test_suite := context.test_suite +func fire_test_skipped(context: GdUnitExecutionContext) -> void: var test_case := context.test_case - var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name var statistics := { GdUnitEvent.ORPHAN_NODES: 0, GdUnitEvent.ELAPSED_TIME: 0, @@ -95,6 +48,11 @@ func fire_test_skipped(context :GdUnitExecutionContext) -> void: GdUnitEvent.SKIPPED: true, GdUnitEvent.SKIPPED_COUNT: 1, } - var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, [report])) + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new() \ + .test_after(context.get_test_suite_path(), + context.get_test_suite_name(), + context.get_test_case_name(), + statistics, + [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd index ebbd6d52..82e90d4a 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -3,8 +3,6 @@ class_name GdUnitTestCaseBeforeStage extends IGdUnitExecutionStage - -var _test_name :StringName = "" var _call_stage :bool @@ -14,16 +12,10 @@ func _init(call_stage := true) -> void: func _execute(context :GdUnitExecutionContext) -> void: var test_suite := context.test_suite - var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name)) - + .test_before(context.get_test_suite_path(), context.get_test_suite_name(), context.get_test_case_name())) if _call_stage: @warning_ignore("redundant_await") await test_suite.before_test() context.error_monitor_start() - - -func set_test_name(test_name :StringName) -> void: - _test_name = test_name diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd index 148d9af6..853070d5 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -16,6 +16,9 @@ var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterize @warning_ignore("redundant_await") func _execute(context :GdUnitExecutionContext) -> void: var test_case := context.test_case + + context.error_monitor_start() + if test_case.is_parameterized(): await _stage_parameterized_test.execute(context) elif test_case.is_fuzzed(): @@ -23,6 +26,20 @@ func _execute(context :GdUnitExecutionContext) -> void: else: await _stage_single_test.execute(context) + await context.gc() + await context.error_monitor_stop() + + # finally fire test statistics report + fire_event(GdUnitEvent.new()\ + .test_statistics(context.get_test_suite_path(), + context.get_test_suite_name(), + context.get_test_case_name(), + context.get_execution_statistics())) + + # finally free the test instance + if is_instance_valid(context.test_case): + context.test_case.dispose() + func set_debug_mode(debug_mode :bool = false) -> void: super.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd index a6de3187..08bf3e56 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -15,14 +15,15 @@ func _execute(context :GdUnitExecutionContext) -> void: # unreference last used assert form the test to prevent memory leaks GdUnitThreadManager.get_current_context().set_assert(null) await context.gc() - - var reports := context.reports() - var orphans := context.count_orphans() - if orphans > 0: - reports.push_front(GdUnitReport.new() \ - .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) - fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports)) + var reports := context.build_reports(false) + fire_event(GdUnitEvent.new()\ + .suite_after(context.get_test_suite_path(),\ + test_suite.get_name(), + context.get_execution_statistics(), + reports)) GdUnitFileAccess.clear_tmp() # Guard that checks if all doubled (spy/mock) objects are released GdUnitClassDoubler.check_leaked_instances() + # we hide the scene/main window after runner is finished + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd index 2260edb3..e9fa7186 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -8,7 +8,7 @@ func _execute(context :GdUnitExecutionContext) -> void: var test_suite := context.test_suite fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count())) + .suite_before(context.get_test_suite_path(), test_suite.get_name(), test_suite.get_child_count())) @warning_ignore("redundant_await") await test_suite.before() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd index ba223910..5aad57d7 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -19,6 +19,7 @@ func _execute(context :GdUnitExecutionContext) -> void: if context.test_suite.__is_skipped: await fire_test_suite_skipped(context) else: + @warning_ignore("return_value_discarded") GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) await _stage_before.execute(context) for test_case_index in context.test_suite.get_child_count(): @@ -27,9 +28,9 @@ func _execute(context :GdUnitExecutionContext) -> void: if not is_instance_valid(test_case): continue context.test_suite.set_active_test_case(test_case.get_name()) - await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name())) + await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case)) # stop on first error or if fail fast is enabled - if _fail_fast and context.test_failed(): + if _fail_fast and not context.is_success(): break if test_case.is_interupted(): # it needs to go this hard way to kill the outstanding awaits of a test case when the test timed out @@ -38,14 +39,14 @@ func _execute(context :GdUnitExecutionContext) -> void: context.test_suite = await clone_test_suite(context.test_suite) await _stage_after.execute(context) GdUnitMemoryObserver.unguard_instance(context.test_suite.__awaiter) - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame context.test_suite.free() context.dispose() # clones a test suite and moves the test cases to new instance func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame dispose_timers(test_suite) await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) var parent := test_suite.get_parent() @@ -56,10 +57,11 @@ func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: test_suite.remove_child(child) _test_suite.add_child(child) parent.add_child(_test_suite) + @warning_ignore("return_value_discarded") GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) # finally free current test suite instance test_suite.free() - await Engine.get_main_loop().process_frame + await (Engine.get_main_loop() as SceneTree).process_frame return _test_suite @@ -67,7 +69,7 @@ func dispose_timers(test_suite :GdUnitTestSuite) -> void: GdUnitTools.release_timers() for child in test_suite.get_children(): if child is Timer: - child.stop() + (child as Timer).stop() test_suite.remove_child(child) child.free() @@ -86,7 +88,7 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: var test_suite := context.test_suite var skip_count := test_suite.get_child_count() fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count)) + .suite_before(context.get_test_suite_path(), test_suite.get_name(), skip_count)) var statistics := { GdUnitEvent.ORPHAN_NODES: 0, GdUnitEvent.ELAPSED_TIME: 0, @@ -99,8 +101,8 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: GdUnitEvent.SKIPPED: true } var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, -1, GdAssertMessages.test_suite_skipped(test_suite.__skip_reason, skip_count)) - fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, [report])) - await Engine.get_main_loop().process_frame + fire_event(GdUnitEvent.new().suite_after(context.get_test_suite_path(), test_suite.get_name(), statistics, [report])) + await (Engine.get_main_loop() as SceneTree).process_frame func set_debug_mode(debug_mode :bool = false) -> void: diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd index 0f6ae93a..39de3809 100644 --- a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -14,7 +14,7 @@ var _debug_mode := false ## await MyExecutionStage.new().execute() ## [/codeblock][br] func execute(context :GdUnitExecutionContext) -> void: - context.set_active() + GdUnitThreadManager.get_current_context().set_execution_context(context) @warning_ignore("redundant_await") await _execute(context) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd index d438b57c..243b889b 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -8,10 +8,16 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() func _execute(context :GdUnitExecutionContext) -> void: - await _stage_before.execute(context) - if not context.test_case.is_skipped(): - await _stage_test.execute(GdUnitExecutionContext.of(context)) - await _stage_after.execute(context) + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not context.test_case.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + @warning_ignore("return_value_discarded") + context.evaluate_test_retry_status() func set_debug_mode(debug_mode :bool = false) -> void: diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd index e6d98521..d32c28ed 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -15,6 +15,7 @@ func _execute(context :GdUnitExecutionContext) -> void: # guard on fuzzers for fuzzer in fuzzers: + @warning_ignore("return_value_discarded") GdUnitMemoryObserver.guard_instance(fuzzer) for iteration in test_case.iterations(): @@ -46,7 +47,8 @@ func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[ test_case.generate_seed() var fuzzers :Array[Fuzzer] = [] for fuzzer_arg in test_case.fuzzer_arguments(): - var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script(), fuzzer_arg.value_as_string()) + @warning_ignore("unsafe_cast") + var fuzzer := _expression_runner.to_fuzzer(test_suite.get_script() as GDScript, fuzzer_arg.plain_value() as String) fuzzer._iteration_index = 0 fuzzer._iteration_limit = test_case.iterations() fuzzers.append(fuzzer) diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd new file mode 100644 index 00000000..5de35c07 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd @@ -0,0 +1,10 @@ +class_name GdUnitTestCaseParameterSetTestStage +extends IGdUnitExecutionStage + + +## Executes a parameterized test case 'test_()' by given parameters.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context: GdUnitExecutionContext) -> void: + await context.test_case.execute_paramaterized(context._test_case_parameter_set) + await context.gc() diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd index 99d616e4..820a9d9f 100644 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd @@ -4,6 +4,7 @@ extends IGdUnitExecutionStage var _stage_before: IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +var _stage_test: IGdUnitExecutionStage = GdUnitTestCaseParameterSetTestStage.new() ## Executes a parameterized test case.[br] @@ -12,9 +13,6 @@ var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() func _execute(context: GdUnitExecutionContext) -> void: var test_case := context.test_case var test_parameter_index := test_case.test_parameter_index() - var is_fail := false - var is_error := false - var failing_index := 0 var parameter_set_resolver := test_case.parameter_set_resolver() var test_names := parameter_set_resolver.build_test_case_names(test_case) @@ -28,44 +26,55 @@ func _execute(context: GdUnitExecutionContext) -> void: if test_parameter_index != -1 and test_parameter_index != parameter_set_index: continue var current_test_case_name := test_names[parameter_set_index] - _stage_before.set_test_name(current_test_case_name) - _stage_after.set_test_name(current_test_case_name) + var test_case_parameter_set: Array + if parameter_set_resolver.is_parameter_set_static(parameter_set_index): + test_case_parameter_set = parameter_sets[parameter_set_index] var test_context := GdUnitExecutionContext.of(context) - await _stage_before.execute(test_context) - var current_parameter_set :Array - if parameter_set_resolver.is_parameter_set_static(parameter_set_index): - current_parameter_set = parameter_sets[parameter_set_index] - else: - current_parameter_set = _load_parameter_set(context, parameter_set_index) - if not test_case.is_interupted(): - await test_case.execute_paramaterized(current_parameter_set) - await _stage_after.execute(test_context) - # we need to clean up the reports here so they are not reported twice - is_fail = is_fail or test_context.count_failures(false) > 0 - is_error = is_error or test_context.count_errors(false) > 0 - failing_index = parameter_set_index - 1 - test_context.reports().clear() + test_context._test_case_name = current_test_case_name + var has_errors := false + while test_context.retry_execution(): + var retry_test_context := GdUnitExecutionContext.of(test_context) + + retry_test_context._test_case_name = current_test_case_name + await _stage_before.execute(retry_test_context) + if not test_case.is_interupted(): + # we need to load paramater set at execution level after the before stage to get the actual variables from the current test + if not parameter_set_resolver.is_parameter_set_static(parameter_set_index): + test_case_parameter_set = _load_parameter_set(context, parameter_set_index) + await _stage_test.execute(GdUnitExecutionContext.of_parameterized_test(retry_test_context, current_test_case_name, test_case_parameter_set)) + await _stage_after.execute(retry_test_context) + has_errors = retry_test_context.has_errors() + if retry_test_context.is_success() or retry_test_context.is_skipped() or retry_test_context.is_interupted(): + break + + var is_success := test_context.evaluate_test_retry_status() + report_test_failure(context, !is_success, has_errors, parameter_set_index) + if test_case.is_interupted(): break - # add report to parent execution context if failed or an error is found - if is_fail: - context.reports().append(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % failing_index)) - if is_error: - context.reports().append(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % failing_index)) await context.gc() +func report_test_failure(test_context: GdUnitExecutionContext, is_failed: bool, has_errors: bool, parameter_set_index: int) -> void: + var test_case := test_context.test_case + + if is_failed: + test_context.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % parameter_set_index)) + if has_errors: + test_context.add_report(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % parameter_set_index)) + + func _load_parameter_set(context: GdUnitExecutionContext, parameter_set_index: int) -> Array: var test_case := context.test_case - var test_suite := context.test_suite # we need to exchange temporary for parameter resolving the execution context # this is necessary because of possible usage of `auto_free` and needs to run in the parent execution context - var save_execution_context: GdUnitExecutionContext = test_suite.__execution_context - context.set_active() + var thread_context := GdUnitThreadManager.get_current_context() + var save_execution_context := thread_context.get_execution_context() + thread_context.set_execution_context(context) var parameters := test_case.load_parameter_sets() # restore the original execution context and restart the orphan monitor to get new instances into account - save_execution_context.set_active() + thread_context.set_execution_context(save_execution_context) save_execution_context.orphan_monitor_start() return parameters[parameter_set_index] @@ -74,3 +83,4 @@ func set_debug_mode(debug_mode: bool=false) -> void: super.set_debug_mode(debug_mode) _stage_before.set_debug_mode(debug_mode) _stage_after.set_debug_mode(debug_mode) + _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd index b54d6a55..775b8dbc 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -9,10 +9,16 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() func _execute(context :GdUnitExecutionContext) -> void: - await _stage_before.execute(context) - if not context.test_case.is_skipped(): - await _stage_test.execute(GdUnitExecutionContext.of(context)) - await _stage_after.execute(context) + while context.retry_execution(): + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + if not test_context.is_skipped(): + await _stage_test.execute(GdUnitExecutionContext.of(test_context)) + await _stage_after.execute(test_context) + if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): + break + @warning_ignore("return_value_discarded") + context.evaluate_test_retry_status() func set_debug_mode(debug_mode :bool = false) -> void: diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd index 87b1aeb5..fc83742c 100644 --- a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd @@ -3,7 +3,6 @@ extends RefCounted var _name :String -var _parent :GdClassDescriptor = null var _is_inner_class :bool var _functions :Array[GdFunctionDescriptor] @@ -14,18 +13,10 @@ func _init(p_name :String, p_is_inner_class :bool, p_functions :Array[GdFunction _functions = p_functions -func set_parent_clazz(p_parent :GdClassDescriptor) -> void: - _parent = p_parent - - func name() -> String: return _name -func parent() -> GdClassDescriptor: - return _parent - - func is_inner_class() -> bool: return _is_inner_class diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd index b1cc38a4..33022fd8 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -64,11 +64,11 @@ func _on_type_StringName(value :StringName) -> String: return 'StringName("%s")' % value -func _on_type_Object(value :Object, _type :int) -> String: +func _on_type_Object(value: Variant, _type: int) -> String: return str(value) -func _on_type_Color(color :Color) -> String: +func _on_type_Color(color: Color) -> String: if color == Color.BLACK: return "Color()" return "Color%s" % color @@ -101,7 +101,8 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_COLOR_ARRAY: var colors := PackedStringArray() - for color in value as PackedColorArray: + for color: Color in value: + @warning_ignore("return_value_discarded") colors.append(_on_type_Color(color)) if colors.is_empty(): return "PackedColorArray()" @@ -109,7 +110,8 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_VECTOR2_ARRAY: var vectors := PackedStringArray() - for vector in value as PackedVector2Array: + for vector: Vector2 in value: + @warning_ignore("return_value_discarded") vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) if vectors.is_empty(): return "PackedVector2Array()" @@ -117,7 +119,8 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_VECTOR3_ARRAY: var vectors := PackedStringArray() - for vector in value as PackedVector3Array: + for vector: Vector3 in value: + @warning_ignore("return_value_discarded") vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) if vectors.is_empty(): return "PackedVector3Array()" @@ -125,7 +128,8 @@ func _on_type_Array(value :Variant, type :int) -> String: GdObjects.TYPE_PACKED_VECTOR4_ARRAY: var vectors := PackedStringArray() - for vector:Variant in value as Array: + for vector: Vector4 in value: + @warning_ignore("return_value_discarded") vectors.append(_on_type_Vector(vector, TYPE_VECTOR4)) if vectors.is_empty(): return "PackedVector4Array()" @@ -133,7 +137,8 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_STRING_ARRAY: var values := PackedStringArray() - for v in value as PackedStringArray: + for v: String in value: + @warning_ignore("return_value_discarded") values.append('"%s"' % v) if values.is_empty(): return "PackedStringArray()" @@ -145,7 +150,8 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_INT32_ARRAY,\ TYPE_PACKED_INT64_ARRAY: var vectors := PackedStringArray() - for vector :Variant in value as Array: + for vector :Variant in value: + @warning_ignore("return_value_discarded") vectors.append(str(vector)) if vectors.is_empty(): return GdObjects.type_as_string(type) + "()" @@ -239,11 +245,16 @@ func _on_type_Basis(basis :Basis) -> String: return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] +@warning_ignore("unsafe_cast") static func decode(value :Variant) -> String: var type := typeof(value) - if GdArrayTools.is_type_array(type) and value.is_empty(): + if GdArrayTools.is_type_array(type) and (value as Array).is_empty(): return "" - var decoder :Callable = instance("GdUnitDefaultValueDecoders", func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()).get_decoder(type) + var decoder :Callable = ( + instance("GdUnitDefaultValueDecoders", + func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new() + ) as GdDefaultValueDecoder + ).get_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" @@ -252,10 +263,15 @@ static func decode(value :Variant) -> String: return decoder.call(value) +@warning_ignore("unsafe_cast") static func decode_typed(type :int, value :Variant) -> String: if value == null: return "null" - var decoder :Callable = instance("GdUnitDefaultValueDecoders", func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new()).get_decoder(type) + var decoder: Callable = ( + instance("GdUnitDefaultValueDecoders", + func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new() + ) as GdDefaultValueDecoder + ).get_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd index 57891860..190d9461 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -2,23 +2,39 @@ class_name GdFunctionArgument extends RefCounted -var _cleanup_leading_spaces := RegEx.create_from_string("(?m)^[ \t]+") -var _fix_comma_space := RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""") +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const UNDEFINED: String = "<-NO_ARG->" +const ARG_PARAMETERIZED_TEST := "test_parameters" + +static var _fuzzer_regex: RegEx +static var _cleanup_leading_spaces: RegEx +static var _fix_comma_space: RegEx + var _name: String var _type: int -var _default_value :Variant -var _parameter_sets :PackedStringArray = [] - -const UNDEFINED :Variant = "<-NO_ARG->" -const ARG_PARAMETERIZED_TEST := "test_parameters" +var _type_hint: int +var _default_value: Variant +var _parameter_sets: PackedStringArray = [] -func _init(p_name :String, p_type :int = TYPE_MAX, value :Variant = UNDEFINED) -> void: +func _init(p_name: String, p_type: int, value: Variant = UNDEFINED, p_type_hint: int = TYPE_NIL) -> void: + _init_static_variables() _name = p_name _type = p_type - if p_name == ARG_PARAMETERIZED_TEST: - _parameter_sets = _parse_parameter_set(value) + _type_hint = p_type_hint + if value != null and p_name == ARG_PARAMETERIZED_TEST: + _parameter_sets = _parse_parameter_set(str(value)) _default_value = value + # is argument a fuzzer? + if _type == TYPE_OBJECT and _fuzzer_regex.search(_name): + _type = GdObjects.TYPE_FUZZER + + +func _init_static_variables() -> void: + if _fuzzer_regex == null: + _fuzzer_regex = GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)") + _cleanup_leading_spaces = RegEx.create_from_string("(?m)^[ \t]+") + _fix_comma_space = RegEx.create_from_string(""", {0,}\t{0,}(?=(?:[^"]*"[^"]*")*[^"]*$)(?!\\s)""") func name() -> String: @@ -29,20 +45,58 @@ func default() -> Variant: return GodotVersionFixures.convert(_default_value, _type) +func set_value(value: String) -> void: + # we onle need to apply default values for Objects, all others are provided by the method descriptor + if _type == GdObjects.TYPE_FUZZER: + _default_value = value + return + if _name == ARG_PARAMETERIZED_TEST: + _parameter_sets = _parse_parameter_set(value) + _default_value = value + return + + if _type == TYPE_NIL or _type == GdObjects.TYPE_VARIANT: + _type = _extract_value_type(value) + _default_value = value + if _default_value == null: + _default_value = value + + +func _extract_value_type(value: String) -> int: + if value != UNDEFINED: + if _fuzzer_regex.search(_name): + return GdObjects.TYPE_FUZZER + if value.rfind(")") == value.length()-1: + return GdObjects.TYPE_FUNC + return _type + + func value_as_string() -> String: if has_default(): - return str(_default_value) + return GdDefaultValueDecoder.decode_typed(_type, _default_value) return "" +func plain_value() -> Variant: + return _default_value + + func type() -> int: return _type +func type_hint() -> int: + return _type_hint + + func has_default() -> bool: return not is_same(_default_value, UNDEFINED) +func is_typed_array() -> bool: + return _type == TYPE_ARRAY and _type_hint != TYPE_NIL + + func is_parameter_set() -> bool: return _name == ARG_PARAMETERIZED_TEST @@ -60,10 +114,12 @@ static func get_parameter_set(parameters :Array[GdFunctionArgument]) -> GdFuncti func _to_string() -> String: var s := _name - if _type != TYPE_MAX: + if _type != TYPE_NIL: s += ":" + GdObjects.type_as_string(_type) - if _default_value != UNDEFINED: - s += "=" + str(_default_value) + if _type_hint != TYPE_NIL: + s += "[%s]" % GdObjects.type_as_string(_type_hint) + if typeof(_default_value) != TYPE_STRING: + s += "=" + value_as_string() return s @@ -85,6 +141,7 @@ func _parse_parameter_set(input :String) -> PackedStringArray: for c in buf: current_index += 1 matched = current_index == buf.size() + @warning_ignore("return_value_discarded") collected_characters.push_back(c) match c: @@ -108,6 +165,7 @@ func _parse_parameter_set(input :String) -> PackedStringArray: if matched: var parameters := _fix_comma_space.sub(collected_characters.get_string_from_utf8(), ", ", true) if not parameters.is_empty(): + @warning_ignore("return_value_discarded") output.append(parameters) collected_characters.clear() matched = false diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd index 2633016e..27a21b2f 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -6,6 +6,7 @@ var _is_static :bool var _is_engine :bool var _is_coroutine :bool var _name :String +var _source_path: String var _line_number :int var _return_type :int var _return_class :String @@ -13,6 +14,18 @@ var _args : Array[GdFunctionArgument] var _varargs :Array[GdFunctionArgument] + +static func create(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, false, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + +static func create_static(p_name: String, p_source_path: String, p_source_line: int, p_return_type: int, p_args: Array[GdFunctionArgument] = []) -> GdFunctionDescriptor: + var fd := GdFunctionDescriptor.new(p_name, p_source_line, false, true, false, p_return_type, "", p_args) + fd.enrich_file_info(p_source_path, p_source_line) + return fd + + func _init(p_name :String, p_line_number :int, p_is_virtual :bool, @@ -34,10 +47,19 @@ func _init(p_name :String, _varargs = p_varargs +func with_return_class(clazz_name: String) -> GdFunctionDescriptor: + _return_class = clazz_name + return self + + func name() -> String: return _name +func source_path() -> String: + return _source_path + + func line_number() -> int: return _line_number @@ -74,7 +96,7 @@ func is_private() -> bool: return name().begins_with("_") and not is_virtual() -func return_type() -> Variant: +func return_type() -> int: return _return_type @@ -84,42 +106,34 @@ func return_type_as_string() -> String: return GdObjects.type_as_string(return_type()) -func args() -> Array[GdFunctionArgument]: - return _args +@warning_ignore("unsafe_cast") +func set_argument_value(arg_name: String, value: String) -> void: + ( + _args.filter(func(arg: GdFunctionArgument) -> bool: return arg.name() == arg_name)\ + .front() as GdFunctionArgument + ).set_value(value) -func varargs() -> Array[GdFunctionArgument]: - return _varargs +func enrich_file_info(p_source_path: String, p_line_number: int) -> void: + _source_path = p_source_path + _line_number = p_line_number -func typeless() -> String: - var func_signature := "" - if _return_type == TYPE_NIL: - func_signature = "func %s(%s) -> void:" % [name(), typeless_args()] - elif _return_type == GdObjects.TYPE_VARIANT: - func_signature = "func %s(%s) -> Variant:" % [name(), typeless_args()] - else: - func_signature = "func %s(%s) -> %s:" % [name(), typeless_args(), return_type_as_string()] - return "static " + func_signature if is_static() else func_signature +func args() -> Array[GdFunctionArgument]: + return _args -func typeless_args() -> String: - var collect := PackedStringArray() - for arg in args(): - if arg.has_default(): - collect.push_back( arg.name() + "=" + arg.value_as_string()) - else: - collect.push_back(arg.name()) - for arg in varargs(): - collect.push_back(arg.name() + "=" + arg.value_as_string()) - return ", ".join(collect) +func varargs() -> Array[GdFunctionArgument]: + return _varargs func typed_args() -> String: var collect := PackedStringArray() for arg in args(): + @warning_ignore("return_value_discarded") collect.push_back(arg._to_string()) for arg in varargs(): + @warning_ignore("return_value_discarded") collect.push_back(arg._to_string()) return ", ".join(collect) @@ -135,28 +149,23 @@ func _to_string() -> String: # extract function description given by Object.get_method_list() -static func extract_from(descriptor :Dictionary) -> GdFunctionDescriptor: - var function_flags :int = descriptor["flags"] - var is_virtual_ :bool = function_flags & METHOD_FLAG_VIRTUAL - var is_static_ :bool = function_flags & METHOD_FLAG_STATIC - var is_vararg_ :bool = function_flags & METHOD_FLAG_VARARG - #var is_const :bool = function_flags & METHOD_FLAG_CONST - #var is_core :bool = function_flags & METHOD_FLAG_OBJECT_CORE - #var is_default :bool = function_flags & METHOD_FLAGS_DEFAULT - #prints("is_virtual: ", is_virtual) - #prints("is_static: ", is_static) - #prints("is_const: ", is_const) - #prints("is_core: ", is_core) - #prints("is_default: ", is_default) - #prints("is_vararg: ", is_vararg) +static func extract_from(descriptor :Dictionary, is_engine_ := true) -> GdFunctionDescriptor: + var func_name: String = descriptor["name"] + var function_flags: int = descriptor["flags"] + var return_descriptor: Dictionary = descriptor["return"] + var clazz_name: String = return_descriptor["class_name"] + var is_virtual_: bool = function_flags & METHOD_FLAG_VIRTUAL + var is_static_: bool = function_flags & METHOD_FLAG_STATIC + var is_vararg_: bool = function_flags & METHOD_FLAG_VARARG + return GdFunctionDescriptor.new( - descriptor["name"], + func_name, -1, is_virtual_, is_static_, - true, - _extract_return_type(descriptor["return"]), - descriptor["return"]["class_name"], + is_engine_, + _extract_return_type(return_descriptor), + clazz_name, _extract_args(descriptor), _build_varargs(is_vararg_) ) @@ -185,13 +194,15 @@ const enum_fix := [ "Control.LayoutMode"] -static func _extract_return_type(return_info :Dictionary) -> Variant: +static func _extract_return_type(return_info :Dictionary) -> int: var type :int = return_info["type"] var usage :int = return_info["usage"] if type == TYPE_INT and usage & PROPERTY_USAGE_CLASS_IS_ENUM: return GdObjects.TYPE_ENUM if type == TYPE_NIL and usage & PROPERTY_USAGE_NIL_IS_VARIANT: return GdObjects.TYPE_VARIANT + if type == TYPE_NIL and usage == 6: + return GdObjects.TYPE_VOID return type @@ -204,11 +215,10 @@ static func _extract_args(descriptor :Dictionary) -> Array[GdFunctionArgument]: var arg :Dictionary = arguments.pop_back() var arg_name := _argument_name(arg) var arg_type := _argument_type(arg) - var arg_default :Variant = GdFunctionArgument.UNDEFINED - if not defaults.is_empty(): - var default_value :Variant = defaults.pop_back() - arg_default = GdDefaultValueDecoder.decode_typed(arg_type, default_value) - args_.push_front(GdFunctionArgument.new(arg_name, arg_type, arg_default)) + var arg_type_hint := _argument_hint(arg) + #var arg_class: StringName = arg["class_name"] + var default_value: Variant = GdFunctionArgument.UNDEFINED if defaults.is_empty() else defaults.pop_back() + args_.push_front(GdFunctionArgument.new(arg_name, arg_type, default_value, arg_type_hint)) return args_ @@ -219,23 +229,41 @@ static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]: # if function has vararg we need to handle this manually by adding 10 default arguments var type := GdObjects.TYPE_VARARG for index in 10: - varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, "\"%s\"" % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)) + varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, '"%s"' % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)) return varargs_ static func _argument_name(arg :Dictionary) -> String: - # add suffix to the name to prevent clash with reserved names - return (arg["name"] + "_") as String + return arg["name"] static func _argument_type(arg :Dictionary) -> int: var type :int = arg["type"] + var usage :int = arg["usage"] + if type == TYPE_OBJECT: if arg["class_name"] == "Node": return GdObjects.TYPE_NODE + if arg["class_name"] == "Fuzzer": + return GdObjects.TYPE_FUZZER + + # if the argument untyped we need to scan the assignef value type + if type == TYPE_NIL and usage == PROPERTY_USAGE_NIL_IS_VARIANT: + return GdObjects.TYPE_VARIANT return type +static func _argument_hint(arg :Dictionary) -> int: + var hint :int = arg["hint"] + var hint_string :String = arg["hint_string"] + + match hint: + PROPERTY_HINT_ARRAY_TYPE: + return GdObjects.string_to_type(hint_string) + _: + return 0 + + static func _argument_type_as_string(arg :Dictionary) -> String: var type := _argument_type(arg) match type: diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd index 38468f3e..bc300fff 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -14,7 +14,7 @@ var TOKEN_CLASS_NAME := Token.new("class_name") var TOKEN_INNER_CLASS := Token.new("class") var TOKEN_EXTENDS := Token.new("extends") var TOKEN_ENUM := Token.new("enum") -var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("staticfunc") +var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("static func") var TOKEN_FUNCTION_DECLARATION := Token.new("func") var TOKEN_FUNCTION := Token.new(".") var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->") @@ -64,17 +64,12 @@ var TOKENS :Array[Token] = [ OPERATOR_REMAINDER, ] -var _regex_clazz_name :RegEx +var _regex_clazz_name := GdUnitTools.to_regex("(class) ([a-zA-Z0-9_]+) (extends[a-zA-Z]+:)|(class) ([a-zA-Z0-9_]+)") var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*") -var _base_clazz :String var _scanned_inner_classes := PackedStringArray() var _script_constants := {} -static func clean_up_row(row :String) -> String: - return to_unix_format(row.replace(" ", "").replace("\t", "")) - - static func to_unix_format(input :String) -> String: return input.replace("\r\n", "\n") @@ -263,6 +258,7 @@ class TokenInnerClass extends Token: func parse(source_rows :PackedStringArray, offset :int) -> void: # add class signature + @warning_ignore("return_value_discarded") _content.append(source_rows[offset]) # parse class content for row_index in range(offset+1, source_rows.size()): @@ -275,8 +271,10 @@ class TokenInnerClass extends Token: source_row = source_row.trim_prefix("\t") # refomat invalid empty lines if source_row.dedent().is_empty(): + @warning_ignore("return_value_discarded") _content.append("") else: + @warning_ignore("return_value_discarded") _content.append(source_row) continue break @@ -287,9 +285,6 @@ class TokenInnerClass extends Token: return "TokenInnerClass{%s}" % [_clazz_name] -func _init() -> void: - _regex_clazz_name = GdUnitTools.to_regex("(class)([a-zA-Z0-9]+)(extends[a-zA-Z]+:)|(class)([a-zA-Z0-9]+)(:)") - func get_token(input :String, current_index :int) -> Token: for t in TOKENS: @@ -352,38 +347,7 @@ func tokenize_inner_class(source_code: String, current: int, token: Token) -> To return TokenInnerClass.new(clazz_name) -@warning_ignore("assert_always_false") -func _process_values(left: Token, token_stack: Array, operator: Token) -> Token: - # precheck - if left.is_variable() and operator.is_operator(): - var lvalue :Variant = left.value() - var value :Variant = null - var next_token_ := token_stack.pop_front() as Token - match operator: - OPERATOR_ADD: - value = lvalue + next_token_.value() - OPERATOR_SUB: - value = lvalue - next_token_.value() - OPERATOR_MUL: - value = lvalue * next_token_.value() - OPERATOR_DIV: - value = lvalue / next_token_.value() - OPERATOR_REMAINDER: - value = lvalue & next_token_.value() - _: - assert(false, "Unsupported operator %s" % operator) - return Variable.new( str(value)) - return operator - - -func parse_func_return_type(row: String) -> int: - var token := parse_return_token(row) - if token == TOKEN_NOT_MATCH: - return TYPE_NIL - return token.type() - - -func parse_return_token(input: String) -> Token: +func parse_return_token(input: String) -> Variable: var index := input.rfind(TOKEN_FUNCTION_RETURN_TYPE._token) if index == -1: return TOKEN_NOT_MATCH @@ -397,10 +361,26 @@ func parse_return_token(input: String) -> Token: return token -# Parses the argument into a argument signature -# e.g. func foo(arg1 :int, arg2 = 20) -> [arg1, arg2] -func parse_arguments(input: String) -> Array[GdFunctionArgument]: - var args :Array[GdFunctionArgument] = [] +func get_function_descriptors(script: GDScript, included_functions: PackedStringArray = []) -> Array[GdFunctionDescriptor]: + var fds: Array[GdFunctionDescriptor] = [] + for method_descriptor in script.get_script_method_list(): + var func_name: String = method_descriptor["name"] + if included_functions.is_empty() or func_name in included_functions: + # exclude type set/geters + if func_name in ["@type_setter", "@type_getter"]: + continue + if not fds.any(func(fd: GdFunctionDescriptor) -> bool: return fd.name() == func_name): + fds.append(GdFunctionDescriptor.extract_from(method_descriptor, false)) + + # we need to enrich it by default arguments and line number by parsing the script + # the engine core functions has no valid methods to get this info + _prescan_script(script) + _enrich_function_descriptor(script, fds) + return fds + + +func _parse_function_arguments(input: String) -> Dictionary: + var arguments := {} var current_index := 0 var token :Token = null var bracket := 0 @@ -431,7 +411,7 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]: bracket -= 1 # if function end? if in_function and bracket == 0: - return args + return arguments # is function if token == TOKEN_FUNCTION_DECLARATION: token = next_token(input, current_index) @@ -441,13 +421,13 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]: if token is FuzzerToken: var arg_value := _parse_end_function(input.substr(current_index), true) current_index += arg_value.length() - args.append(GdFunctionArgument.new(token.name(), token.type(), arg_value)) + var arg_name :String = (token as FuzzerToken).name() + arguments[arg_name] = arg_value.lstrip(" ") continue # is value argument if in_function and token.is_variable(): - var arg_name :String = token.plain_value() - var arg_type :int = TYPE_NIL - var arg_value :Variant = GdFunctionArgument.UNDEFINED + var arg_name: String = (token as Variable).plain_value() + var arg_value: String = GdFunctionArgument.UNDEFINED # parse type and default value while current_index < len(input): token = next_token(input, current_index) @@ -460,10 +440,6 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]: if token == TOKEN_SPACE: current_index += token._consumed token = next_token(input, current_index) - arg_type = GdObjects.string_as_typeof(token._token) - # handle enum detection as argument - if arg_type == GdObjects.TYPE_VARIANT and is_class_enum_type(token._token): - arg_type = GdObjects.TYPE_ENUM TOKEN_ARGUMENT_TYPE_ASIGNMENT: arg_value = _parse_end_function(input.substr(current_index), true) current_index += arg_value.length() @@ -489,28 +465,8 @@ func parse_arguments(input: String) -> Array[GdFunctionArgument]: TOKEN_ARGUMENT_SEPARATOR: if bracket <= 1: break - arg_value = arg_value.lstrip(" ") - if arg_type == TYPE_NIL and arg_value != GdFunctionArgument.UNDEFINED: - if arg_value.begins_with("Color."): - arg_type = TYPE_COLOR - elif arg_value.begins_with("Vector2."): - arg_type = TYPE_VECTOR2 - elif arg_value.begins_with("Vector3."): - arg_type = TYPE_VECTOR3 - elif arg_value.begins_with("AABB("): - arg_type = TYPE_AABB - elif arg_value.begins_with("["): - arg_type = TYPE_ARRAY - elif arg_value.begins_with("{"): - arg_type = TYPE_DICTIONARY - else: - arg_type = typeof(str_to_var(arg_value)) - if arg_value.rfind(")") == arg_value.length()-1: - arg_type = GdObjects.TYPE_FUNC - elif arg_type == TYPE_NIL: - arg_type = TYPE_STRING - args.append(GdFunctionArgument.new(arg_name, arg_type, arg_value)) - return args + arguments[arg_name] = arg_value.lstrip(" ") + return arguments func _parse_end_function(input: String, remove_trailing_char := false) -> String: @@ -563,9 +519,10 @@ func _parse_end_function(input: String, remove_trailing_char := false) -> String return input.substr(0, current_index) +@warning_ignore("unsafe_method_access") func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray: for row_index in source_rows.size(): - var input := GdScriptParser.clean_up_row(source_rows[row_index]) + var input := source_rows[row_index] var token := next_token(input, 0) if token.is_inner_class(): if token.is_class_name(clazz_name): @@ -574,21 +531,6 @@ func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> return PackedStringArray() -func extract_source_code(script_path :PackedStringArray) -> PackedStringArray: - if script_path.is_empty(): - push_error("Invalid script path '%s'" % script_path) - return PackedStringArray() - #load the source code - var resource_path := script_path[0] - var script :GDScript = load(resource_path) - var source_code := load_source_code(script, script_path) - var base_script := script.get_base_script() - if base_script: - _base_clazz = GdObjects.extract_class_name_from_class_path([base_script.resource_path]) - source_code += load_source_code(base_script, script_path) - return source_code - - func extract_func_signature(rows :PackedStringArray, index :int) -> String: var signature := "" @@ -604,25 +546,6 @@ func extract_func_signature(rows :PackedStringArray, index :int) -> String: return "" -func load_source_code(script :GDScript, script_path :PackedStringArray) -> PackedStringArray: - var map := script.get_script_constant_map() - for key :String in map.keys(): - var value :Variant = map.get(key) - if value is GDScript: - var class_path := GdObjects.extract_class_path(value) - if class_path.size() > 1: - _scanned_inner_classes.append(class_path[1]) - - var source_code := GdScriptParser.to_unix_format(script.source_code) - var source_rows := source_code.split("\n") - # extract all inner class names - # want to extract an inner class? - if script_path.size() > 1: - var inner_clazz := script_path[1] - source_rows = extract_inner_class(source_rows, inner_clazz) - return PackedStringArray(source_rows) - - func get_class_name(script :GDScript) -> String: var source_code := GdScriptParser.to_unix_format(script.source_code) var source_rows := source_code.split("\n") @@ -635,13 +558,12 @@ func get_class_name(script :GDScript) -> String: token = next_token(input, current_index) current_index += token._consumed token = tokenize_value(input, current_index, token) - return token.value() + return (token as Variable).value() # if no class_name found extract from file name return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file()) -func parse_func_name(row :String) -> String: - var input := GdScriptParser.clean_up_row(row) +func parse_func_name(input :String) -> String: var current_index := 0 var token := next_token(input, current_index) current_index += token._consumed @@ -653,100 +575,67 @@ func parse_func_name(row :String) -> String: return token._token -func parse_functions(rows :PackedStringArray, clazz_name :String, clazz_path :PackedStringArray, included_functions := PackedStringArray()) -> Array[GdFunctionDescriptor]: - var func_descriptors :Array[GdFunctionDescriptor] = [] - for rowIndex in rows.size(): - var row := rows[rowIndex] - # step over inner class functions - if row.begins_with("\t"): - continue - var input := GdScriptParser.clean_up_row(row) - # skip comments and empty lines - if input.begins_with("#") or input.length() == 0: - continue - var token := next_token(input, 0) - if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: - if _is_func_included(input, included_functions): - var func_signature := extract_func_signature(rows, rowIndex) - var fd := parse_func_description(func_signature, clazz_name, clazz_path, rowIndex+1) - fd._is_coroutine = is_func_coroutine(rows, rowIndex) - func_descriptors.append(fd) - return func_descriptors +## Enriches the function descriptor by line number and argument default values +## - enrich all function descriptors form current script up to all inherited scrips +func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void: + var enriched_functions := PackedStringArray() + var script_to_scan := script + while script_to_scan != null: + # do not scan the test suite base class itself + if script_to_scan.resource_path == "res://addons/gdUnit4/src/GdUnitTestSuite.gd": + break + + var rows := script_to_scan.source_code.split("\n") + for rowIndex in rows.size(): + var input := rows[rowIndex] + # step over inner class functions + if input.begins_with("\t"): + continue + # skip comments and empty lines + if input.begins_with("#") or input.length() == 0: + continue + var token := next_token(input, 0) + if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: + var function_name := parse_func_name(input) + var fd: GdFunctionDescriptor = fds.filter(func(element: GdFunctionDescriptor) -> bool: + # is same function name and not already enriched + return function_name == element.name() and not enriched_functions.has(element.name()) + ).pop_front() + if fd != null: + # add as enriched function to exclude from next iteration (could be inherited) + @warning_ignore("return_value_discarded") + enriched_functions.append(fd.name()) + var func_signature := extract_func_signature(rows, rowIndex) + var func_arguments := _parse_function_arguments(func_signature) + # enrich missing default values + for arg_name: String in func_arguments.keys(): + var func_argument: String = func_arguments[arg_name] + fd.set_argument_value(arg_name, func_argument) + fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1) + fd._is_coroutine = is_func_coroutine(rows, rowIndex) + # enrich return class name if not set + if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]: + var var_token := parse_return_token(func_signature) + if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT: + fd._return_class = _patch_inner_class_names(var_token.plain_value(), "") + # if the script ihnerits we need to scan this also + script_to_scan = script_to_scan.get_base_script() func is_func_coroutine(rows :PackedStringArray, index :int) -> bool: var is_coroutine := false - for rowIndex in range( index+1, rows.size()): - var row := rows[rowIndex] - is_coroutine = row.contains("await") + for rowIndex in range(index+1, rows.size()): + var input := rows[rowIndex] + is_coroutine = input.contains("await") if is_coroutine: return true - var input := GdScriptParser.clean_up_row(row) var token := next_token(input, 0) + # scan until next function if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: break return is_coroutine -func _is_func_included(row :String, included_functions :PackedStringArray) -> bool: - if included_functions.is_empty(): - return true - for name in included_functions: - if row.find(name) != -1: - return true - return false - - -func parse_func_description(func_signature :String, clazz_name :String, clazz_path :PackedStringArray, line_number :int) -> GdFunctionDescriptor: - var name := parse_func_name(func_signature) - var return_type :int - var return_clazz := "" - var token := parse_return_token(func_signature) - if token == TOKEN_NOT_MATCH: - return_type = GdObjects.TYPE_VARIANT - else: - return_type = token.type() - if token.type() == TYPE_OBJECT: - return_clazz = _patch_inner_class_names(token.value(), clazz_name) - # is return type an enum? - if is_class_enum_type(return_clazz): - return_type = GdObjects.TYPE_ENUM - - return GdFunctionDescriptor.new( - name, - line_number, - is_virtual_func(clazz_name, clazz_path, name), - is_static_func(func_signature), - false, - return_type, - return_clazz, - parse_arguments(func_signature) - ) - - -# caches already parsed classes for virtual functions -# key: value: a Array of virtual function names -var _virtual_func_cache := Dictionary() - -func is_virtual_func(clazz_name :String, clazz_path :PackedStringArray, func_name :String) -> bool: - if _virtual_func_cache.has(clazz_name): - return _virtual_func_cache[clazz_name].has(func_name) - var virtual_functions := Array() - var method_list := GdObjects.extract_class_functions(clazz_name, clazz_path) - for method_descriptor :Dictionary in method_list: - var is_virtual_function :bool = method_descriptor["flags"] & METHOD_FLAG_VIRTUAL - if is_virtual_function: - virtual_functions.append(method_descriptor["name"]) - _virtual_func_cache[clazz_name] = virtual_functions - return _virtual_func_cache[clazz_name].has(func_name) - - -func is_static_func(func_signature :String) -> bool: - var input := GdScriptParser.clean_up_row(func_signature) - var token := next_token(input, 0) - return token == TOKEN_FUNCTION_STATIC_DECLARATION - - func is_inner_class(clazz_path :PackedStringArray) -> bool: return clazz_path.size() > 1 @@ -755,39 +644,24 @@ func is_func_end(row :String) -> bool: return row.strip_edges(false, true).ends_with(":") -func is_class_enum_type(value :String) -> bool: - if value == "Variant": - return false - # first check is given value a enum from the current class - if _script_constants.has(value): - return true - # otherwise we need to determie it by reflection - var script := GDScript.new() - script.source_code = """ - extends Resource - - static func is_class_enum_type() -> bool: - return typeof(%s) == TYPE_DICTIONARY - - """.dedent() % value - script.reload() - return script.call("is_class_enum_type") - - -func _patch_inner_class_names(clazz :String, clazz_name :String) -> String: - var base_clazz := clazz_name.split(".")[0] +func _patch_inner_class_names(clazz :String, clazz_name :String = "") -> String: var inner_clazz_name := clazz.split(".")[0] if _scanned_inner_classes.has(inner_clazz_name): - return base_clazz + "." + clazz + return inner_clazz_name + #var base_clazz := clazz_name.split(".")[0] + #return base_clazz + "." + clazz if _script_constants.has(clazz): return clazz_name + "." + clazz return clazz -func extract_functions(script :GDScript, clazz_name :String, clazz_path :PackedStringArray) -> Array[GdFunctionDescriptor]: - var source_code := load_source_code(script, clazz_path) +func _prescan_script(script: GDScript) -> void: _script_constants = script.get_script_constant_map() - return parse_functions(source_code, clazz_name, clazz_path) + for key :String in _script_constants.keys(): + var value :Variant = _script_constants.get(key) + if value is GDScript: + @warning_ignore("return_value_discarded") + _scanned_inner_classes.append(key) func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: @@ -795,13 +669,22 @@ func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: return GdUnitResult.error("Invalid script path '%s'" % clazz_path) var is_inner_class_ := is_inner_class(clazz_path) var script :GDScript = load(clazz_path[0]) - var function_descriptors := extract_functions(script, clazz_name, clazz_path) + _prescan_script(script) + + if is_inner_class_: + var inner_class_name := clazz_path[1] + if _scanned_inner_classes.has(inner_class_name): + # do load only on inner class source code and enrich the stored script instance + var source_code := _load_inner_class(script, inner_class_name) + script = _script_constants.get(inner_class_name) + script.source_code = source_code + var function_descriptors := get_function_descriptors(script) var gd_class := GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors) - # iterate over class dependencies - script = script.get_base_script() - while script != null: - clazz_name = GdObjects.extract_class_name_from_class_path([script.resource_path]) - function_descriptors = extract_functions(script, clazz_name, clazz_path) - gd_class.set_parent_clazz(GdClassDescriptor.new(clazz_name, is_inner_class_, function_descriptors)) - script = script.get_base_script() return GdUnitResult.success(gd_class) + + +func _load_inner_class(script: GDScript, inner_clazz: String) -> String: + var source_rows := GdScriptParser.to_unix_format(script.source_code).split("\n") + # extract all inner class names + var inner_class_code := extract_inner_class(source_rows, inner_clazz) + return "\n".join(inner_class_code) diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd index f3130a07..9faf830b 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -9,18 +9,66 @@ func __run_expression() -> Variant: """ -func execute(src_script :GDScript, expression :String) -> Variant: +var constructor_args_regex := RegEx.create_from_string("new\\((?.*)\\)") + + +func execute(src_script: GDScript, value: Variant) -> Variant: + if typeof(value) != TYPE_STRING: + return value + + var expression: String = value + var parameter_map := src_script.get_script_constant_map() + for key: String in parameter_map.keys(): + var parameter_value: Variant = parameter_map[key] + # check we need to construct from inner class + # we need to use the original class instance from the script_constant_map otherwise we run into a runtime error + if expression.begins_with(key + ".new") and parameter_value is GDScript: + var object: GDScript = parameter_value + var args := build_constructor_arguments(parameter_map, expression.substr(expression.find("new"))) + if args.is_empty(): + return object.new() + return object.callv("new", args) + var script := GDScript.new() var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path script.source_code = CLASS_TEMPLATE.dedent()\ .replace("${clazz_path}", resource_path)\ .replace("$expression", expression) - script.reload(false) - var runner :Variant = script.new() + #script.take_over_path(resource_path) + @warning_ignore("return_value_discarded") + script.reload(true) + var runner: Object = script.new() if runner.has_method("queue_free"): - runner.queue_free() + (runner as Node).queue_free() + @warning_ignore("unsafe_method_access") return runner.__run_expression() -func to_fuzzer(src_script :GDScript, expression :String) -> Fuzzer: - return execute(src_script, expression) as Fuzzer +func build_constructor_arguments(parameter_map: Dictionary, expression: String) -> Array[Variant]: + var result := constructor_args_regex.search(expression) + var extracted_arguments := result.get_string("args").strip_edges() + if extracted_arguments.is_empty(): + return [] + var arguments :Array = extracted_arguments.split(",") + return arguments.map(func(argument: String) -> Variant: + var value := argument.strip_edges() + + # is argument an constant value + if parameter_map.has(value): + return parameter_map[value] + # is typed named value like Vector3.ONE + for type:int in GdObjects.TYPE_AS_STRING_MAPPINGS: + var type_as_string:String = GdObjects.TYPE_AS_STRING_MAPPINGS[type] + if value.begins_with(type_as_string): + return type_convert(value, type) + # is value a string + if value.begins_with("'") or value.begins_with('"'): + return value.trim_prefix("'").trim_suffix("'").trim_prefix('"').trim_suffix('"') + # fallback to default value converting + return str_to_var(value) + ) + + +func to_fuzzer(src_script: GDScript, expression: String) -> Fuzzer: + @warning_ignore("unsafe_cast") + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd index d67ee254..73db59e4 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd @@ -45,10 +45,11 @@ func validate(input_value_set: Array) -> String: for input_values :Variant in input_value_set: var parameter_set_index := input_value_set.find(input_values) if input_values is Array: - var current_arg_count :int = input_values.size() + var arr_values: Array = input_values + var current_arg_count := arr_values.size() if current_arg_count != expected_arg_count: return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] - var error := GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values, parameter_set_index) + var error := GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, arr_values, parameter_set_index) if not error.is_empty(): return error else: @@ -97,6 +98,7 @@ func build_test_case_names(test_case: _TestCase) -> PackedStringArray: for parameter_set_index in parameter_sets.size(): var parameter_set := parameter_sets[parameter_set_index] _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names) + @warning_ignore("return_value_discarded") _test_case_names_cache.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, parameter_set, parameter_set_index)) parameter_set_index += 1 return _test_case_names_cache @@ -121,6 +123,7 @@ func _extract_test_names_by_reflection(test_case: _TestCase) -> PackedStringArra var parameter_sets := load_parameter_sets(test_case) var test_case_names: PackedStringArray = [] for index in parameter_sets.size(): + @warning_ignore("return_value_discarded") test_case_names.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, str(parameter_sets[index]), index)) return test_case_names @@ -149,10 +152,10 @@ func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array: if result != OK: push_error("Extracting test parameters failed! Script loading error: %s" % result) return [] - var instance :Variant = script.new() + var instance :Object = script.new() GdUnitTestParameterSetResolver.copy_properties(test_case.get_parent(), instance) - instance.queue_free() - var parameter_sets :Variant = instance.call("__extract_test_parameters") + (instance as Node).queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") if not do_validate: return parameter_sets # validate the parameter set @@ -169,14 +172,32 @@ func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array: """.dedent().trim_prefix("\n") % [ GdAssertMessages._error("Internal Error"), GdAssertMessages._error("Please report this issue as a bug!")] - test_case.get_parent().__execution_context\ - .reports()\ - .append(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error)) + GdUnitThreadManager.get_current_context()\ + .get_execution_context()\ + .add_report(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error)) test_case.skip(true, error) test_case._interupted = true + @warning_ignore("return_value_discarded") + fixure_typed_parameters(parameter_sets, _fd.args()) return parameter_sets +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + + static func copy_properties(source: Object, dest: Object) -> void: for property in source.get_property_list(): var property_name :String = property["name"] diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd index 3e241f34..6fc282db 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd @@ -86,8 +86,10 @@ static func default_CS_template() -> String: static func build_template(source_path: String) -> String: - var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value() as String) - return GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template())\ + var clazz_name :String = GdObjects.to_pascal_case(GdObjects.extract_class_name(source_path).value_as_string()) + var template: String = GdUnitSettings.get_setting(GdUnitSettings.TEMPLATE_TS_GD, default_GD_template()) + + return template\ .replace(TAG_TEST_SUITE_CLASS, clazz_name+"Test")\ .replace(TAG_SOURCE_RESOURCE_PATH, source_path)\ .replace(TAG_SOURCE_CLASS_NAME, clazz_name)\ diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd index fe326a68..8b4ae4ff 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -29,9 +29,8 @@ func dispose() -> void: _thread = null -func set_assert(value :GdUnitAssert) -> GdUnitThreadContext: +func set_assert(value :GdUnitAssert) -> void: _assert = value - return self func get_assert() -> GdUnitAssert: diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd index ad124ced..31b10782 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -36,6 +36,7 @@ func _run(name :String, cb :Callable) -> Variant: var save_current_thread_id := _current_thread_id var thread := Thread.new() thread.set_meta("name", name) + @warning_ignore("return_value_discarded") thread.start(cb) _current_thread_id = thread.get_id() as int _register_thread(thread, _current_thread_id) @@ -52,8 +53,9 @@ func _register_thread(thread :Thread, thread_id :int) -> void: func _unregister_thread(thread_id :int) -> void: - var context := _thread_context_by_id.get(thread_id) as GdUnitThreadContext + var context: GdUnitThreadContext = _thread_context_by_id.get(thread_id) if context: + @warning_ignore("return_value_discarded") _thread_context_by_id.erase(thread_id) context.dispose() diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd b/addons/gdUnit4/src/doubler/CallableDoubler.gd new file mode 100644 index 00000000..14a5947d --- /dev/null +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd @@ -0,0 +1,210 @@ +## The helper class to allow to double Callable +## Is just a wrapper to the original callable with the same function signature. +## +## Due to interface conflicts between 'Callable' and 'Object', +## it is not possible to stub the 'call' and 'call_deferred' methods. +## +## The Callable interface and the Object class have overlapping method signatures, +## which causes conflicts when attempting to stub these methods. +## As a result, you cannot create stubs for 'call' and 'call_deferred' methods. + +class_name CallableDoubler + + +const doubler_script :Script = preload("res://addons/gdUnit4/src/doubler/CallableDoubler.gd") + +var _cb: Callable + + +func _init(cb: Callable) -> void: + assert(cb!=null, "Invalid argument must not be null") + _cb = cb + +## --- helpers ----------------------------------------------------------------------------------------------------------------------------- +static func map_func_name(method_info: Dictionary) -> String: + return method_info["name"] + + +## We do not want to double all functions based on Object for this class +## Is used on SpyBuilder to excluding functions to be doubled for Callable +static func excluded_functions() -> PackedStringArray: + return ClassDB.class_get_method_list("Object")\ + .map(CallableDoubler.map_func_name)\ + .filter(func (name: String) -> bool: + return !CallableDoubler.callable_functions().has(name)) + + +static func non_callable_functions(name: String) -> bool: + return ![ + # we allow "_init", is need to construct it, + "excluded_functions", + "non_callable_functions", + "callable_functions", + "map_func_name" + ].has(name) + + +## Returns the list of supported Callable functions +static func callable_functions() -> PackedStringArray: + var supported_functions :Array = doubler_script.get_script_method_list()\ + .map(CallableDoubler.map_func_name)\ + .filter(CallableDoubler.non_callable_functions) + # We manually add these functions that we cannot/may not overwrite in this class + supported_functions.append_array(["call_deferred", "callv"]) + return supported_functions + + +## ----------------------------------------------------------------------------------------------------------------------------------------- +## Callable functions stubing +## ----------------------------------------------------------------------------------------------------------------------------------------- + +@warning_ignore("untyped_declaration") +func bind(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Callable: + # save + var bind_values: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) + _cb = _cb.bindv(bind_values) + return _cb + + +func bindv(caller_args: Array) -> Callable: + _cb = _cb.bindv(caller_args) + return _cb + + +@warning_ignore("untyped_declaration", "native_method_override", "unused_parameter") +func call(arg0=null, + arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Variant: + + # This is a placeholder function signanture without any functionallity! + # It is used by the function doubler to double function signature of Callable:call() + # The doubled function calls direct _cb.callv() see GdUnitSpyFunctionDoubler:TEMPLATE_CALLABLE_CALL template + assert(false) + return null + + +# Is not supported, see class description +#func call_deferred(a) -> void: +# pass + + +# Is not supported, see class description +#func callv(a) -> void: +# pass + + + +func get_bound_arguments() -> Array: + return _cb.get_bound_arguments() + + +func get_bound_arguments_count() -> int: + return _cb.get_bound_arguments_count() + + +func get_method() -> StringName: + return _cb.get_method() + + +func get_object() -> Object: + return _cb.get_object() + + +func get_object_id() -> int: + return _cb.get_object_id() + + +func hash() -> int: + return _cb.hash() + + +func is_custom() -> bool: + return _cb.is_custom() + + +func is_null() -> bool: + return _cb.is_null() + + +func is_standard() -> bool: + return _cb.is_standard() + + +func is_valid() -> bool: + return _cb.is_valid() + + +@warning_ignore("untyped_declaration") +func rpc(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void: + + var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) + match args.size(): + 0: _cb.rpc(0) + 1: _cb.rpc(args[0]) + 2: _cb.rpc(args[0], args[1]) + 3: _cb.rpc(args[0], args[1], args[2]) + 4: _cb.rpc(args[0], args[1], args[2], args[3]) + 5: _cb.rpc(args[0], args[1], args[2], args[3], args[4]) + 6: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5]) + 7: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) + 8: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) + 9: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]) + 10: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]) + + +@warning_ignore("untyped_declaration") +func rpc_id(peer_id: int, + arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, + arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void: + + var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) + match args.size(): + 0: _cb.rpc_id(peer_id) + 1: _cb.rpc_id(peer_id, args[0]) + 2: _cb.rpc_id(peer_id, args[0], args[1]) + 3: _cb.rpc_id(peer_id, args[0], args[1], args[2]) + 4: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3]) + 5: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4]) + 6: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5]) + 7: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6]) + 8: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) + 9: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]) + 10: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]) + + +func unbind(argcount: int) -> Callable: + _cb = _cb.unbind(argcount) + return _cb diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd index 29cc62b6..124d3d4e 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -1,4 +1,5 @@ # This class defines a value extractor by given function name and args +class_name GdUnitFuncValueExtractor extends GdUnitValueExtractor var _func_names :PackedStringArray @@ -27,13 +28,14 @@ func args() -> Array: # # if the value not a Object or not accesible be `func_name` the value is converted to `"n.a."` # expecing null values -func extract_value(value :Variant) -> Variant: +func extract_value(value: Variant) -> Variant: if value == null: return null for func_name in func_names(): if GdArrayTools.is_array_type(value): var values := Array() - for element :Variant in Array(value): + @warning_ignore("unsafe_cast") + for element: Variant in (value as Array): values.append(_call_func(element, func_name)) value = values else: @@ -50,17 +52,19 @@ func _call_func(value :Variant, func_name :String) -> Variant: # for array types we need to call explicit by function name, using funcref is only supported for Objects # TODO extend to all array functions if GdArrayTools.is_array_type(value) and func_name == "empty": - return value.is_empty() + @warning_ignore("unsafe_cast") + return (value as Array).is_empty() if is_instance_valid(value): # extract from function - if value.has_method(func_name): - var extract := Callable(value, func_name) + var obj_value: Object = value + if obj_value.has_method(func_name): + var extract := Callable(obj_value, func_name) if extract.is_valid(): - return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) + return obj_value.call(func_name) if args().is_empty() else obj_value.callv(func_name, args()) else: # if no function exists than try to extract form parmeters - var parameter :Variant = value.get(func_name) + var parameter: Variant = obj_value.get(func_name) if parameter != null: return parameter # nothing found than return 'n.a.' diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd index 9b13e8aa..ca165d33 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -60,5 +60,6 @@ func next_value() -> String: var max_char := len(_charset) var length :int = max(_min_length, randi() % _max_length) for i in length: + @warning_ignore("return_value_discarded") value.append(_charset[randi() % max_char]) return value.get_string_from_utf8() diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd index 2cf07904..b5e3de3a 100644 --- a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd @@ -12,12 +12,14 @@ func is_match(value :Variant) -> bool: if typeof(value) != TYPE_OBJECT: return false if is_instance_valid(value) and GdObjects.is_script(_clazz): - return value.get_script() == _clazz + @warning_ignore("unsafe_cast") + return (value as Object).get_script() == _clazz return is_instance_of(value, _clazz) func _to_string() -> String: if (_clazz as Object).is_class("GDScriptNativeClass"): + @warning_ignore("unsafe_method_access") var instance :Object = _clazz.new() var clazz_name := instance.get_class() if not instance is RefCounted: diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd index ec62ecf6..f779bd79 100644 --- a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd @@ -9,13 +9,13 @@ func _init(matchers :Array) -> void: func is_match(arguments :Variant) -> bool: - var arg_array := arguments as Array - if arg_array.size() != _matchers.size(): + var arg_array: Array = arguments + if arg_array == null or arg_array.size() != _matchers.size(): return false for index in arg_array.size(): - var arg :Variant = arg_array[index] - var matcher := _matchers[index] as GdUnitArgumentMatcher + var arg: Variant = arg_array[index] + var matcher: GdUnitArgumentMatcher = _matchers[index] if not matcher.is_match(arg): return false diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd b/addons/gdUnit4/src/mocking/GdUnitMock.gd index bb50e5eb..c520d922 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMock.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd @@ -9,10 +9,10 @@ const RETURN_DEFAULTS = "RETURN_DEFAULTS" ## builds full deep mocked object const RETURN_DEEP_STUB = "RETURN_DEEP_STUB" -var _value :Variant +var _value: Variant -func _init(value :Variant) -> void: +func _init(value: Variant) -> void: _value = value @@ -21,9 +21,10 @@ func _init(value :Variant) -> void: ## [codeblock] ## do_return(false).on(myMock).is_selected() ## [/codeblock] -func on(obj :Object) -> Object: - if not GdUnitMock._is_mock_or_spy( obj, "__do_return"): +func on(obj: Variant) -> Variant: + if not GdUnitMock._is_mock_or_spy(obj, "__do_return"): return obj + @warning_ignore("unsafe_method_access") return obj.__do_return(_value) @@ -33,8 +34,12 @@ func checked(obj :Object) -> Object: return on(obj) -static func _is_mock_or_spy(obj :Object, func_sig :String) -> bool: - if obj is GDScript and not obj.get_script().has_script_method(func_sig): +static func _is_mock_or_spy(obj: Variant, func_sig: String) -> bool: + if obj is Object and not as_object(obj).has_method(func_sig): push_error("Error: You try to use a non mock or spy!") return false return true + + +static func as_object(value: Variant) -> Object: + return value diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd index b3bb41a2..bc26d657 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -9,6 +9,7 @@ static func is_push_errors() -> bool: return GdUnitSettings.is_report_push_errors() +@warning_ignore("unsafe_method_access", "unsafe_cast") static func build(clazz :Variant, mock_mode :String, debug_write := false) -> Variant: var push_errors := is_push_errors() if not is_mockable(clazz, push_errors): @@ -17,7 +18,7 @@ static func build(clazz :Variant, mock_mode :String, debug_write := false) -> Va if GdObjects.is_scene(clazz): return mock_on_scene(clazz as PackedScene, debug_write) elif typeof(clazz) == TYPE_STRING and clazz.ends_with(".tscn"): - return mock_on_scene(load(clazz), debug_write) + return mock_on_scene(load(clazz as String) as PackedScene, debug_write) # mocking a script var instance := create_instance(clazz) var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) @@ -25,32 +26,34 @@ static func build(clazz :Variant, mock_mode :String, debug_write := false) -> Va instance.free() if mock == null: return null - var mock_instance :Variant = mock.new() + var mock_instance: Variant = mock.new() mock_instance.__set_script(mock) mock_instance.__set_singleton() mock_instance.__set_mode(mock_mode) return register_auto_free(mock_instance) -static func create_instance(clazz :Variant) -> Object: +@warning_ignore("unsafe_method_access", "unsafe_cast") +static func create_instance(clazz: Variant) -> Object: if typeof(clazz) == TYPE_OBJECT and (clazz as Object).is_class("GDScriptNativeClass"): return clazz.new() elif (clazz is GDScript) || (typeof(clazz) == TYPE_STRING and clazz.ends_with(".gd")): - var script :GDScript = null + var script: GDScript = null if clazz is GDScript: script = clazz else: - script = load(clazz) + script = load(clazz as String) var args := GdObjects.build_function_default_arguments(script, "_init") return script.callv("new", args) - elif typeof(clazz) == TYPE_STRING and ClassDB.can_instantiate(clazz): - return ClassDB.instantiate(clazz) + elif typeof(clazz) == TYPE_STRING and ClassDB.can_instantiate(clazz as String): + return ClassDB.instantiate(clazz as String) push_error("Can't create a mock validation instance from class: `%s`" % clazz) return null -static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Object: +@warning_ignore("unsafe_method_access") +static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Variant: var push_errors := is_push_errors() if not scene.can_instantiate(): if push_errors: @@ -61,6 +64,7 @@ static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Object: if scene_instance.get_script() == null: if push_errors: push_error("Can't create a mockable instance for a scene without script '%s'" % scene.resource_path) + @warning_ignore("return_value_discarded") GdUnitTools.free_instance(scene_instance) return null @@ -95,11 +99,13 @@ static func mock_on_script(instance :Object, clazz :Variant, function_excludes : var mock := GDScript.new() mock.source_code = "\n".join(lines) - mock.resource_name = "Mock%s.gd" % clazz_name - mock.resource_path = GdUnitFileAccess.create_temp_dir("mock") + "/Mock%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + mock.resource_name = "Mock%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] + mock.resource_path = "%s/%s" % [GdUnitFileAccess.create_temp_dir("mock"), mock.resource_name] if debug_write: + @warning_ignore("return_value_discarded") DirAccess.remove_absolute(mock.resource_path) + @warning_ignore("return_value_discarded") ResourceSaver.save(mock, mock.resource_path) var error := mock.reload(true) if error != OK: @@ -131,7 +137,7 @@ static func is_mockable(clazz :Variant, push_errors :bool=false) -> bool: return false return true # verify by class name checked registered classes - var clazz_name := clazz as String + var clazz_name: String = clazz if ClassDB.class_exists(clazz_name): if Engine.has_singleton(clazz_name): if push_errors: diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd index 4d8d300f..f7464210 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd @@ -3,36 +3,36 @@ extends GdFunctionDoubler const TEMPLATE_FUNC_WITH_RETURN_VALUE = """ - var args :Array = ["$(func_name)", $(arguments)] + var args__: Array = ["$(func_name)", $(arguments)] if $(instance)__is_prepare_return_value(): - $(instance)__save_function_return_value(args) + $(instance)__save_function_return_value(args__) return ${default_return_value} if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return ${default_return_value} else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) - if $(instance)__do_call_real_func("$(func_name)", args): + if $(instance)__do_call_real_func("$(func_name)", args__): return $(await)super($(arguments)) - return $(instance)__get_mocked_return_value_or_default(args, ${default_return_value}) + return $(instance)__get_mocked_return_value_or_default(args__, ${default_return_value}) """ const TEMPLATE_FUNC_WITH_RETURN_VOID = """ - var args :Array = ["$(func_name)", $(arguments)] + var args__: Array = ["$(func_name)", $(arguments)] if $(instance)__is_prepare_return_value(): if $(push_errors): push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") return if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) if $(instance)__do_call_real_func("$(func_name)"): $(await)super($(arguments)) @@ -41,34 +41,34 @@ const TEMPLATE_FUNC_WITH_RETURN_VOID = """ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ - var varargs :Array = __filter_vargs([$(varargs)]) - var args :Array = ["$(func_name)", $(arguments)] + varargs + var varargs__: Array = __filter_vargs([$(varargs)]) + var args__: Array = ["$(func_name)", $(arguments)] + varargs__ if $(instance)__is_prepare_return_value(): if $(push_errors): push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") - $(instance)__save_function_return_value(args) + $(instance)__save_function_return_value(args__) return ${default_return_value} if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return ${default_return_value} else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) - if $(instance)__do_call_real_func("$(func_name)", args): - match varargs.size(): + if $(instance)__do_call_real_func("$(func_name)", args__): + match varargs__.size(): 0: return $(await)super($(arguments)) - 1: return $(await)super($(arguments), varargs[0]) - 2: return $(await)super($(arguments), varargs[0], varargs[1]) - 3: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2]) - 4: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3]) - 5: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) - 6: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) - 7: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) - 8: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) - 9: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) - 10: return $(await)super($(arguments), varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) - return __get_mocked_return_value_or_default(args, ${default_return_value}) + 1: return $(await)super($(arguments), varargs__[0]) + 2: return $(await)super($(arguments), varargs__[0], varargs__[1]) + 3: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2]) + 4: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3]) + 5: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4]) + 6: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5]) + 7: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6]) + 8: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7]) + 9: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7], varargs__[8]) + 10: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7], varargs__[8], varargs__[9]) + return __get_mocked_return_value_or_default(args__, ${default_return_value}) """ @@ -77,9 +77,10 @@ func _init(push_errors :bool = false) -> void: super._init(push_errors) -func get_template(return_type :Variant, is_vararg :bool) -> String: - if is_vararg: +func get_template(fd: GdFunctionDescriptor, _is_callable: bool) -> String: + if fd.is_vararg(): return TEMPLATE_FUNC_VARARG_RETURN_VALUE + var return_type :Variant = fd.return_type() if return_type is StringName: return TEMPLATE_FUNC_WITH_RETURN_VALUE return TEMPLATE_FUNC_WITH_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_FUNC_WITH_RETURN_VALUE diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd index cbeb782b..de43da98 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -77,6 +77,7 @@ func __save_function_return_value(__fuction_args :Array) -> void: __prepare_return_value = false +@warning_ignore("unsafe_method_access") func __is_mocked_args_match(__func_args :Array, __mocked_args :Array) -> bool: var __is_matching := false for __index in __mocked_args.size(): @@ -98,6 +99,7 @@ func __is_mocked_args_match(__func_args :Array, __mocked_args :Array) -> bool: return __is_matching +@warning_ignore("unsafe_method_access") func __get_mocked_return_value_or_default(__fuction_args :Array, __default_return_value :Variant) -> Variant: var __func_name :String = __fuction_args[0] if not __mocked_return_values.has(__func_name): @@ -120,6 +122,7 @@ func __set_mode(mock_working_mode :String) -> Object: return self +@warning_ignore("unsafe_method_access") func __do_call_real_func(__func_name :String, __func_args := []) -> bool: var __is_call_real_func := __mock_working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(__func_name) # do not call real funcions for mocked functions diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd index 4f767bb9..5ee14878 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -14,37 +14,48 @@ const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const PATTERN_SCRIPT_ERROR := "USER SCRIPT ERROR:" const PATTERN_PUSH_ERROR := "USER ERROR:" const PATTERN_PUSH_WARNING := "USER WARNING:" +# With Godot 4.4 the pattern has changed +const PATTERN_4x4_SCRIPT_ERROR := "SCRIPT ERROR:" +const PATTERN_4x4_PUSH_ERROR := "ERROR:" +const PATTERN_4x4_PUSH_WARNING := "WARNING:" static var _regex_parse_error_line_number: RegEx -var _type :TYPE -var _line :int -var _message :String -var _details :String +var _type: TYPE +var _line: int +var _message: String +var _details: String -func _init(type :TYPE, line :int, message :String, details :String) -> void: +func _init(type: TYPE, line: int, message: String, details: String) -> void: _type = type _line = line _message = message _details = details -static func extract_push_warning(records :PackedStringArray, index :int) -> ErrorLogEntry: - return _extract(records, index, TYPE.PUSH_WARNING, PATTERN_PUSH_WARNING) +static func is_godot4x4() -> bool: + return Engine.get_version_info().hex >= 0x40400 -static func extract_push_error(records :PackedStringArray, index :int) -> ErrorLogEntry: - return _extract(records, index, TYPE.PUSH_ERROR, PATTERN_PUSH_ERROR) +static func extract_push_warning(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_PUSH_WARNING if is_godot4x4() else PATTERN_PUSH_WARNING + return _extract(records, index, TYPE.PUSH_WARNING, pattern) -static func extract_error(records :PackedStringArray, index :int) -> ErrorLogEntry: - return _extract(records, index, TYPE.SCRIPT_ERROR, PATTERN_SCRIPT_ERROR) +static func extract_push_error(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_PUSH_ERROR if is_godot4x4() else PATTERN_PUSH_ERROR + return _extract(records, index, TYPE.PUSH_ERROR, pattern) -static func _extract(records :PackedStringArray, index :int, type :TYPE, pattern :String) -> ErrorLogEntry: +static func extract_error(records: PackedStringArray, index: int) -> ErrorLogEntry: + var pattern := PATTERN_4x4_SCRIPT_ERROR if is_godot4x4() else PATTERN_SCRIPT_ERROR + return _extract(records, index, TYPE.SCRIPT_ERROR, pattern) + + +static func _extract(records: PackedStringArray, index: int, type: TYPE, pattern: String) -> ErrorLogEntry: var message := records[index] - if message.contains(pattern): + if message.begins_with(pattern): var error := message.replace(pattern, "").strip_edges() var details := records[index+1].strip_edges() var line := _parse_error_line_number(details) @@ -52,7 +63,7 @@ static func _extract(records :PackedStringArray, index :int, type :TYPE, pattern return null -static func _parse_error_line_number(record :String) -> int: +static func _parse_error_line_number(record: String) -> int: if _regex_parse_error_line_number == null: _regex_parse_error_line_number = GdUnitTools.to_regex("at: .*res://.*:(\\d+)") var matches := _regex_parse_error_line_number.search(record) diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd index 720fe5c8..2ea2234e 100644 --- a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -1,8 +1,8 @@ class_name GodotGdErrorMonitor extends GdUnitMonitor -var _godot_log_file :String -var _eof :int +var _godot_log_file: String +var _eof: int var _report_enabled := false var _entries: Array[ErrorLogEntry] = [] @@ -25,14 +25,14 @@ func stop() -> void: func to_reports() -> Array[GdUnitReport]: - var reports_ :Array[GdUnitReport] = [] + var reports_: Array[GdUnitReport] = [] if _report_enabled: reports_.assign(_entries.map(_to_report)) _entries.clear() return reports_ -static func _to_report(errorLog :ErrorLogEntry) -> GdUnitReport: +static func _to_report(errorLog: ErrorLogEntry) -> GdUnitReport: var failure := "%s\n\t%s\n%s %s" % [ GdAssertMessages._error("Godot Runtime Error !"), GdAssertMessages._colored_value(errorLog._details), @@ -42,25 +42,26 @@ static func _to_report(errorLog :ErrorLogEntry) -> GdUnitReport: func scan(force_collect_reports := false) -> Array[ErrorLogEntry]: - await Engine.get_main_loop().process_frame - await Engine.get_main_loop().physics_frame + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame _entries.append_array(_collect_log_entries(force_collect_reports)) return _entries -func erase_log_entry(entry :ErrorLogEntry) -> void: +func erase_log_entry(entry: ErrorLogEntry) -> void: _entries.erase(entry) -func _collect_log_entries(force_collect_reports :bool) -> Array[ErrorLogEntry]: +func _collect_log_entries(force_collect_reports: bool) -> Array[ErrorLogEntry]: var file := FileAccess.open(_godot_log_file, FileAccess.READ) file.seek(_eof) var records := PackedStringArray() while not file.eof_reached(): + @warning_ignore("return_value_discarded") records.append(file.get_line()) file.seek_end(0) _eof = file.get_length() - var log_entries :Array[ErrorLogEntry]= [] + var log_entries: Array[ErrorLogEntry]= [] var is_report_errors := force_collect_reports or _is_report_push_errors() var is_report_script_errors := force_collect_reports or _is_report_script_errors() for index in records.size(): @@ -70,7 +71,7 @@ func _collect_log_entries(force_collect_reports :bool) -> Array[ErrorLogEntry]: log_entries.append(ErrorLogEntry.extract_push_error(records, index)) if is_report_script_errors: log_entries.append(ErrorLogEntry.extract_error(records, index)) - return log_entries.filter(func(value :ErrorLogEntry) -> bool: return value != null ) + return log_entries.filter(func(value: ErrorLogEntry) -> bool: return value != null ) func _is_reporting_enabled() -> bool: diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd index e828a9e8..d6c815c8 100644 --- a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd +++ b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd @@ -6,6 +6,7 @@ static func instance() -> Object: return GdUnitSingleton.instance("GdUnit4CSharpApi", func() -> Object: if not GdUnit4CSharpApiLoader.is_mono_supported(): return null + @warning_ignore("unsafe_method_access") return load("res://addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs").new() ) @@ -22,15 +23,17 @@ static func is_mono_supported() -> bool: static func version() -> String: if not GdUnit4CSharpApiLoader.is_mono_supported(): return "unknown" + @warning_ignore("unsafe_method_access") return instance().Version() static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> GdUnitResult: if not GdUnit4CSharpApiLoader.is_mono_supported(): return GdUnitResult.error("Can't create test suite. No C# support found.") - var result := instance().CreateTestSuite(source_path, line_number, test_suite_path) as Dictionary + @warning_ignore("unsafe_method_access") + var result: Dictionary = instance().CreateTestSuite(source_path, line_number, test_suite_path) if result.has("error"): - return GdUnitResult.error(result.get("error")) + return GdUnitResult.error(str(result.get("error"))) return GdUnitResult.success(result) @@ -42,6 +45,7 @@ static func is_test_suite(resource_path :String) -> bool: if GdUnitSettings.is_report_push_errors(): push_error("Can't create test suite. Missing resource path.") return false + @warning_ignore("unsafe_method_access") return instance().IsTestSuite(resource_path) @@ -50,12 +54,14 @@ static func parse_test_suite(source_path :String) -> Node: if GdUnitSettings.is_report_push_errors(): push_error("Can't create test suite. No c# support found.") return null + @warning_ignore("unsafe_method_access") return instance().ParseTestSuite(source_path) static func create_executor(listener :Node) -> RefCounted: if not GdUnit4CSharpApiLoader.is_mono_supported(): return null + @warning_ignore("unsafe_method_access") return instance().Executor(listener) diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd b/addons/gdUnit4/src/network/GdUnitServer.gd index 7b7be090..ea638c51 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.gd +++ b/addons/gdUnit4/src/network/GdUnitServer.gd @@ -4,6 +4,7 @@ extends Node @onready var _server :GdUnitTcpServer = $TcpServer +@warning_ignore("return_value_discarded") func _ready() -> void: var result := _server.start() if result.is_error(): diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd index 5cf8b053..953c9f51 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -51,6 +51,7 @@ func _process(_delta :float) -> void: set_process(false) # wait until client is connected to server for retry in 10: + @warning_ignore("return_value_discarded") _stream.poll() console("wait to connect ..") if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTING: @@ -71,7 +72,7 @@ func _process(_delta :float) -> void: await get_tree().create_timer(0.500).timeout rpc_ = rpc_receive() set_process(true) - _client_id = rpc_.client_id() + _client_id = (rpc_ as RPCClientConnect).client_id() console("Connected to Server: %d" % _client_id) connection_succeeded.emit("Connect to TCP Server %s:%d success." % [_host, _port]) _connected = true @@ -98,6 +99,7 @@ func process_rpc() -> void: func rpc_send(p_rpc :RPC) -> void: if _stream != null: var data := GdUnitServerConstants.JSON_RESPONSE_DELIMITER + p_rpc.serialize() + GdUnitServerConstants.JSON_RESPONSE_DELIMITER + @warning_ignore("return_value_discarded") _stream.put_data(data.to_utf8_buffer()) @@ -106,7 +108,7 @@ func rpc_receive() -> RPC: while _stream.get_available_bytes() > 0: var available_bytes := _stream.get_available_bytes() var data := _stream.get_data(available_bytes) - var received_data := data[1] as PackedByteArray + var received_data: PackedByteArray = data[1] # data send by Godot has this magic header of 12 bytes var header := Array(received_data.slice(0, 4)) if header == [0, 0, 0, 124]: diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index e27fe18b..05ccf3d0 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -4,7 +4,8 @@ extends Node signal client_connected(client_id :int) signal client_disconnected(client_id :int) -signal rpc_data(rpc_data :RPC) +@warning_ignore("unused_signal") +signal rpc_data(rpc_data: RPC) var _server :TCPServer @@ -17,6 +18,7 @@ class TcpConnection extends Node: var _readBuffer :String = "" + @warning_ignore("unsafe_method_access") func _init(p_server :Variant) -> void: assert(p_server is TCPServer) _stream = p_server.take_connection() @@ -31,6 +33,7 @@ class TcpConnection extends Node: func close() -> void: if _stream != null: + @warning_ignore("unsafe_method_access") _stream.disconnect_from_host() _readBuffer = "" _stream = null @@ -46,15 +49,18 @@ class TcpConnection extends Node: func rpc_send(p_rpc: RPC) -> void: + @warning_ignore("unsafe_method_access") _stream.put_var(p_rpc.serialize(), true) func _process(_delta: float) -> void: + @warning_ignore("unsafe_method_access") if _stream == null or _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: return receive_packages() + @warning_ignore("unsafe_method_access") func receive_packages() -> void: var available_bytes :int = _stream.get_available_bytes() if available_bytes > 0: @@ -64,7 +70,7 @@ class TcpConnection extends Node: push_error("Error getting data from stream: %s " % partial_data[0]) return else: - var received_data := partial_data[1] as PackedByteArray + var received_data: PackedByteArray = partial_data[1] for package in _read_next_data_packages(received_data): var rpc_ := RPC.deserialize(package) if rpc_ is RPCClientDisconnect: @@ -95,6 +101,7 @@ class TcpConnection extends Node: pass +@warning_ignore("return_value_discarded") func _ready() -> void: _server = TCPServer.new() client_connected.connect(_on_client_connected) @@ -130,6 +137,7 @@ func stop() -> void: _server.stop() for connection in get_children(): if connection is TcpConnection: + @warning_ignore("unsafe_method_access") connection.close() remove_child(connection) _server = null @@ -151,6 +159,7 @@ func _on_client_connected(client_id: int) -> void: console("Client connected %d" % client_id) +@warning_ignore("unsafe_method_access") func _on_client_disconnected(client_id: int) -> void: for connection in get_children(): if connection is TcpConnection and connection.id() == client_id: diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd b/addons/gdUnit4/src/network/rpc/RPC.gd index 190b05ab..c3ddaaf1 100644 --- a/addons/gdUnit4/src/network/rpc/RPC.gd +++ b/addons/gdUnit4/src/network/rpc/RPC.gd @@ -13,7 +13,7 @@ static func deserialize(json_value :String) -> Object: if err != OK: push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [json.get_error_line(), json.get_error_message(), json_value]) return null - var result := json.get_data() as Dictionary + var result :Dictionary = json.get_data() if not typeof(result) == TYPE_DICTIONARY: push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [result.error_line, result.error_string, json_value]) return null diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd index 9152c8d2..9cfd1fe1 100644 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd @@ -8,6 +8,7 @@ var _path :String func serialize(resource :Node) -> Dictionary: var serialized := Dictionary() serialized["name"] = resource.get_name() + @warning_ignore("unsafe_method_access") serialized["resource_path"] = resource.ResourcePath() return serialized diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd index 26f5dda5..cfb093e0 100644 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd @@ -2,15 +2,23 @@ class_name GdUnitTestCaseDto extends GdUnitResourceDto var _line_number :int = -1 +var _script_path: String var _test_case_names :PackedStringArray = [] +@warning_ignore("unsafe_method_access") func serialize(test_case :Node) -> Dictionary: var serialized := super.serialize(test_case) if test_case.has_method("line_number"): serialized["line_number"] = test_case.line_number() else: serialized["line_number"] = test_case.get("LineNumber") + if test_case.has_method("script_path"): + serialized["script_path"] = test_case.script_path() + else: + # TODO 'script_path' needs to be implement in c# the the + # serialized["script_path"] = test_case.get("ScriptPath") + serialized["script_path"] = serialized["resource_path"] if test_case.has_method("test_case_names"): serialized["test_case_names"] = test_case.test_case_names() elif test_case.has_method("TestCaseNames"): @@ -18,9 +26,11 @@ func serialize(test_case :Node) -> Dictionary: return serialized -func deserialize(data :Dictionary) -> GdUnitResourceDto: +func deserialize(data :Dictionary) -> GdUnitTestCaseDto: + @warning_ignore("return_value_discarded") super.deserialize(data) _line_number = data.get("line_number", -1) + _script_path = data.get("script_path", data.get("resource_path", "")) _test_case_names = data.get("test_case_names", []) return self @@ -29,5 +39,9 @@ func line_number() -> int: return _line_number +func script_path() -> String: + return _script_path + + func test_case_names() -> PackedStringArray: return _test_case_names diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd index edbae381..c5d4501c 100644 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd @@ -21,6 +21,7 @@ func serialize(test_suite :Node) -> Dictionary: func deserialize(data :Dictionary) -> GdUnitResourceDto: + @warning_ignore("return_value_discarded") super.deserialize(data) var test_cases_ :Array = data.get("test_cases", []) for test_case :Dictionary in test_cases_: diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd b/addons/gdUnit4/src/report/GdUnitByPathReport.gd index d08600e9..d857ad64 100644 --- a/addons/gdUnit4/src/report/GdUnitByPathReport.gd +++ b/addons/gdUnit4/src/report/GdUnitByPathReport.gd @@ -2,8 +2,8 @@ class_name GdUnitByPathReport extends GdUnitReportSummary -func _init(path :String, report_summaries :Array[GdUnitReportSummary]) -> void: - _resource_path = path +func _init(p_path :String, report_summaries :Array[GdUnitReportSummary]) -> void: + _resource_path = p_path _reports = report_summaries @@ -11,7 +11,7 @@ func _init(path :String, report_summaries :Array[GdUnitReportSummary]) -> void: static func sort_reports_by_path(report_summaries :Array[GdUnitReportSummary]) -> Dictionary: var by_path := Dictionary() for report in report_summaries: - var suite_path :String = report.path() + var suite_path :String = ProjectSettings.localize_path(report.path()) var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) suite_report.append(report) by_path[suite_path] = suite_report @@ -27,6 +27,7 @@ func create_record(report_link :String) -> String: func write(report_dir :String) -> String: + calculate_summary() var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/folder_report.html") var path_report := GdUnitHtmlPatterns.build(template, self, "") path_report = apply_testsuite_reports(report_dir, path_report, _reports) @@ -34,6 +35,7 @@ func write(report_dir :String) -> String: var output_path := "%s/path/%s.html" % [report_dir, path().replace("/", ".")] var dir := output_path.get_base_dir() if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(dir) FileAccess.open(output_path, FileAccess.WRITE).store_string(path_report) return output_path @@ -41,9 +43,18 @@ func write(report_dir :String) -> String: func apply_testsuite_reports(report_dir :String, template :String, test_suite_reports :Array[GdUnitReportSummary]) -> String: var table_records := PackedStringArray() - for report in test_suite_reports: - if report is GdUnitTestSuiteReport: - var test_suite_report := report as GdUnitTestSuiteReport - var report_link := test_suite_report.output_path(report_dir).replace(report_dir, "..") - table_records.append(test_suite_report.create_record(report_link)) + for report:GdUnitTestSuiteReport in test_suite_reports: + var report_link := report.output_path(report_dir).replace(report_dir, "..") + @warning_ignore("return_value_discarded") + table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func calculate_summary() -> void: + for report:GdUnitTestSuiteReport in get_reports(): + _error_count += report.error_count() + _failure_count += report.failure_count() + _orphan_count += report.orphan_count() + _skipped_count += report.skipped_count() + _flaky_count += report.flaky_count() + _duration += report.duration() diff --git a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd index 1d9cf860..722c528f 100644 --- a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd +++ b/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd @@ -2,48 +2,77 @@ class_name GdUnitHtmlPatterns extends RefCounted const TABLE_RECORD_TESTSUITE = """ - - ${testsuite_name} + + ${testsuite_name} + ${report_state_label} ${test_count} ${skipped_count} + ${flaky_count} ${failure_count} ${orphan_count} ${duration} - ${success_percent} + +
+
+
+
+
+
+
+ """ const TABLE_RECORD_PATH = """ - + ${path} + ${report_state_label} ${test_count} ${skipped_count} + ${flaky_count} ${failure_count} ${orphan_count} ${duration} - ${success_percent} + +
+
+
+
+
+
+
+ """ const TABLE_REPORT_TESTSUITE = """ - - TestSuite hooks + + TestSuite hooks n/a ${orphan_count} ${duration} - ${failure-report} + +
+${failure-report}
+										
+ """ const TABLE_RECORD_TESTCASE = """ - - ${testcase_name} + + ${testcase_name} + ${report_state_label} ${skipped_count} ${orphan_count} ${duration} - ${failure-report} + +
+${failure-report}
+										
+ """ @@ -53,16 +82,25 @@ const TABLE_BY_TESTCASES = "${report_table_tests}" # the report state success, error, warning const REPORT_STATE = "${report_state}" +const REPORT_STATE_LABEL = "${report_state_label}" const PATH = "${path}" +const RESOURCE_PATH = "${resource_path}" const TESTSUITE_COUNT = "${suite_count}" const TESTCASE_COUNT = "${test_count}" const FAILURE_COUNT = "${failure_count}" +const FLAKY_COUNT = "${flaky_count}" const SKIPPED_COUNT = "${skipped_count}" const ORPHAN_COUNT = "${orphan_count}" const DURATION = "${duration}" const FAILURE_REPORT = "${failure-report}" const SUCCESS_PERCENT = "${success_percent}" +const QUICK_STATE_PASSED = "${passed-percent}" +const QUICK_STATE_FLAKY = "${flaky-percent}" +const QUICK_STATE_ERROR = "${error-percent}" +const QUICK_STATE_FAILED = "${failed-percent}" +const QUICK_STATE_WARNING = "${warning-percent}" + const TESTSUITE_NAME = "${testsuite_name}" const TESTCASE_NAME = "${testcase_name}" const REPORT_LINK = "${report_link}" @@ -74,21 +112,47 @@ static func current_date() -> String: return Time.get_datetime_string_from_system(true, true) -static func build(template :String, report :GdUnitReportSummary, report_link :String) -> String: +static func build(template: String, report: GdUnitReportSummary, report_link: String) -> String: return template\ - .replace(PATH, report.path())\ - .replace(TESTSUITE_NAME, report.name())\ + .replace(PATH, get_report_path(report))\ + .replace(BREADCRUMP_PATH_LINK, get_path_as_link(report))\ + .replace(RESOURCE_PATH, report.resource_path())\ + .replace(TESTSUITE_NAME, report.name_html_encoded())\ .replace(TESTSUITE_COUNT, str(report.suite_count()))\ .replace(TESTCASE_COUNT, str(report.test_count()))\ .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\ + .replace(FLAKY_COUNT, str(report.flaky_count()))\ .replace(SKIPPED_COUNT, str(report.skipped_count()))\ .replace(ORPHAN_COUNT, str(report.orphan_count()))\ .replace(DURATION, LocalTime.elapsed(report.duration()))\ .replace(SUCCESS_PERCENT, report.calculate_succes_rate(report.test_count(), report.error_count(), report.failure_count()))\ - .replace(REPORT_STATE, report.report_state())\ + .replace(REPORT_STATE, report.report_state().to_lower())\ + .replace(REPORT_STATE_LABEL, report.report_state())\ + .replace(QUICK_STATE_PASSED, calculate_percentage(report.test_count(), report.success_count()))\ + .replace(QUICK_STATE_FLAKY, calculate_percentage(report.test_count(), report.flaky_count()))\ + .replace(QUICK_STATE_ERROR, calculate_percentage(report.test_count(), report.error_count()))\ + .replace(QUICK_STATE_FAILED, calculate_percentage(report.test_count(), report.failure_count()))\ + .replace(QUICK_STATE_WARNING, calculate_percentage(report.test_count(), 0))\ .replace(REPORT_LINK, report_link)\ .replace(BUILD_DATE, current_date()) static func load_template(template_name :String) -> String: return FileAccess.open(template_name, FileAccess.READ).get_as_text() + + +static func get_path_as_link(report: GdUnitReportSummary) -> String: + return "../path/%s.html" % report.path().replace("/", ".") + + +static func get_report_path(report: GdUnitReportSummary) -> String: + var path := report.path() + if path.is_empty(): + return "/" + return path + + +static func calculate_percentage(p_test_count: int, count: int) -> String: + if count <= 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd index fda9833e..94bee48f 100644 --- a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd +++ b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd @@ -7,50 +7,97 @@ var _report_path :String var _iteration :int -func _init(report_path :String) -> void: - _iteration = GdUnitFileAccess.find_last_path_index(report_path, REPORT_DIR_PREFIX) + 1 +func _init(report_path :String, max_reports: int) -> void: + if max_reports > 1: + _iteration = GdUnitFileAccess.find_last_path_index(report_path, REPORT_DIR_PREFIX) + 1 + else: + _iteration = 1 _report_path = "%s/%s%d" % [report_path, REPORT_DIR_PREFIX, _iteration] + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(_report_path) -func add_testsuite_report(suite_report :GdUnitTestSuiteReport) -> void: - _reports.append(suite_report) +func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void: + _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count)) @warning_ignore("shadowed_variable") -func add_testcase_report(resource_path :String, suite_report :GdUnitTestCaseReport) -> void: - for report in _reports: +func add_testcase(resource_path :String, suite_name :String, test_name: String) -> void: + for report:GdUnitTestSuiteReport in _reports: if report.resource_path() == resource_path: - report.add_report(suite_report) - - -@warning_ignore("shadowed_variable") -func update_test_suite_report( - resource_path :String, - duration :int, - _is_error :bool, - is_failed: bool, - _is_warning :bool, - _is_skipped :bool, - skipped_count :int, - failed_count :int, - orphan_count :int, - reports :Array = []) -> void: - - for report in _reports: - if report.resource_path() == resource_path: - report.set_duration(duration) - report.set_failed(is_failed, failed_count) - report.set_skipped(skipped_count) - report.set_orphans(orphan_count) - report.set_reports(reports) - - -@warning_ignore("shadowed_variable") -func update_testcase_report(resource_path :String, test_report :GdUnitTestCaseReport) -> void: - for report in _reports: - if report.resource_path() == resource_path: - report.update(test_report) + var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name) + report.add_or_create_test_report(test_report) + + +func add_testsuite_reports( + p_resource_path :String, + p_error_count :int, + p_failure_count :int, + p_orphan_count :int, + p_duration :int, + p_reports :Array = []) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.resource_path() == p_resource_path: + report.set_reports(p_reports) + update_summary_counters(p_error_count, p_failure_count, p_orphan_count, 0, 0, p_duration) + + +func add_testcase_reports( + p_resource_path: String, + p_test_name: String, + p_reports: Array[GdUnitReport]) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.resource_path() == p_resource_path: + report.add_testcase_reports(p_test_name, p_reports) + + +func update_testsuite_counters( + p_resource_path :String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.resource_path() == p_resource_path: + report.update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) + update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, 0) + + +func set_testcase_counters( + p_resource_path: String, + p_test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.resource_path() == p_resource_path: + report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count, + p_is_skipped, p_is_flaky, p_duration) + + +func update_summary_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_is_skipped as int + _flaky_count += p_is_flaky as int + _duration += p_duration func write() -> String: @@ -61,6 +108,7 @@ func write() -> String: # write report var index_file := "%s/index.html" % _report_path FileAccess.open(index_file, FileAccess.WRITE).store_string(to_write) + @warning_ignore("return_value_discarded") GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/report/template/css/", _report_path + "/css") return index_file @@ -77,17 +125,20 @@ func apply_path_reports(report_dir :String, template :String, report_summaries : paths.append_array(path_report_mapping.keys()) paths.sort() for report_path in paths: - var report := GdUnitByPathReport.new(report_path, path_report_mapping.get(report_path)) + var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_path) + var report := GdUnitByPathReport.new(report_path, reports) var report_link :String = report.write(report_dir).replace(report_dir, ".") + @warning_ignore("return_value_discarded") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) -func apply_testsuite_reports(report_dir :String, template :String, test_suite_reports :Array[GdUnitReportSummary]) -> String: +func apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: var table_records := PackedStringArray() - for report in test_suite_reports: + for report: GdUnitTestSuiteReport in test_suite_reports: var report_link :String = report.write(report_dir).replace(report_dir, ".") - table_records.append(report.create_record(report_link)) + @warning_ignore("return_value_discarded") + table_records.append(report.create_record(report_link) as String) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitReportSummary.gd b/addons/gdUnit4/src/report/GdUnitReportSummary.gd index 41d423e7..e702491e 100644 --- a/addons/gdUnit4/src/report/GdUnitReportSummary.gd +++ b/addons/gdUnit4/src/report/GdUnitReportSummary.gd @@ -15,11 +15,15 @@ var _failure_count := 0 var _error_count := 0 var _orphan_count := 0 var _skipped_count := 0 +var _flaky_count := 0 var _duration := 0 var _reports :Array[GdUnitReportSummary] = [] - func name() -> String: + return _name + + +func name_html_encoded() -> String: return html_encode(_name) @@ -54,42 +58,35 @@ func test_executed_count() -> int: return test_count() - skipped_count() +func success_count() -> int: + return test_count() - error_count() - failure_count() - flaky_count() + + func error_count() -> int: - var count := _error_count - for report in _reports: - count += report.error_count() - return count + return _error_count func failure_count() -> int: - var count := _failure_count - for report in _reports: - count += report.failure_count() - return count + return _failure_count func skipped_count() -> int: - var count := _skipped_count - for report in _reports: - count += report.skipped_count() - return count + return _skipped_count + + +func flaky_count() -> int: + return _flaky_count func orphan_count() -> int: - var count := _orphan_count - for report in _reports: - count += report.orphan_count() - return count + return _orphan_count func duration() -> int: - var count := _duration - for report in _reports: - count += report.duration() - return count + return _duration -func reports() -> Array: +func get_reports() -> Array: return _reports @@ -98,21 +95,23 @@ func add_report(report :GdUnitReportSummary) -> void: func report_state() -> String: - return calculate_state(error_count(), failure_count(), orphan_count()) + return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count()) func succes_rate() -> String: return calculate_succes_rate(test_count(), error_count(), failure_count()) -func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int) -> String: +func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int) -> String: if p_error_count > 0: - return "error" + return "ERROR" if p_failure_count > 0: - return "failure" + return "FAILED" + if p_flaky_count > 0: + return "FLAKY" if p_orphan_count > 0: - return "warning" - return "success" + return "WARNING" + return "PASSED" func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: @@ -128,16 +127,12 @@ func create_summary(_report_dir :String) -> String: return "" -func html_encode(value :String) -> String: - for key in CHARACTERS_TO_ENCODE.keys() as Array[String]: - value =value.replace(key, CHARACTERS_TO_ENCODE[key]) +func html_encode(value: String) -> String: + for key: String in CHARACTERS_TO_ENCODE.keys(): + @warning_ignore("unsafe_cast") + value = value.replace(key, CHARACTERS_TO_ENCODE[key] as String) return value func convert_rtf_to_html(bbcode :String) -> String: - var as_text: = GdUnitTools.richtext_normalize(bbcode) - var converted := PackedStringArray() - var lines := as_text.split("\n") - for line in lines: - converted.append("

%s

" % line) - return "\n".join(converted) + return GdUnitTools.richtext_normalize(bbcode) diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd index deaf7908..5eec799e 100644 --- a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd @@ -6,26 +6,10 @@ var _failure_reports :Array[GdUnitReport] @warning_ignore("shadowed_variable") -func _init( - p_resource_path :String, - p_suite_name :String, - test_name :String, - is_error := false, - _is_failed := false, - failed_count :int = 0, - orphan_count :int = 0, - is_skipped := false, - failure_reports :Array[GdUnitReport] = [], - p_duration :int = 0) -> void: +func _init(p_resource_path: String, p_suite_name: String, p_test_name: String) -> void: _resource_path = p_resource_path _suite_name = p_suite_name - _name = test_name - _error_count = is_error - _failure_count = failed_count - _orphan_count = orphan_count - _skipped_count = is_skipped - _failure_reports = failure_reports - _duration = p_duration + _name = p_test_name func suite_name() -> String: @@ -34,14 +18,15 @@ func suite_name() -> String: func failure_report() -> String: var html_report := "" - for report in _failure_reports: + for report in get_test_reports(): html_report += convert_rtf_to_html(str(report)) return html_report func create_record(_report_dir :String) -> String: return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ - .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report_state())\ .replace(GdUnitHtmlPatterns.TESTCASE_NAME, name())\ .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(skipped_count()))\ .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ @@ -49,10 +34,19 @@ func create_record(_report_dir :String) -> String: .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) -func update(report :GdUnitTestCaseReport) -> void: - _error_count += report.error_count() - _failure_count += report.failure_count() - _orphan_count += report.orphan_count() - _skipped_count += report.skipped_count() - _failure_reports += report._failure_reports - _duration += report.duration() +func add_testcase_reports(reports: Array[GdUnitReport]) -> void: + _failure_reports.append_array(reports) + + +func set_testcase_counters(p_error_count: int, p_failure_count: int, p_orphan_count: int, + p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: + _error_count = p_error_count + _failure_count = p_failure_count + _orphan_count = p_orphan_count + _skipped_count = p_is_skipped + _flaky_count = p_is_flaky as int + _duration = p_duration + + +func get_test_reports() -> Array[GdUnitReport]: + return _failure_reports diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd index fa07c8de..4a62087b 100644 --- a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd @@ -1,16 +1,15 @@ class_name GdUnitTestSuiteReport extends GdUnitReportSummary -var _time_stamp :int -var _failure_reports :Array[GdUnitReport] = [] +var _time_stamp: int +var _failure_reports: Array[GdUnitReport] = [] -@warning_ignore("shadowed_variable") -func _init(resource_path :String, name :String, test_count :int) -> void: - _resource_path = resource_path - _name = name +func _init(p_resource_path: String, p_name: String, p_test_count: int) -> void: + _resource_path = p_resource_path + _name = p_name + _test_count = p_test_count _time_stamp = Time.get_unix_time_from_system() as int - _test_count = test_count func create_record(report_link :String) -> String: @@ -21,10 +20,6 @@ func output_path(report_dir :String) -> String: return "%s/test_suites/%s.%s.html" % [report_dir, path().replace("/", "."), name()] -func path_as_link() -> String: - return "../path/%s.html" % path().replace("/", ".") - - func failure_report() -> String: var html_report := "" for report in _failure_reports: @@ -34,7 +29,8 @@ func failure_report() -> String: func test_suite_failure_report() -> String: return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ - .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report_state())\ .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(_duration))\ .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) @@ -42,20 +38,22 @@ func test_suite_failure_report() -> String: func write(report_dir :String) -> String: var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/suite_report.html") - template = GdUnitHtmlPatterns.build(template, self, "")\ - .replace(GdUnitHtmlPatterns.BREADCRUMP_PATH_LINK, path_as_link()) + template = GdUnitHtmlPatterns.build(template, self, "") var report_output_path := output_path(report_dir) var test_report_table := PackedStringArray() if not _failure_reports.is_empty(): + @warning_ignore("return_value_discarded") test_report_table.append(test_suite_failure_report()) - for test_report in _reports: + for test_report: GdUnitTestCaseReport in _reports: + @warning_ignore("return_value_discarded") test_report_table.append(test_report.create_record(report_output_path)) template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) var dir := report_output_path.get_base_dir() if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(dir) FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template) return report_output_path @@ -74,23 +72,53 @@ func duration() -> int: func set_skipped(skipped :int) -> void: - _skipped_count = skipped + _skipped_count += skipped func set_orphans(orphans :int) -> void: _orphan_count = orphans -func set_failed(failed :bool, count :int) -> void: - if failed: - _failure_count += count +func set_failed(count :int) -> void: + _failure_count += count func set_reports(failure_reports :Array[GdUnitReport]) -> void: _failure_reports = failure_reports -func update(test_report :GdUnitTestCaseReport) -> void: - for report in _reports: - if report.name() == test_report.name(): - report.update(test_report) +func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void: + _reports.append(test_report) + + +func update_testsuite_counters(p_error_count: int, p_failure_count: int, p_orphan_count: int, + p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_is_skipped as int + _flaky_count += p_is_flaky as int + _duration += p_duration + + +func set_testcase_counters(test_name: String, p_error_count: int, p_failure_count: int, p_orphan_count: int, + p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: + if _reports.is_empty(): + return + var test_report:GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) + + +func add_testcase_reports(test_name: String, reports: Array[GdUnitReport] ) -> void: + if reports.is_empty(): + return + # we lookup to latest matching report because of flaky tests could be retry the tests + # and resultis in multipe report entries with the same name + var test_report:GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.add_testcase_reports(reports) diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd b/addons/gdUnit4/src/report/JUnitXmlReport.gd index a4c040dc..8921564b 100644 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd +++ b/addons/gdUnit4/src/report/JUnitXmlReport.gd @@ -14,6 +14,7 @@ const ATTR_MESSAGE := "message" const ATTR_NAME := "name" const ATTR_PACKAGE := "package" const ATTR_SKIPPED := "skipped" +const ATTR_FLAKY := "flaky" const ATTR_TESTS := "tests" const ATTR_TIME := "time" const ATTR_TIMESTAMP := "timestamp" @@ -46,6 +47,8 @@ func build_junit_report(report :GdUnitReportSummary) -> String: .attribute(ATTR_NAME, "report_%s" % _iteration)\ .attribute(ATTR_TESTS, report.test_count())\ .attribute(ATTR_FAILURES, report.failure_count())\ + .attribute(ATTR_SKIPPED, report.skipped_count())\ + .attribute(ATTR_FLAKY, report.flaky_count())\ .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ .add_childs(build_test_suites(report)) var as_string := test_suites.to_xml() @@ -55,8 +58,8 @@ func build_junit_report(report :GdUnitReportSummary) -> String: func build_test_suites(summary :GdUnitReportSummary) -> Array: var test_suites :Array[XmlElement] = [] - for index in summary.reports().size(): - var suite_report :GdUnitTestSuiteReport = summary.reports()[index] + for index in summary.get_reports().size(): + var suite_report :GdUnitTestSuiteReport = summary.get_reports()[index] var iso8601_datetime := Time.get_datetime_string_from_unix_time(suite_report.time_stamp()) test_suites.append(XmlElement.new("testsuite")\ .attribute(ATTR_ID, index)\ @@ -68,6 +71,7 @@ func build_test_suites(summary :GdUnitReportSummary) -> Array: .attribute(ATTR_FAILURES, suite_report.failure_count())\ .attribute(ATTR_ERRORS, suite_report.error_count())\ .attribute(ATTR_SKIPPED, suite_report.skipped_count())\ + .attribute(ATTR_FLAKY, suite_report.flaky_count())\ .attribute(ATTR_TIME, JUnitXmlReport.to_time(suite_report.duration()))\ .add_childs(build_test_cases(suite_report))) return test_suites @@ -75,8 +79,8 @@ func build_test_suites(summary :GdUnitReportSummary) -> Array: func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: var test_cases :Array[XmlElement] = [] - for index in suite_report.reports().size(): - var report :GdUnitTestCaseReport = suite_report.reports()[index] + for index in suite_report.get_reports().size(): + var report :GdUnitTestCaseReport = suite_report.get_reports()[index] test_cases.append( XmlElement.new("testcase")\ .attribute(ATTR_NAME, JUnitXmlReport.encode_xml(report.name()))\ .attribute(ATTR_CLASSNAME, report.suite_name())\ @@ -85,26 +89,24 @@ func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: return test_cases -func build_reports(test_report :GdUnitTestCaseReport) -> Array: +func build_reports(test_report: GdUnitTestCaseReport) -> Array: var failure_reports :Array[XmlElement] = [] - if test_report.failure_count() or test_report.error_count(): - for failure in test_report._failure_reports: - var report := failure as GdUnitReport - if report.is_failure(): - failure_reports.append( XmlElement.new("failure")\ - .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report._resource_path, report.line_number()])\ - .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ - .text(convert_rtf_to_text(report.message()))) - elif report.is_error(): - failure_reports.append( XmlElement.new("error")\ - .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report._resource_path, report.line_number()])\ - .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ - .text(convert_rtf_to_text(report.message()))) - if test_report.skipped_count(): - for failure in test_report._failure_reports: - var report := failure as GdUnitReport - failure_reports.append( XmlElement.new("skipped")\ - .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report._resource_path, report.line_number()])) + + for report: GdUnitReport in test_report.get_test_reports(): + if report.is_failure(): + failure_reports.append(XmlElement.new("failure")\ + .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report._resource_path, report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + elif report.is_error(): + failure_reports.append(XmlElement.new("error")\ + .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report._resource_path, report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .text(convert_rtf_to_text(report.message()))) + elif report.is_skipped(): + failure_reports.append(XmlElement.new("skipped")\ + .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report._resource_path, report.line_number()])\ + .text(convert_rtf_to_text(report.message()))) return failure_reports diff --git a/addons/gdUnit4/src/report/XmlElement.gd b/addons/gdUnit4/src/report/XmlElement.gd index b5d2ed38..86c74212 100644 --- a/addons/gdUnit4/src/report/XmlElement.gd +++ b/addons/gdUnit4/src/report/XmlElement.gd @@ -39,6 +39,7 @@ func add_child(child :XmlElement) -> XmlElement: func add_childs(childs :Array[XmlElement]) -> XmlElement: for child in childs: + @warning_ignore("return_value_discarded") add_child(child) return self diff --git a/addons/gdUnit4/src/report/template/css/breadcrumb.css b/addons/gdUnit4/src/report/template/css/breadcrumb.css index 2dd65fee..17215ff2 100644 --- a/addons/gdUnit4/src/report/template/css/breadcrumb.css +++ b/addons/gdUnit4/src/report/template/css/breadcrumb.css @@ -1,15 +1,12 @@ - .breadcrumb { display: flex; border-radius: 6px; overflow: hidden; height: 45px; z-index: 1; - background-color: #055d9c; - font-weight: bold; - font-size: 16px; + background-color: #9d73eb; margin-top: 0px; - margin-bottom: 20px; + margin-bottom: 10px; box-shadow: 0 0 3px black; } @@ -25,11 +22,11 @@ } .breadcrumb a:first-child { - padding-left: 5.2px; + padding-left: 5.2px; } .breadcrumb a:last-child { - padding-right: 5.2px; + padding-right: 5.2px; } .breadcrumb a:after { @@ -40,28 +37,30 @@ height: 45px; top: 0; right: -20px; - background-color: #055d9c; + background-color: #9d73eb; border-top-right-radius: 5px; transform: scale(0.707) rotate(45deg); - box-shadow: 2px -2px rgba(0,0,0,0.25); + box-shadow: 2px -2px rgba(0, 0, 0, 0.25); z-index: 1; } .breadcrumb a:last-child:after { - content: none; + content: none; } -.breadcrumb a.active, .breadcrumb a:hover { - background: #347bad; +.breadcrumb a.active, +.breadcrumb a:hover { + background: #b899f2; color: white; text-decoration: underline; } -.breadcrumb a.active:after, .breadcrumb a:hover:after { - background: #347bad; +.breadcrumb a.active:after, +.breadcrumb a:hover:after { + background: #b899f2; } .breadcrumb span { - margin:inherit; + margin: inherit; z-index: 2; } diff --git a/addons/gdUnit4/src/report/template/css/icon.png b/addons/gdUnit4/src/report/template/css/icon.png deleted file mode 100644 index eeac2924..00000000 Binary files a/addons/gdUnit4/src/report/template/css/icon.png and /dev/null differ diff --git a/addons/gdUnit4/src/report/template/css/logo.png b/addons/gdUnit4/src/report/template/css/logo.png new file mode 100644 index 00000000..12de79ff Binary files /dev/null and b/addons/gdUnit4/src/report/template/css/logo.png differ diff --git a/addons/gdUnit4/src/report/template/css/icon.png.import b/addons/gdUnit4/src/report/template/css/logo.png.import similarity index 68% rename from addons/gdUnit4/src/report/template/css/icon.png.import rename to addons/gdUnit4/src/report/template/css/logo.png.import index c1a20620..0bf55c36 100644 --- a/addons/gdUnit4/src/report/template/css/icon.png.import +++ b/addons/gdUnit4/src/report/template/css/logo.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://c8duoevjrvx65" -path="res://.godot/imported/icon.png-62697e17645466394a12e1821970bc28.ctex" +uid="uid://2n0cbwv2t23g" +path="res://.godot/imported/logo.png-37f10c549c1299049c5a6012ef6f9868.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://addons/gdUnit4/src/report/template/css/icon.png" -dest_files=["res://.godot/imported/icon.png-62697e17645466394a12e1821970bc28.ctex"] +source_file="res://addons/gdUnit4/src/report/template/css/logo.png" +dest_files=["res://.godot/imported/logo.png-37f10c549c1299049c5a6012ef6f9868.ctex"] [params] diff --git a/addons/gdUnit4/src/report/template/css/style.css b/addons/gdUnit4/src/report/template/css/style.css deleted file mode 100644 index 747d57cb..00000000 --- a/addons/gdUnit4/src/report/template/css/style.css +++ /dev/null @@ -1,312 +0,0 @@ - -body { - margin: 0; - padding: 0; - font-family: sans-serif; - font-size: 12pt; - background: #dbf1ff; -} - -footer { - position: fixed; - left: 0; - bottom: 0; - width: 100%; - height: 30px; - font-size: 14px; - white-space: nowrap; - background-image: linear-gradient(to bottom right, #347bad, #055d9c); -} - -footer a { - padding-left: 5px; -} - -footer p { - padding-left: 10px; -} - -.header { - padding-top: 5px; - padding-bottom: 5px; - width: 100%; - height: 100px; - text-align: left; - background-image: linear-gradient(to bottom right, #347bad, #055d9c); -} - -.header h1 { - font-size: 200%; - display: flex; - line-height: 64px; - text-align: center; - text-shadow: 2px 2px 5px black; -} - -.header h1 .color1 { - color: #9887c4; -} - -.header h1 .color2 { - color: #7a57d6; -} - -.header h1 .color3 { - color: #2fa5f5; - padding-left: 10px; -} - -.header img { - padding-left: 50px; - width: 64px; - height: 64px; - padding-right: 10px; -} - -.content { - padding-left: 50px; - padding-right: 50px; - padding-top: 30px; - padding-bottom: 30px; -} - -.content h1 { - font-size: 130%; - text-align: left; - margin-bottom: 10px; - margin: 0; - padding: 0; -} - -.grid-container { - display: grid; - grid-template-columns: 420px 400px auto; - margin-bottom: 20px; -} - -.grid-item { - border-radius: 15px; - border: 2px solid #9887c4; - padding: 10px; - margin: 5px; - box-shadow: 0 0 10px #9887c4; -} - -.summary table { - border-collapse: collapse; - border-collapse: inherit; - border-spacing: 10px 10px; -} - -.summary cell { - width: 110px; - font-size: 110%; - text-align: left; -} - -.summary .counter, .percent { - width: 300px; - font-size: 120%; - font-weight: bold; - text-align: right; - box-shadow: 0 0 3px black; -} - -.report { - margin-top: 10px; - margin-bottom: 10px; -} - -/* Style tab links */ -input, section { - clear: both; - padding-top: 0px; - padding-bottom: 0px; - display: none; - background-color: transparent; - box-shadow: 0 0 5px black; -} - -label { - width: auto !important; - min-width: 200px; - height: 20px; - - font-weight: bold; - font-size: 16px; - color: white; - - margin: auto; - display: flex; - float: left; - justify-content: space-between; - - padding-top: 18px; - padding-left: 10px; - padding-right: 10px; - padding-bottom: 9px; - - border-top: 1px solid #DDD; - border-right: 1px solid #DDD; - border-left: 1px solid #DDD; - - -moz-border-radius: 7px; - border-radius: 8px 4px 0px 0px; - text-decoration: none; - background-color: #055d9c -} - -label:hover { - cursor: pointer; - text-decoration: underline; -} - -#tab1:checked ~ #content1, #tab2:checked ~ #content2, #tab3:checked ~ #content3 { - display: block; -} - -input:checked + label { - height: 14px; - border-top: 8px solid #9887c4; - border-right: 1px solid #9887c4; - border-left: 1px solid #9887c4; - border-bottom-color: transparent; - background-color: #347bad; - box-shadow: -2px -2px 2px #9887c4; -} - -div.selected { - display: block; -} - -div.deselected { - display: none; -} - -.tab-report-grid { - display: grid; - grid-template-columns: 60% 40%; - height: 400px; - margin-bottom: 20px; -} - -.grid-item { - overflow: auto; -} - -div.tab table { - border-collapse: collapse; -} - -div.tab th, div.tab table { - border-bottom: solid 1px #055d9c; -} - -div.tab th { - text-align: left; - white-space: nowrap; - padding-left: 6em; - padding-right: 10px; -} - -div.tab th:first-child { - padding-left: 5px; -} - -div.tab td { - white-space: nowrap; - padding-left: 6em; - padding-right: 10px; - padding-top: 5px; - padding-bottom: 5px; -} - -div.tab td:first-child { - padding-left: 5px; - text-align: left; -} - -div.tab td.numeric, div.tab { - text-align: right; -} - -div.tab td.report-column, th.report-column { - display:none; -} - -div.tab td.success, div.tab a.success { - color: #03a606; -} - -div.tab td.error, div.tab a.error { - color: #bf3600; -} - -div.tab td.failure, div.tab a.failure { - color: #bf3600; -} - -div.tab td.warning, div.tab a.warning { - color: #cca704; -} - -div.tab tr:hover { - background-color: #d9e7fa; - box-shadow: 0 0 5px black; -} - -div.tab tr.selected { - background-color: #347bad; -} - -div.tab th { - width: 100%; - height: 40px; - background-color: #347bad; - color: white; - font-weight: bold; -} - -.success_percent { - font-size: 750%; -} - -.border_success { - border: solid 3px #03a606; - background-image: repeating-linear-gradient(45deg, white, white 20%, #03a606); -} - -.border_error { - border: solid 3px #bf3600; - background-image: repeating-linear-gradient(45deg, #e86363, white 0%, #bf3600); -} - -.border_failure { - border: solid 3px #bf3600; - background-image: repeating-linear-gradient(45deg, #e86363, white 0%, #bf3600); -} - -div.logging { - padding: 10px; - height: 400px; - margin: 0px; - background-image: linear-gradient(to bottom right, #347bad, #055d9c); - color: white; -} - -div.logging p { - padding: 10px; - font: arial; - background-color: #dbf1ff; - height: 85%; -} - -div.report-column { - margin-top: 10px; - width: 100%; - height: 300px; - font: arial; - font-size: 16; - text-align: left; - background-color: #dbf1ff; -} diff --git a/addons/gdUnit4/src/report/template/css/styles.css b/addons/gdUnit4/src/report/template/css/styles.css new file mode 100644 index 00000000..23eac48e --- /dev/null +++ b/addons/gdUnit4/src/report/template/css/styles.css @@ -0,0 +1,468 @@ +html, +body { + display: flex; + flex-direction: column; + margin: 0; + padding: 0; + font-family: sans-serif; + background-color: white; + height: 100%; +} + +main { + flex-grow: 1; + overflow: auto; + margin: 0 10em; +} + + +header { + color: white; + padding: 1px; + position: relative; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); +} + +.logo { + position: fixed; + top: 20px; + left: 20px; + display: flex; + align-items: center; + z-index: 1000; + filter: grayscale(1); + mix-blend-mode: plus-lighter; +} + +.logo img { + width: 64px; + height: 64px; +} + +.logo span { + font-size: 1.2em; + color: lightslategray; +} + +.report-container { + margin: 0 15em; + text-align: center; + margin-top: 60px; + flex-grow: 0; +} + +h1 { + margin: 0 0 20px 0; + font-size: 2.5em; + font-weight: normal; +} + +.summary { + display: inline-flex; + justify-content: center; + flex-wrap: nowrap; + margin-bottom: 20px; + align-items: baseline; + max-width: 960px; +} + +.summary-item { + flex: 1; + min-width: 80px; +} + +.label { + font-size: 1em; + flex-wrap: nowrap; +} + +.value { + font-size: 0.9em; + display: block; + padding-top: 10px; + color: lightgray; +} + +.success-rate { + padding-left: 40px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.check-icon { + background-color: #34c538; + color: white; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4em; +} + +.rate-text { + text-align: center; + flex-wrap: nowrap; +} + +.percentage { + font-size: 1.2em; + font-weight: bold; +} + + +nav { + padding: 20px 0px; + font-family: monospace; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + justify-content: flex-start; + border-bottom: 1px solid lightgray; +} + +nav li { + cursor: pointer; + padding: 5px 20px; + font-size: 1.1em; + color: lightslategray; +} + +nav li.active { + color: darkslategray; + border-bottom: 1px solid darkslategray; + font-weight: bold; +} + +div#content { + height: calc(100vh - 400px); +} + + +table { + width: 100%; + height: 100%; + border-collapse: collapse; + overflow: hidden; +} + +thead th { + position: sticky; + top: 0; + background-color: white; + z-index: 1; + border-bottom: 2px solid #ddd; +} + +tbody { + display: block; + /* Limit the height of the table body */ + max-height: calc(100vh - 400px); + /* Enable scrolling on the table body */ + overflow-y: auto; +} + +thead, +tbody tr { + display: table; + width: 100%; + table-layout: fixed; +} + +tbody td { + overflow: hidden; +} + +/* Ensure scrollbar visibility */ +tbody::-webkit-scrollbar { + height: 4px; + width: 14px; +} + +tbody::-webkit-scrollbar-thumb { + background-color: #aaa6a6; + border-radius: 4px; +} + +tbody::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +th, +td { + font-size: .9em; + padding: 5px 0px; + border-bottom: 1px solid #eee; + color: lightslategrey; + text-align: left; + text-wrap: nowrap; + /* Default max and min width for all columns */ + max-width: 150px; + min-width: 80px; + width: 80px; +} + +th { + font-size: 1em; + font-weight: normal; + padding-top: 20px; + color: gray; + text-wrap: nowrap; +} + +.tab-report { + display: grid; + grid-template-columns: 100%; + margin-bottom: 20px; +} + +.tab-report-grid { + display: grid; + grid-template-columns: 70% 30%; + margin-bottom: 20px; +} + + +/* Specific styling for the first column (Testcase) */ +th:first-child, +td:first-child { + padding-left: 5px; + text-align: left; + /* Max width for the first column */ + min-width: 249px; + width: 250px; + /* Enable scrollbar if content exceeds max-width */ + white-space: nowrap; + overflow: auto; +} + +/* Scrollbar styles for first column */ +td:first-child { + overflow-x: auto; + text-overflow: initial; +} + +/* Scrollbar appearance */ +td:first-child::-webkit-scrollbar { + height: 6px; +} + +td:first-child::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; +} + +td:first-child::-webkit-scrollbar-track { + background-color: #f1f1f1; +} + +/* Max width for Result column */ +th:nth-child(2), +td:nth-child(2) { + max-width: 140px; + min-width: 140px; + width: 140px; +} + +/* Max width for Quick Results column */ +th:nth-child(9), +td:nth-child(9) { + max-width: 140px; + min-width: 140px; + width: 140px; + padding-right: 10px; +} + +/* Background color for alternating groups */ +.group-bg-1 { + background-color: #f1f1f1; +} + +.group-bg-2 { + background-color: #e0e0e0; +} + +.grid-item { + overflow: auto; + padding-left: 20px; + color: lightslategrey; + max-height: calc(100vh - 350px); +} + +div.tab td.report-column, +th.report-column { + display: none; +} + +/* Result status styles */ +.status { + padding: 2px 40px; + border-radius: 6px; + color: black; + width: 40px; + display: flex; + align-content: center; + align-items: center; +} + +.status-bar { + display: flex; + border-radius: 8px; + overflow: hidden; + height: 20px; + flex-wrap: nowrap; + justify-content: space-evenly; +} + +.status-bar-column { + margin: -2px; + color: black; + display: flex; + align-content: center; + align-items: center; + transition: width 0.3s ease; +} + +.status-passed { + background-color: #63bb38; +} + +.status-error { + background-color: #fd1100; +} + +.status-failed { + background-color: #ed594f; +} + +.status-flaky { + background-color: #1d9a1f; +} + +.status-warning { + background-color: #fdda3f; +} + +div.tab tr:hover { + background-color: #d9e7fa; + box-shadow: 0 0 5px black; +} + +div.tab tr.selected { + background-color: #d9e7fa; +} + +div.report-column { + margin-top: 10px; + width: 100%; + text-align: left; +} + +.logging-container { + width: 100%; + height: 100%; +} + +div.godot-report-frame { + margin: 10px; + font-family: monospace; + height: 100%; + background-color: #eee; +} + +div.include-footer { + position: fixed; + bottom: 0; + width: 100%; + display: flex; +} + +footer { + position: fixed; + padding-left: 10em; + left: 0; + bottom: 0; + width: 100%; + white-space: nowrap; + color: lightgray; + font-size: 12px; + background-image: linear-gradient(to bottom right, #8058e3, #9d73eb); + display: flex; + justify-content: space-between; + align-items: center; +} + +footer .status-legend { + display: flex; + gap: 15px; + width: 500px; +} + +footer a { + color: lightgray; +} + +footer a:hover { + color: whitesmoke; +} + +footer a:visited { + color: whitesmoke; +} + +.status-legend-item { + display: flex; + align-items: center; + gap: 5px; +} + +.status-box { + width: 15px; + height: 15px; + border-radius: 3px; + display: inline-block; +} + +/* Normal link */ +a { + color: lightslategrey; +} + +/* Link when hovered */ +a:hover { + color: #9d73eb; +} + +/* Visited link */ +a:visited { + color: #8058e3; +} + +/* Active link (while being clicked) */ +a:active { + color: #8058e3; + /* Custom color when link is clicked */ +} + + +@media (max-width: 1024px) { + .summary { + flex-direction: column; + } + + nav ul { + flex-wrap: wrap; + } + + nav li { + margin-right: 10px; + margin-bottom: 5px; + } +} diff --git a/addons/gdUnit4/src/report/template/folder_report.html b/addons/gdUnit4/src/report/template/folder_report.html index 6f217725..c6df9810 100644 --- a/addons/gdUnit4/src/report/template/folder_report.html +++ b/addons/gdUnit4/src/report/template/folder_report.html @@ -1,99 +1,119 @@ + - - - Testsuite Results - - + + + + + GdUnit4 Testsuite + + - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
TestSuites${suite_count}
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
-
-
-

Success Rate

- - - - -
${success_percent}
+
+ +
+

Report by Paths

+
+ ${resource_path}
-
-

History

-

Coming Next

+
+
+ TestSuites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
- +
+
-
- - - -
-
- - - - - - - - - - - - - - ${report_table_testsuites} - -
TestsSkippedFailuresOrphansDurationSuccess rate
-
-
+
+
+ + + + + + + + + + + + + + + + ${report_table_testsuites} + +
TestSuitesResultTestsSkippedFlakyFailuresOrphansDurationSuccess rate
+
-
+
-

Generated byGdUnit4 at ${buid_date}

+

Generated by GdUnit4 at ${buid_date}

+
+ + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
- + diff --git a/addons/gdUnit4/src/report/template/index.html b/addons/gdUnit4/src/report/template/index.html index 2f6571e6..1c0e7694 100644 --- a/addons/gdUnit4/src/report/template/index.html +++ b/addons/gdUnit4/src/report/template/index.html @@ -1,123 +1,161 @@ - + + - - - Report Summary - + + + GdUnit4 Report + -
-

GdUnit4 Report

-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
TestSuites${suite_count}
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
-
-
-

Success Rate

- - - - -
${success_percent}
-
-
-

History

-

Coming Next

+
+ +
+

Summary Report

+
+
+ Test Suites + ${suite_count} +
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
+
+
+ +
+ +
+
-
-

Reports

-
- - - - - - +
+

Generated by GdUnit4 at ${buid_date}

+
+ + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
+
-
-
+ + diff --git a/addons/gdUnit4/src/report/template/suite_report.html b/addons/gdUnit4/src/report/template/suite_report.html index 94fa47dd..bf55cf2f 100644 --- a/addons/gdUnit4/src/report/template/suite_report.html +++ b/addons/gdUnit4/src/report/template/suite_report.html @@ -2,10 +2,12 @@ + - Testsuite results - + + GdUnit4 Testsuite + @@ -24,86 +26,149 @@ - - -
-
-
- - - - - - - - - - - - - - - - - - - - - -
Tests${test_count}
Skipped${skipped_count}
Failures${failure_count}
Orphans${orphan_count}
Duration${duration}
-
-
-

Success Rate

- - - - -
${success_percent}
+
+ +
+

Testsuite Report

+
+ ${resource_path}
-
-

History

-

Coming Next

+
+
+ Tests + ${test_count} +
+
+ Skipped + ${skipped_count} +
+
+ Flaky + ${flaky_count} +
+
+ Failures + ${failure_count} +
+
+ Orphans + ${orphan_count} +
+
+ Duration + ${duration} +
+
+
✓
+
+ Success Rate + ${success_percent} +
+
- +
-
-
-
- - - - - - - - - - - - ${report_table_tests} - -
TestcaseSkippedOrphansDurationReport
-
-
-

Failure Report

-
-
+
+
+
+ + + + + + + + + + + + + ${report_table_tests} + +
TestcaseResultSkippedOrphansDurationReport
+
+
+

Failure Report

+
-
+
-

Generated byGdUnit4 at ${buid_date}

+

Generated by GdUnit4 at ${buid_date}

+
+ + Passed + + + Flaky + + + Warning + + + Failed + + + Error + +
- \ No newline at end of file + + + diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd index 03a184e7..3bd4b2ea 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -3,33 +3,50 @@ extends GdUnitClassDoubler const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") +const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] -static func build(to_spy :Variant, debug_write := false) -> Variant: +static func build(to_spy: Variant, debug_write := false) -> Variant: if GdObjects.is_singleton(to_spy): - push_error("Spy on a Singleton is not allowed! '%s'" % to_spy.get_class()) + @warning_ignore("unsafe_cast") + push_error("Spy on a Singleton is not allowed! '%s'" % (to_spy as Object).get_class()) return null + # if resource path load it before if GdObjects.is_scene_resource_path(to_spy): - if not FileAccess.file_exists(to_spy): - push_error("Can't build spy on scene '%s'! The given resource not exists!" % to_spy) + var scene_resource_path :String = to_spy + if not FileAccess.file_exists(scene_resource_path): + push_error("Can't build spy on scene '%s'! The given resource not exists!" % scene_resource_path) return null - to_spy = load(to_spy) + var scene_to_spy: PackedScene = load(scene_resource_path) + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) # spy checked PackedScene if GdObjects.is_scene(to_spy): - return spy_on_scene(to_spy.instantiate(), debug_write) + var scene_to_spy: PackedScene = to_spy + return spy_on_scene(scene_to_spy.instantiate() as Node, debug_write) # spy checked a scene instance if GdObjects.is_instance_scene(to_spy): - return spy_on_scene(to_spy, debug_write) + @warning_ignore("unsafe_cast") + return spy_on_scene(to_spy as Node, debug_write) + + var excluded_functions := [] + if to_spy is Callable: + @warning_ignore("unsafe_cast") + to_spy = CallableDoubler.new(to_spy as Callable) + excluded_functions = CallableDoubler.excluded_functions() - var spy := spy_on_script(to_spy, [], debug_write) + var spy := spy_on_script(to_spy, excluded_functions, debug_write) if spy == null: return null - var spy_instance :Variant = spy.new() - copy_properties(to_spy, spy_instance) + var spy_instance :Object = spy.new() + @warning_ignore("unsafe_cast") + copy_properties(to_spy as Object, spy_instance) + @warning_ignore("return_value_discarded") GdUnitObjectInteractions.reset(spy_instance) + @warning_ignore("unsafe_method_access") spy_instance.__set_singleton(to_spy) # we do not call the original implementation for _ready and all input function, this is actualy done by the engine + @warning_ignore("unsafe_method_access") spy_instance.__exclude_method_call([ "_input", "_gui_input", "_input_event", "_unhandled_input"]) return register_auto_free(spy_instance) @@ -46,7 +63,7 @@ static func get_class_info(clazz :Variant) -> Dictionary: static func spy_on_script(instance :Variant, function_excludes :PackedStringArray, debug_write :bool) -> GDScript: if GdArrayTools.is_array_type(instance): if GdUnitSettings.is_verbose_assert_errors(): - push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % instance.get_class()) + push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % type_string(typeof(instance))) return null var class_info := get_class_info(instance) var clazz_name :String = class_info.get("class_name") @@ -55,8 +72,10 @@ static func spy_on_script(instance :Variant, function_excludes :PackedStringArra if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) return null - var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance) - lines += double_functions(instance, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) + @warning_ignore("unsafe_cast") + var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance as Object) + @warning_ignore("unsafe_cast") + lines += double_functions(instance as Object, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) var spy := GDScript.new() spy.source_code = "\n".join(lines) @@ -64,7 +83,9 @@ static func spy_on_script(instance :Variant, function_excludes :PackedStringArra spy.resource_path = GdUnitFileAccess.create_temp_dir("spy") + "/Spy%s_%d.gd" % [clazz_name, Time.get_ticks_msec()] if debug_write: + @warning_ignore("return_value_discarded") DirAccess.remove_absolute(spy.resource_path) + @warning_ignore("return_value_discarded") ResourceSaver.save(spy, spy.resource_path) var error := spy.reload(true) if error != OK: @@ -79,7 +100,8 @@ static func spy_on_scene(scene :Node, debug_write :bool) -> Object: push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) return null # buils spy checked original script - var scene_script :Object = scene.get_script().new() + @warning_ignore("unsafe_cast") + var scene_script :Object = (scene.get_script() as GDScript).new() var spy := spy_on_script(scene_script, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) scene_script.free() if spy == null: @@ -89,9 +111,6 @@ static func spy_on_scene(scene :Node, debug_write :bool) -> Object: return register_auto_free(scene) -const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] - - static func copy_properties(source :Object, dest :Object) -> void: for property in source.get_property_list(): var property_name :String = property["name"] diff --git a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd index 20b8e896..c20061b1 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd @@ -3,13 +3,13 @@ extends GdFunctionDoubler const TEMPLATE_RETURN_VARIANT = """ - var args :Array = ["$(func_name)", $(arguments)] + var args__: Array = ["$(func_name)", $(arguments)] if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return ${default_return_value} else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) if $(instance)__do_call_real_func("$(func_name)"): return $(await)super($(arguments)) @@ -19,13 +19,13 @@ const TEMPLATE_RETURN_VARIANT = """ const TEMPLATE_RETURN_VOID = """ - var args :Array = ["$(func_name)", $(arguments)] + var args__: Array = ["$(func_name)", $(arguments)] if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) if $(instance)__do_call_real_func("$(func_name)"): $(await)super($(arguments)) @@ -34,31 +34,50 @@ const TEMPLATE_RETURN_VOID = """ const TEMPLATE_RETURN_VOID_VARARG = """ - var varargs :Array = __filter_vargs([$(varargs)]) - var args :Array = ["$(func_name)", $(arguments)] + varargs + var varargs__: Array = __filter_vargs([$(varargs)]) + var args__: Array = ["$(func_name)", $(arguments)] + varargs__ if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) - $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs) + $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs__) """ const TEMPLATE_RETURN_VARIANT_VARARG = """ - var varargs :Array = __filter_vargs([$(varargs)]) - var args :Array = ["$(func_name)", $(arguments)] + varargs + var varargs__: Array = __filter_vargs([$(varargs)]) + var args__: Array = ["$(func_name)", $(arguments)] + varargs__ if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args) + $(instance)__verify_interactions(args__) return ${default_return_value} else: - $(instance)__save_function_interaction(args) + $(instance)__save_function_interaction(args__) - return $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs) + return $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs__) + +""" + + +const TEMPLATE_CALLABLE_CALL = """ + var used_arguments__ := __filter_vargs([$(arguments)]) + + if __is_verify_interactions(): + __verify_interactions(["call", used_arguments__]) + return ${default_return_value} + else: + # append possible binded values to complete the original argument list + var args__ := used_arguments__.duplicate() + args__.append_array(super.get_bound_arguments()) + __save_function_interaction(["call", args__]) + + if __do_call_real_func("call"): + return _cb.callv(used_arguments__) + return ${default_return_value} """ @@ -67,9 +86,12 @@ func _init(push_errors :bool = false) -> void: super._init(push_errors) -func get_template(return_type :Variant, is_vararg :bool) -> String: - if is_vararg: - return TEMPLATE_RETURN_VOID_VARARG if return_type == TYPE_NIL else TEMPLATE_RETURN_VARIANT_VARARG +func get_template(fd: GdFunctionDescriptor, is_callable: bool) -> String: + if is_callable and fd.name() == "call": + return TEMPLATE_CALLABLE_CALL + if fd.is_vararg(): + return TEMPLATE_RETURN_VOID_VARARG if fd.return_type() == TYPE_NIL else TEMPLATE_RETURN_VARIANT_VARARG + var return_type :Variant = fd.return_type() if return_type is StringName: return TEMPLATE_RETURN_VARIANT return TEMPLATE_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_RETURN_VARIANT diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd index 283aa1ea..ae4295b2 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -16,10 +16,12 @@ var _summary := { "error_count": 0, "failed_count": 0, "skipped_count": 0, + "flaky_count": 0, "orphan_nodes": 0 } +@warning_ignore("return_value_discarded") func _ready() -> void: init_colors() GdUnitFonts.init_fonts(output) @@ -58,6 +60,7 @@ func init_statistics(event: GdUnitEvent) -> void: _statistics["error_count"] = 0 _statistics["failed_count"] = 0 _statistics["skipped_count"] = 0 + _statistics["flaky_count"] = 0 _statistics["orphan_nodes"] = 0 _summary["total_count"] += event.total_count() @@ -72,11 +75,13 @@ func reset_statistics() -> void: func update_statistics(event: GdUnitEvent) -> void: _statistics["error_count"] += event.error_count() _statistics["failed_count"] += event.failed_count() - _statistics["skipped_count"] += event.skipped_count() + _statistics["skipped_count"] += event.is_skipped() as int + _statistics["flaky_count"] += event.is_flaky() as int _statistics["orphan_nodes"] += event.orphan_nodes() _summary["error_count"] += event.error_count() _summary["failed_count"] += event.failed_count() - _summary["skipped_count"] += event.skipped_count() + _summary["skipped_count"] += event.is_skipped() as int + _summary["flaky_count"] += event.is_flaky() as int _summary["orphan_nodes"] += event.orphan_nodes() @@ -106,7 +111,14 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: GdUnitEvent.STOP: print_message("Summary:", Color.DODGER_BLUE) - println_message("| %d total | %d error | %d failed | %d skipped | %d orphans |" % [_summary["total_count"], _summary["error_count"], _summary["failed_count"], _summary["skipped_count"], _summary["orphan_nodes"]], _text_color, 1) + println_message("| %d total | %d error | %d failed | %d flaky | %d skipped | %d orphans |" %\ + [_summary["total_count"], + _summary["error_count"], + _summary["failed_count"], + _summary["flaky_count"], + _summary["skipped_count"], + _summary["orphan_nodes"]], + _text_color, 1) print_message("[wave][/wave]") GdUnitEvent.TESTSUITE_BEFORE: @@ -115,19 +127,27 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: println_message(event._suite_name, _engine_type_color) GdUnitEvent.TESTSUITE_AFTER: - update_statistics(event) if not event.reports().is_empty(): println_message("\t" + event._suite_name, _engine_type_color) for report: GdUnitReport in event.reports(): println_message("line %s: %s" % [line_number(report), report._message], _text_color, 2) - if event.is_success(): + if event.is_success() and event.is_flaky(): + print_message("[wave]FLAKY[/wave]", Color.GREEN_YELLOW) + elif event.is_success(): print_message("[wave]PASSED[/wave]", Color.LIGHT_GREEN) else: print_message("[shake rate=5 level=10][b]FAILED[/b][/shake]", Color.FIREBRICK) - print_message(" | %d total | %d error | %d failed | %d skipped | %d orphans |" % [_statistics["total_count"], _statistics["error_count"], _statistics["failed_count"], _statistics["skipped_count"], _statistics["orphan_nodes"]]) + print_message(" | %d total | %d error | %d failed | %d flaky | %d skipped | %d orphans |" %\ + [_statistics["total_count"], + _statistics["error_count"], + _statistics["failed_count"], + _statistics["flaky_count"], + _statistics["skipped_count"], + _statistics["orphan_nodes"]]) println_message("%+12s" % LocalTime.elapsed(event.elapsed_time())) println_message(" ") + GdUnitEvent.TESTCASE_BEFORE: var spaces := "-%d" % (80 - event._suite_name.length()) print_message(event._suite_name, _engine_type_color, 1) @@ -136,13 +156,19 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: GdUnitEvent.TESTCASE_AFTER: var reports := event.reports() - update_statistics(event) - if event.is_success(): + if event.is_flaky() and event.is_success(): + var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) + print_message("[wave]FLAKY[/wave] (%d retries)" % retries, Color.GREEN_YELLOW) + elif event.is_success(): print_message("PASSED", Color.LIGHT_GREEN) elif event.is_skipped(): print_message("SKIPPED", Color.GOLDENROD) elif event.is_error() or event.is_failed(): - print_message("[wave]FAILED[/wave]", Color.FIREBRICK) + var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) + if retries > 1: + print_message("[wave]FAILED[/wave] (retry %d)" % retries, Color.FIREBRICK) + else: + print_message("[wave]FAILED[/wave]", Color.FIREBRICK) elif event.is_warning(): print_message("WARNING", Color.YELLOW) println_message(" %+12s" % LocalTime.elapsed(event.elapsed_time())) @@ -150,6 +176,9 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: for report: GdUnitReport in event.reports(): println_message("line %s: %s" % [line_number(report), report._message], _text_color, 2) + GdUnitEvent.TESTCASE_STATISTICS: + update_statistics(event) + func _on_gdunit_client_connected(client_id: int) -> void: output.clear() diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd b/addons/gdUnit4/src/ui/GdUnitFonts.gd index ad5c687a..24483454 100644 --- a/addons/gdUnit4/src/ui/GdUnitFonts.gd +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd @@ -33,11 +33,11 @@ static func init_fonts(item: CanvasItem) -> float: return 16.0 -static func load_and_resize_font(font_resource: String, size: float) -> Font: +static func load_and_resize_font(font_resource: String, size: float) -> FontFile: var font: FontFile = ResourceLoader.load(font_resource, "FontFile") if font == null: push_error("Can't load font '%s'" % font_resource) return null - var resized_font := font.duplicate() + var resized_font: FontFile = font.duplicate() resized_font.fixed_size = int(size) return resized_font diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index b82fb35b..55bcba3e 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -11,11 +11,15 @@ var _command_handler := GdUnitCommandHandler.instance() func _ready() -> void: if Engine.is_editor_hint(): _getEditorThemes() + @warning_ignore("return_value_discarded") GdUnitCommandHandler.instance().gdunit_runner_start.connect(func() -> void: - var tab_container :TabContainer = get_parent_control() - for tab_index in tab_container.get_tab_count(): - if tab_container.get_tab_title(tab_index) == "GdUnit": - tab_container.set_current_tab(tab_index) + var control :Control = get_parent_control() + # if the tab is floating we dont need to set as current + if control is TabContainer: + var tab_container :TabContainer = control + for tab_index in tab_container.get_tab_count(): + if tab_container.get_tab_title(tab_index) == "GdUnit": + tab_container.set_current_tab(tab_index) ) if Engine.is_editor_hint(): add_script_editor_context_menu() diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn index 0055b983..569eefcc 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.tscn +++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn @@ -2,11 +2,11 @@ [ext_resource type="PackedScene" uid="uid://dx7xy4dgi3wwb" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"] [ext_resource type="PackedScene" uid="uid://dva3tonxsxrlk" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"] -[ext_resource type="PackedScene" uid="uid://bf53e4y5peguj" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" uid="uid://c22l4odk7qesc" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] [ext_resource type="PackedScene" uid="uid://djp8ait0bxpsc" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"] [ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] [ext_resource type="PackedScene" uid="uid://bqfpidewtpeg0" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"] -[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] +[ext_resource type="PackedScene" uid="uid://cn5mp3tmi2gb1" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] [node name="GdUnit" type="Panel"] use_parent_material = true @@ -54,9 +54,13 @@ layout_mode = 2 [node name="event_server" parent="." instance=ExtResource("7_721no")] -[connection signal="failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="select_next_failure"] -[connection signal="failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="select_previous_failure"] [connection signal="request_discover_tests" from="VBoxContainer/Header/StatusBar" to="." method="_on_status_bar_request_discover_tests"] +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [4]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [4]] [connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] [connection signal="run_testcase" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testcase"] [connection signal="run_testsuite" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testsuite"] diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd b/addons/gdUnit4/src/ui/ScriptEditorControls.gd index 54a6eab7..5e07fe15 100644 --- a/addons/gdUnit4/src/ui/ScriptEditorControls.gd +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd @@ -76,17 +76,18 @@ static func close_open_editor_scripts() -> void: # The script is openend in the current editor and selected in the file system dock. # The line and column on which to open the script can also be specified. # The script will be open with the user-configured editor for the script's language which may be an external editor. -static func edit_script(script_path: String, line_number:=-1) -> void: +static func edit_script(script_path: String, line_number := -1) -> void: var file_system := EditorInterface.get_resource_filesystem() file_system.update_file(script_path) var file_system_dock := EditorInterface.get_file_system_dock() file_system_dock.navigate_to_path(script_path) EditorInterface.select_file(script_path) - var script := load(script_path) + var script: GDScript = load(script_path) EditorInterface.edit_script(script, line_number) static func _menu_popup() -> PopupMenu: + @warning_ignore("unsafe_method_access") return EditorInterface.get_script_editor().get_child(0).get_child(0).get_child(0).get_popup() diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd index 1c3b30d6..9c58a209 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -10,14 +10,16 @@ func _init(context_menus: Array[GdUnitContextMenuItem]) -> void: _context_menus[menu.id] = menu var popup := _menu_popup() var file_tree := _file_tree() + @warning_ignore("return_value_discarded") popup.about_to_popup.connect(on_context_menu_show.bind(popup, file_tree)) + @warning_ignore("return_value_discarded") popup.id_pressed.connect(on_context_menu_pressed.bind(file_tree)) func on_context_menu_show(context_menu: PopupMenu, file_tree: Tree) -> void: context_menu.add_separator() var current_index := context_menu.get_item_count() - var selected_test_suites := collect_testsuites(_context_menus.values()[0], file_tree) + var selected_test_suites := collect_testsuites(_context_menus.values()[0] as GdUnitContextMenuItem, file_tree) for menu_id: int in _context_menus.keys(): var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] @@ -48,12 +50,14 @@ func collect_testsuites(_menu_item: GdUnitContextMenuItem, file_tree: Tree) -> P var file_type := file_system.get_file_type(resource_path) var is_dir := DirAccess.dir_exists_absolute(resource_path) if is_dir: + @warning_ignore("return_value_discarded") selected_test_suites.append(resource_path) elif is_dir or file_type == "GDScript" or file_type == "CSharpScript": # find a performant way to check if the selected item a testsuite #var resource := ResourceLoader.load(resource_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) #prints("loaded", resource) #if resource is GDScript and menu_item.is_visible(resource): + @warning_ignore("return_value_discarded") selected_test_suites.append(resource_path) selected_item = file_tree.get_next_selected(selected_item) return selected_test_suites diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd index 7fec3bca..c32568c5 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -10,6 +10,7 @@ func _init(context_menus: Array[GdUnitContextMenuItem]) -> void: for menu in context_menus: _context_menus[menu.id] = menu _editor = EditorInterface.get_script_editor() + @warning_ignore("return_value_discarded") _editor.editor_script_changed.connect(on_script_changed) on_script_changed(active_script()) @@ -26,13 +27,13 @@ func _input(event: InputEvent) -> void: func has_editor_focus() -> bool: - return Engine.get_main_loop().root.gui_get_focus_owner() == active_base_editor() + return (Engine.get_main_loop() as SceneTree).root.gui_get_focus_owner() == active_base_editor() func on_script_changed(script: Script) -> void: if script is Script: var popups: Array[Node] = GdObjects.find_nodes_by_class(active_editor(), "PopupMenu", true) - for popup in popups: + for popup: PopupMenu in popups: if not popup.about_to_popup.is_connected(on_context_menu_show): popup.about_to_popup.connect(on_context_menu_show.bind(script, popup)) if not popup.id_pressed.is_connected(on_context_menu_pressed): diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd index e6939f43..c36b3ecb 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd @@ -6,16 +6,17 @@ signal jump_to_orphan_nodes() @onready var ICON_GREEN := GdUnitUiTools.get_icon("Unlinked", Color.WEB_GREEN) @onready var ICON_RED := GdUnitUiTools.get_color_animated_icon("Unlinked", Color.YELLOW, Color.ORANGE_RED) -@onready var _button_time := %btn_time -@onready var _time := %time_value -@onready var _orphans := %orphan_value -@onready var _orphan_button := %btn_orphan +@onready var _button_time: Button = %btn_time +@onready var _time: Label = %time_value +@onready var _orphans: Label = %orphan_value +@onready var _orphan_button: Button = %btn_orphan var total_elapsed_time := 0 var total_orphans := 0 func _ready() -> void: + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) _time.text = "" _orphans.text = "0" diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd index 44c8d6b9..4c18f114 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd @@ -1,35 +1,35 @@ @tool extends ProgressBar -@onready var bar := $"." -@onready var status := $Label -@onready var style: StyleBoxFlat = bar.get("theme_override_styles/fill") +@onready var status: Label = $Label +@onready var style: StyleBoxFlat = get("theme_override_styles/fill") func _ready() -> void: + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) style.bg_color = Color.DARK_GREEN - bar.value = 0 - bar.max_value = 0 + value = 0 + max_value = 0 update_text() func progress_init(p_max_value: int) -> void: - bar.value = 0 - bar.max_value = p_max_value + value = 0 + max_value = p_max_value style.bg_color = Color.DARK_GREEN update_text() func progress_update(p_value: int, is_failed: bool) -> void: - bar.value += p_value + value += p_value update_text() if is_failed: style.bg_color = Color.DARK_RED func update_text() -> void: - status.text = "%d:%d" % [bar.value, bar.max_value] + status.text = "%d:%d" % [value, max_value] func _on_gdunit_event(event: GdUnitEvent) -> void: @@ -40,11 +40,8 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: GdUnitEvent.DISCOVER_END: progress_init(event.total_count()) - GdUnitEvent.TESTCASE_AFTER: - # we only count when the test is finished (excluding parameterized test iterrations) - # test_name: indicates a parameterized test run - if event.test_name().find(":") == -1: - progress_update(1, event.is_failed() or event.is_error()) + GdUnitEvent.TESTCASE_STATISTICS: + progress_update(1, event.is_failed() or event.is_error()) GdUnitEvent.TESTSUITE_AFTER: progress_update(0, event.is_failed() or event.is_error()) diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd index 00750856..d3f36d83 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -1,25 +1,33 @@ @tool extends PanelContainer -signal failure_next() -signal failure_prevous() +signal select_failure_next() +signal select_failure_prevous() +signal select_error_next() +signal select_error_prevous() +signal select_flaky_next() +signal select_flaky_prevous() signal request_discover_tests() +@warning_ignore("unused_signal") signal tree_view_mode_changed(flat :bool) -@onready var _errors := %error_value -@onready var _failures := %failure_value -@onready var _button_errors := %btn_errors -@onready var _button_failures := %btn_failures -@onready var _button_failure_up := %btn_failure_up -@onready var _button_failure_down := %btn_failure_down -@onready var _button_sync := %btn_tree_sync -@onready var _button_view_mode := %btn_tree_mode -@onready var _button_sort_mode := %btn_tree_sort +@onready var _errors: Label = %error_value +@onready var _failures: Label = %failure_value +@onready var _flaky_value: Label = %flaky_value +@onready var _button_failure_up: Button = %btn_failure_up +@onready var _button_failure_down: Button = %btn_failure_down +@onready var _button_sync: Button = %btn_tree_sync +@onready var _button_view_mode: Button = %btn_tree_mode +@onready var _button_sort_mode: Button = %btn_tree_sort +@onready var _icon_errors: TextureRect = %icon_errors +@onready var _icon_failures: TextureRect = %icon_failures +@onready var _icon_flaky: TextureRect = %icon_flaky var total_failed := 0 var total_errors := 0 +var total_flaky := 0 var icon_mappings := { @@ -34,11 +42,14 @@ var icon_mappings := { } +@warning_ignore("return_value_discarded") func _ready() -> void: _failures.text = "0" _errors.text = "0" - _button_errors.icon = GdUnitUiTools.get_icon("StatusError") - _button_failures.icon = GdUnitUiTools.get_icon("StatusError", Color.SKY_BLUE) + _icon_failures.texture = GdUnitUiTools.get_icon("StatusError", Color.SKY_BLUE) + _icon_errors.texture = GdUnitUiTools.get_icon("StatusError", Color.DARK_RED) + _icon_flaky.texture = GdUnitUiTools.get_icon("CheckBox", Color.GREEN_YELLOW) + _button_failure_up.icon = GdUnitUiTools.get_icon("ArrowUp") _button_failure_down.icon = GdUnitUiTools.get_icon("ArrowDown") _button_sync.icon = GdUnitUiTools.get_icon("Loop") @@ -59,6 +70,7 @@ func _set_sort_mode_menu_options() -> void: context_menu.clear() if not context_menu.index_pressed.is_connected(_on_sort_mode_changed): + @warning_ignore("return_value_discarded") context_menu.index_pressed.connect(_on_sort_mode_changed) var configured_sort_mode := GdUnitSettings.get_inspector_tree_sort_mode() @@ -76,6 +88,7 @@ func _set_view_mode_menu_options() -> void: context_menu.clear() if not context_menu.index_pressed.is_connected(_on_tree_view_mode_changed): + @warning_ignore("return_value_discarded") context_menu.index_pressed.connect(_on_tree_view_mode_changed) var configured_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode() @@ -92,11 +105,13 @@ func normalise(value: String) -> String: return " ".join(parts) -func status_changed(errors: int, failed: int) -> void: +func status_changed(errors: int, failed: int, flaky: int) -> void: total_failed += failed total_errors += errors + total_flaky += flaky _failures.text = str(total_failed) _errors.text = str(total_errors) + _flaky_value.text = str(total_flaky) func disable_buttons(value :bool) -> void: @@ -116,29 +131,46 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: GdUnitEvent.INIT: total_failed = 0 total_errors = 0 - status_changed(0, 0) + total_flaky = 0 + status_changed(0, 0, 0) GdUnitEvent.TESTCASE_BEFORE: pass - GdUnitEvent.TESTCASE_AFTER: + GdUnitEvent.TESTCASE_STATISTICS: if event.is_error(): - status_changed(event.error_count(), 0) + status_changed(event.error_count(), 0, event.is_flaky()) else: - status_changed(0, event.failed_count()) + status_changed(0, event.failed_count(), event.is_flaky()) GdUnitEvent.TESTSUITE_BEFORE: pass GdUnitEvent.TESTSUITE_AFTER: if event.is_error(): - status_changed(event.error_count(), 0) + status_changed(event.error_count(), 0, 0) else: - status_changed(0, event.failed_count()) + status_changed(0, event.failed_count(), 0) + + +func _on_btn_error_up_pressed() -> void: + select_error_prevous.emit() + + +func _on_btn_error_down_pressed() -> void: + select_error_next.emit() func _on_failure_up_pressed() -> void: - failure_prevous.emit() + select_failure_prevous.emit() func _on_failure_down_pressed() -> void: - failure_next.emit() + select_failure_next.emit() + + +func _on_btn_flaky_up_pressed() -> void: + select_flaky_prevous.emit() + + +func _on_btn_flaky_down_pressed() -> void: + select_flaky_next.emit() func _on_tree_sync_pressed() -> void: diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn index 3ef31f8f..2c869e47 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn @@ -1,126 +1,186 @@ -[gd_scene load_steps=22 format=3 uid="uid://bf53e4y5peguj"] +[gd_scene load_steps=32 format=3 uid="uid://c22l4odk7qesc"] [ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" id="3"] -[sub_resource type="Image" id="Image_xlwc6"] +[sub_resource type="Image" id="Image_knei0"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 100, 100, 23, 255, 95, 95, 126, 255, 95, 95, 206, 255, 96, 96, 240, 255, 96, 96, 240, 255, 95, 95, 206, 255, 97, 97, 124, 255, 104, 104, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 80, 255, 96, 96, 240, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 239, 255, 96, 96, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 78, 255, 95, 95, 254, 255, 95, 95, 255, 255, 96, 96, 240, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 96, 96, 240, 255, 95, 95, 255, 255, 95, 95, 254, 255, 95, 95, 75, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 104, 104, 22, 255, 95, 95, 239, 255, 95, 95, 255, 255, 95, 95, 107, 255, 97, 97, 42, 255, 95, 95, 233, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 231, 255, 96, 96, 40, 255, 96, 96, 112, 255, 95, 95, 255, 255, 95, 95, 238, 255, 102, 102, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 124, 255, 95, 95, 255, 255, 96, 96, 240, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 95, 95, 233, 255, 96, 96, 232, 255, 99, 99, 41, 255, 255, 255, 0, 255, 96, 96, 45, 255, 95, 95, 242, 255, 95, 95, 255, 255, 96, 96, 119, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 207, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 95, 95, 235, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 202, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 242, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 95, 95, 235, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 242, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 96, 96, 232, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 98, 98, 44, 255, 95, 95, 234, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 207, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 231, 255, 99, 99, 41, 255, 255, 255, 0, 255, 96, 96, 45, 255, 98, 98, 44, 255, 255, 255, 0, 255, 95, 95, 43, 255, 95, 95, 233, 255, 95, 95, 255, 255, 95, 95, 255, 255, 96, 96, 200, 255, 255, 255, 0, 255, 255, 255, 0, 255, 95, 95, 123, 255, 95, 95, 255, 255, 96, 96, 240, 255, 96, 96, 40, 255, 255, 255, 0, 255, 96, 96, 45, 255, 95, 95, 235, 255, 95, 95, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 95, 95, 43, 255, 95, 95, 242, 255, 95, 95, 255, 255, 97, 97, 116, 255, 255, 255, 0, 255, 255, 255, 0, 255, 104, 104, 22, 255, 95, 95, 238, 255, 95, 95, 255, 255, 96, 96, 112, 255, 96, 96, 45, 255, 95, 95, 235, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 233, 255, 95, 95, 43, 255, 96, 96, 117, 255, 95, 95, 255, 255, 95, 95, 235, 255, 99, 99, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 76, 255, 95, 95, 254, 255, 95, 95, 255, 255, 95, 95, 242, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 242, 255, 95, 95, 255, 255, 95, 95, 253, 255, 95, 95, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 77, 255, 95, 95, 239, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 255, 255, 95, 95, 236, 255, 97, 97, 71, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 21, 255, 96, 96, 122, 255, 95, 95, 203, 255, 95, 95, 238, 255, 95, 95, 238, 255, 95, 95, 202, 255, 96, 96, 119, 255, 102, 102, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 75, 224, 224, 224, 188, 224, 224, 224, 238, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 224, 224, 224, 96, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 133, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 117, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 212, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 113, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 159, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 242, 224, 224, 224, 255, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 25, 224, 224, 224, 255, 224, 224, 224, 238, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 243, 224, 224, 224, 254, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 29, 224, 224, 224, 255, 224, 224, 224, 236, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 189, 224, 224, 224, 255, 225, 225, 225, 68, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 160, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 121, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 72, 224, 224, 224, 121, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 43, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 225, 225, 225, 124, 224, 224, 224, 254, 224, 224, 224, 255, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 125, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 95, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 224, 224, 224, 185, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 225, 225, 225, 159, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_6b21o"] -image = SubResource("Image_xlwc6") +[sub_resource type="ImageTexture" id="ImageTexture_jvn24"] +image = SubResource("Image_knei0") -[sub_resource type="Image" id="Image_d3qlj"] +[sub_resource type="Image" id="Image_cetp0"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 222, 232, 147, 195, 221, 242, 147, 195, 221, 250, 147, 195, 221, 254, 147, 195, 221, 254, 147, 195, 221, 250, 147, 195, 221, 242, 147, 196, 222, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 238, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 237, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 196, 222, 232, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 240, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 147, 195, 221, 241, 147, 195, 221, 255, 147, 195, 221, 253, 147, 196, 222, 232, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 242, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 250, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 250, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 242, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 253, 147, 195, 221, 234, 255, 255, 255, 0, 147, 195, 221, 234, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 196, 222, 232, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 241, 147, 195, 221, 234, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 234, 147, 195, 221, 241, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 237, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 254, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 237, 147, 195, 221, 253, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 255, 147, 195, 221, 253, 147, 195, 221, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 195, 221, 232, 147, 195, 221, 242, 147, 195, 221, 250, 147, 195, 221, 253, 147, 195, 221, 253, 147, 195, 221, 250, 147, 195, 221, 241, 147, 196, 222, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_y22c0"] -image = SubResource("Image_d3qlj") +[sub_resource type="ImageTexture" id="ImageTexture_k82x4"] +image = SubResource("Image_cetp0") -[sub_resource type="Image" id="Image_dqkot"] +[sub_resource type="Image" id="Image_f2x20"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_lxhvf"] -image = SubResource("Image_dqkot") +[sub_resource type="ImageTexture" id="ImageTexture_bs7qq"] +image = SubResource("Image_f2x20") -[sub_resource type="Image" id="Image_tuw0u"] +[sub_resource type="Image" id="Image_k73x4"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_odc2f"] -image = SubResource("Image_tuw0u") +[sub_resource type="ImageTexture" id="ImageTexture_0ck6a"] +image = SubResource("Image_k73x4") -[sub_resource type="Image" id="Image_epiia"] +[sub_resource type="Image" id="Image_ujiln"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 76, 224, 224, 224, 189, 224, 224, 224, 238, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 224, 224, 224, 96, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 135, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 117, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 212, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 225, 225, 225, 68, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 113, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 159, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 255, 225, 225, 225, 183, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 242, 224, 224, 224, 255, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 25, 224, 224, 224, 255, 224, 224, 224, 237, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 243, 224, 224, 224, 254, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 29, 224, 224, 224, 255, 224, 224, 224, 236, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 188, 224, 224, 224, 255, 225, 225, 225, 68, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 160, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 120, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 71, 225, 225, 225, 126, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 43, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 34, 225, 225, 225, 124, 224, 224, 224, 254, 224, 224, 224, 255, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 125, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 95, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 224, 224, 224, 185, 227, 227, 227, 71, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 225, 225, 225, 159, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 224, 224, 224, 105, 224, 224, 224, 192, 224, 224, 224, 244, 224, 224, 224, 238, 224, 224, 224, 197, 224, 224, 224, 105, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 207, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 226, 226, 226, 26, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 224, 224, 224, 205, 224, 224, 224, 255, 224, 224, 224, 218, 225, 225, 225, 83, 237, 237, 237, 14, 237, 237, 237, 14, 224, 224, 224, 82, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 102, 224, 224, 224, 255, 224, 224, 224, 218, 227, 227, 227, 18, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 16, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 198, 224, 224, 224, 255, 225, 225, 225, 84, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 86, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 4, 224, 224, 224, 238, 224, 224, 224, 255, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 229, 229, 229, 19, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 159, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 237, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 90, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 245, 224, 224, 224, 245, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 98, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 20, 224, 224, 224, 88, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 236, 224, 224, 224, 195, 224, 224, 224, 96, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_ulpsv"] -image = SubResource("Image_epiia") +[sub_resource type="ImageTexture" id="ImageTexture_t7ac1"] +image = SubResource("Image_ujiln") -[sub_resource type="Image" id="Image_ft60s"] +[sub_resource type="Image" id="Image_6qet5"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 200, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_vn5m0"] -image = SubResource("Image_ft60s") +[sub_resource type="ImageTexture" id="ImageTexture_03vfp"] +image = SubResource("Image_6qet5") -[sub_resource type="Image" id="Image_pj4yi"] +[sub_resource type="Image" id="Image_atf74"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_0bjic"] -image = SubResource("Image_pj4yi") +[sub_resource type="ImageTexture" id="ImageTexture_fv3i4"] +image = SubResource("Image_atf74") -[sub_resource type="Image" id="Image_ykisp"] +[sub_resource type="Image" id="Image_dd3uy"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 200, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 255, 230, 246, 246, 252, 230, 249, 249, 255, 230, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 237, 246, 246, 252, 255, 246, 246, 252, 248, 255, 255, 255, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 236, 246, 246, 252, 254, 246, 246, 252, 247, 255, 255, 255, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 253, 231, 246, 246, 253, 232, 246, 246, 252, 230, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 243, 246, 246, 252, 255, 246, 246, 252, 242, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 242, 246, 246, 252, 253, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_uvtgc"] -image = SubResource("Image_ykisp") +[sub_resource type="ImageTexture" id="ImageTexture_ab51p"] +image = SubResource("Image_dd3uy") -[sub_resource type="Image" id="Image_57ty0"] +[sub_resource type="Image" id="Image_gu8ck"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 224, 224, 224, 105, 224, 224, 224, 192, 224, 224, 224, 244, 224, 224, 224, 238, 224, 224, 224, 197, 224, 224, 224, 105, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 207, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 226, 226, 226, 26, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 224, 224, 224, 205, 224, 224, 224, 255, 224, 224, 224, 218, 225, 225, 225, 83, 237, 237, 237, 14, 237, 237, 237, 14, 224, 224, 224, 82, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 102, 224, 224, 224, 255, 224, 224, 224, 218, 227, 227, 227, 18, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 16, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 198, 224, 224, 224, 255, 225, 225, 225, 84, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 86, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 4, 224, 224, 224, 238, 224, 224, 224, 255, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 229, 229, 229, 19, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 159, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 237, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 90, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 245, 224, 224, 224, 245, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 98, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 20, 224, 224, 224, 88, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 236, 224, 224, 224, 195, 224, 224, 224, 96, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 111, 23, 255, 121, 107, 126, 255, 120, 108, 206, 255, 120, 107, 240, 255, 120, 107, 240, 255, 120, 108, 206, 255, 121, 107, 124, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 108, 80, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 121, 107, 239, 255, 123, 109, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 108, 78, 255, 120, 107, 254, 255, 120, 107, 255, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 254, 255, 122, 109, 75, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 121, 107, 239, 255, 120, 107, 255, 255, 122, 107, 107, 255, 121, 109, 42, 255, 120, 107, 233, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 231, 255, 121, 108, 40, 255, 121, 107, 112, 255, 120, 107, 255, 255, 120, 107, 238, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 124, 255, 120, 107, 255, 255, 120, 107, 240, 255, 121, 109, 42, 255, 255, 255, 0, 255, 121, 109, 42, 255, 120, 107, 233, 255, 120, 107, 232, 255, 124, 112, 41, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 119, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 207, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 121, 109, 42, 255, 255, 255, 0, 255, 121, 109, 42, 255, 121, 109, 42, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 202, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 121, 109, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 108, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 232, 255, 121, 109, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 110, 44, 255, 120, 107, 234, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 108, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 207, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 231, 255, 124, 112, 41, 255, 255, 255, 0, 255, 125, 108, 45, 255, 122, 110, 44, 255, 255, 255, 0, 255, 125, 107, 43, 255, 120, 107, 233, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 200, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 108, 123, 255, 120, 107, 255, 255, 120, 107, 240, 255, 121, 108, 40, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 234, 255, 125, 107, 43, 255, 255, 255, 0, 255, 125, 107, 43, 255, 120, 107, 242, 255, 120, 107, 255, 255, 121, 108, 116, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 107, 238, 255, 120, 107, 255, 255, 121, 107, 112, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 125, 107, 43, 255, 120, 107, 117, 255, 120, 107, 255, 255, 120, 107, 235, 255, 128, 113, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 76, 255, 120, 107, 254, 255, 120, 107, 255, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 253, 255, 120, 109, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 123, 109, 77, 255, 121, 107, 239, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 236, 255, 122, 108, 71, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 109, 21, 255, 121, 107, 122, 255, 121, 107, 203, 255, 120, 107, 238, 255, 120, 107, 238, 255, 120, 107, 202, 255, 120, 107, 119, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_2tlxj"] -image = SubResource("Image_57ty0") +[sub_resource type="ImageTexture" id="ImageTexture_2rpr0"] +image = SubResource("Image_gu8ck") -[sub_resource type="Image" id="Image_ot6ar"] +[sub_resource type="Image" id="Image_1rlh2"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_143fp"] -image = SubResource("Image_ot6ar") +[sub_resource type="ImageTexture" id="ImageTexture_1oriu"] +image = SubResource("Image_1rlh2") + +[sub_resource type="Image" id="Image_7053f"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ikyhk"] +image = SubResource("Image_7053f") + +[sub_resource type="Image" id="Image_we0dj"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 198, 222, 242, 147, 197, 222, 250, 147, 197, 222, 254, 147, 197, 222, 254, 147, 197, 222, 250, 147, 198, 222, 242, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 238, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 198, 222, 253, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 198, 222, 253, 147, 197, 222, 255, 147, 198, 222, 240, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 198, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 198, 222, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 198, 222, 241, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 198, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 232, 147, 198, 222, 242, 147, 198, 222, 250, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 250, 147, 197, 222, 241, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_i2d73"] +image = SubResource("Image_we0dj") + +[sub_resource type="Image" id="Image_u8t6h"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_mph2m"] +image = SubResource("Image_u8t6h") + +[sub_resource type="Image" id="Image_x3jpw"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_k6fqi"] +image = SubResource("Image_x3jpw") + +[sub_resource type="Image" id="Image_oprg2"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 144, 239, 151, 76, 142, 239, 151, 228, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 143, 239, 152, 229, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 143, 239, 152, 175, 149, 255, 170, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 130, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 144, 244, 155, 23, 151, 244, 151, 22, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 144, 244, 155, 23, 143, 239, 151, 213, 142, 239, 152, 212, 145, 240, 152, 67, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 151, 244, 151, 22, 142, 239, 152, 212, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 142, 240, 152, 128, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 146, 243, 158, 21, 143, 239, 151, 211, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 146, 243, 158, 21, 143, 239, 152, 141, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 228, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 151, 225, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 143, 241, 154, 73, 142, 239, 151, 226, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 151, 225, 142, 241, 153, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_04e57"] +image = SubResource("Image_oprg2") [node name="StatusBar" type="PanelContainer"] clip_contents = true @@ -133,125 +193,237 @@ size_flags_horizontal = 3 size_flags_vertical = 0 script = ExtResource("3") -[node name="bar" type="HBoxContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="tree_tools" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 size_flags_vertical = 0 -[node name="errors" type="HBoxContainer" parent="bar"] +[node name="Label" type="Label" parent="VBoxContainer/tree_tools"] +layout_mode = 2 +size_flags_horizontal = 0 +text = "Statisitics" + +[node name="tree_buttons" type="HBoxContainer" parent="VBoxContainer/tree_tools"] layout_mode = 2 +size_flags_horizontal = 10 size_flags_vertical = 4 +alignment = 2 + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/tree_tools/tree_buttons"] +layout_mode = 2 -[node name="btn_errors" type="Button" parent="bar/errors"] +[node name="btn_tree_sync" type="Button" parent="VBoxContainer/tree_tools/tree_buttons"] unique_name_in_owner = true layout_mode = 2 -size_flags_horizontal = 3 -auto_translate = false -localize_numeral_system = false -text = "Errors" -icon = SubResource("ImageTexture_6b21o") +tooltip_text = "Run discover tests." +disabled = true +icon = SubResource("ImageTexture_jvn24") + +[node name="btn_tree_sort" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree sorting mode." +disabled = true +icon = SubResource("ImageTexture_k82x4") +flat = false +item_count = 4 +popup/item_0/text = "Unsorted" +popup/item_0/icon = SubResource("ImageTexture_bs7qq") +popup/item_0/checkable = 1 +popup/item_1/text = "Name ascending" +popup/item_1/icon = SubResource("ImageTexture_k82x4") +popup/item_1/checkable = 1 +popup/item_1/checked = true +popup/item_1/id = 1 +popup/item_2/text = "Name descending" +popup/item_2/icon = SubResource("ImageTexture_0ck6a") +popup/item_2/checkable = 1 +popup/item_2/id = 2 +popup/item_3/text = "Execution time" +popup/item_3/icon = SubResource("ImageTexture_t7ac1") +popup/item_3/checkable = 1 +popup/item_3/id = 3 -[node name="error_value" type="Label" parent="bar/errors"] +[node name="btn_tree_mode" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Sets tree presentaion mode." +disabled = true +icon = SubResource("ImageTexture_03vfp") +flat = false +item_count = 2 +popup/item_0/text = "Tree" +popup/item_0/icon = SubResource("ImageTexture_fv3i4") +popup/item_0/checkable = 1 +popup/item_0/checked = true +popup/item_1/text = "Flat" +popup/item_1/icon = SubResource("ImageTexture_ab51p") +popup/item_1/checkable = 1 +popup/item_1/id = 1 + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="status_bar" type="HFlowContainer" parent="VBoxContainer"] +layout_mode = 2 +last_wrap_alignment = 1 + +[node name="errors" type="HBoxContainer" parent="VBoxContainer/status_bar"] +layout_mode = 2 +size_flags_vertical = 4 + +[node name="error_value" type="Label" parent="VBoxContainer/status_bar/errors"] unique_name_in_owner = true use_parent_material = true +custom_minimum_size = Vector2(24, 0) layout_mode = 2 -size_flags_horizontal = 3 +size_flags_horizontal = 2 text = "0" -vertical_alignment = 1 -justification_flags = 160 -max_lines_visible = 1 +horizontal_alignment = 2 +justification_flags = 0 -[node name="failures" type="HBoxContainer" parent="bar"] +[node name="icon_errors" type="TextureRect" parent="VBoxContainer/status_bar/errors"] +unique_name_in_owner = true layout_mode = 2 size_flags_vertical = 4 +texture = SubResource("ImageTexture_2rpr0") +stretch_mode = 2 -[node name="btn_failures" type="Button" parent="bar/failures"] -unique_name_in_owner = true -clip_contents = true +[node name="Label" type="Label" parent="VBoxContainer/status_bar/errors"] layout_mode = 2 -size_flags_horizontal = 9 -size_flags_vertical = 3 -auto_translate = false +text = "Errors" +justification_flags = 0 + +[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/errors"] +auto_translate_mode = 2 +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 localize_numeral_system = false -tooltip_text = "Shows the total test failures." -text = "Failures" -icon = SubResource("ImageTexture_y22c0") -[node name="failure_value" type="Label" parent="bar/failures"] +[node name="btn_error_up" type="Button" parent="VBoxContainer/status_bar/errors/navigation"] +layout_mode = 2 +size_flags_vertical = 3 +tooltip_text = "Shows the total test errors." +icon = SubResource("ImageTexture_1oriu") + +[node name="btn_error_down" type="Button" parent="VBoxContainer/status_bar/errors/navigation"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 3 +tooltip_text = "Shows the total test errors." +icon = SubResource("ImageTexture_ikyhk") + +[node name="VSeparator" type="VSeparator" parent="VBoxContainer/status_bar"] +layout_mode = 2 + +[node name="failures" type="HBoxContainer" parent="VBoxContainer/status_bar"] +layout_mode = 2 +size_flags_vertical = 4 + +[node name="failure_value" type="Label" parent="VBoxContainer/status_bar/failures"] unique_name_in_owner = true use_parent_material = true +custom_minimum_size = Vector2(24, 0) layout_mode = 2 -size_flags_horizontal = 3 +size_flags_horizontal = 0 text = "0" +horizontal_alignment = 2 vertical_alignment = 1 justification_flags = 160 max_lines_visible = 1 -[node name="navigation" type="HBoxContainer" parent="bar"] +[node name="icon_failures" type="TextureRect" parent="VBoxContainer/status_bar/failures"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 4 +texture = SubResource("ImageTexture_i2d73") +stretch_mode = 2 + +[node name="Label" type="Label" parent="VBoxContainer/status_bar/failures"] layout_mode = 2 +text = "Failures" +justification_flags = 0 + +[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/failures"] +auto_translate_mode = 2 +layout_mode = 2 +size_flags_horizontal = 4 size_flags_vertical = 4 -auto_translate = false localize_numeral_system = false -[node name="btn_failure_up" type="Button" parent="bar/navigation"] +[node name="btn_failure_up" type="Button" parent="VBoxContainer/status_bar/failures/navigation"] unique_name_in_owner = true layout_mode = 2 -size_flags_horizontal = 3 size_flags_vertical = 3 tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_lxhvf") +icon = SubResource("ImageTexture_mph2m") -[node name="btn_failure_down" type="Button" parent="bar/navigation"] +[node name="btn_failure_down" type="Button" parent="VBoxContainer/status_bar/failures/navigation"] unique_name_in_owner = true layout_mode = 2 -size_flags_horizontal = 3 +size_flags_horizontal = 0 size_flags_vertical = 3 tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_odc2f") +icon = SubResource("ImageTexture_k6fqi") -[node name="tree_buttons" type="HBoxContainer" parent="bar"] +[node name="VSeparator2" type="VSeparator" parent="VBoxContainer/status_bar"] layout_mode = 2 -size_flags_horizontal = 10 -size_flags_vertical = 4 -alignment = 2 -[node name="VSeparator" type="VSeparator" parent="bar/tree_buttons"] +[node name="flaky" type="HBoxContainer" parent="VBoxContainer/status_bar"] layout_mode = 2 +size_flags_vertical = 4 -[node name="btn_tree_sync" type="Button" parent="bar/tree_buttons"] +[node name="flaky_value" type="Label" parent="VBoxContainer/status_bar/flaky"] unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(24, 0) layout_mode = 2 -tooltip_text = "Run discover tests." -icon = SubResource("ImageTexture_ulpsv") +size_flags_horizontal = 0 +text = "0" +horizontal_alignment = 2 +vertical_alignment = 1 +justification_flags = 160 +max_lines_visible = 1 -[node name="btn_tree_sort" type="MenuButton" parent="bar/tree_buttons"] +[node name="icon_flaky" type="TextureRect" parent="VBoxContainer/status_bar/flaky"] unique_name_in_owner = true layout_mode = 2 -tooltip_text = "Sets tree sorting mode." -icon = SubResource("ImageTexture_vn5m0") -item_count = 4 -popup/item_0/text = "Unsorted" -popup/item_0/icon = SubResource("ImageTexture_0bjic") -popup/item_0/checkable = 1 -popup/item_0/id = 8192 -popup/item_1/text = "Name ascending" -popup/item_1/icon = SubResource("ImageTexture_vn5m0") -popup/item_1/checkable = 1 -popup/item_1/id = 8193 -popup/item_2/text = "Name descending" -popup/item_2/icon = SubResource("ImageTexture_uvtgc") -popup/item_2/checkable = 1 -popup/item_2/id = 8194 -popup/item_3/text = "Execution time" -popup/item_3/icon = SubResource("ImageTexture_2tlxj") -popup/item_3/checkable = 1 -popup/item_3/id = 8195 +size_flags_vertical = 4 +texture = SubResource("ImageTexture_04e57") +stretch_mode = 2 -[node name="btn_tree_mode" type="MenuButton" parent="bar/tree_buttons"] -unique_name_in_owner = true +[node name="Label" type="Label" parent="VBoxContainer/status_bar/flaky"] layout_mode = 2 -tooltip_text = "Sets tree presentaion mode." -icon = SubResource("ImageTexture_143fp") +text = "Flaky" +justification_flags = 0 + +[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +auto_translate_mode = 2 +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +localize_numeral_system = false + +[node name="btn_flaky_up" type="Button" parent="VBoxContainer/status_bar/flaky/navigation"] +layout_mode = 2 +size_flags_vertical = 3 +tooltip_text = "Shows the total test errors." +icon = SubResource("ImageTexture_1oriu") -[connection signal="pressed" from="bar/navigation/btn_failure_up" to="." method="_on_failure_up_pressed"] -[connection signal="pressed" from="bar/navigation/btn_failure_down" to="." method="_on_failure_down_pressed"] -[connection signal="pressed" from="bar/tree_buttons/btn_tree_sync" to="." method="_on_tree_sync_pressed"] +[node name="btn_flaky_down" type="Button" parent="VBoxContainer/status_bar/flaky/navigation"] +layout_mode = 2 +size_flags_horizontal = 0 +size_flags_vertical = 3 +tooltip_text = "Shows the total test errors." +icon = SubResource("ImageTexture_ikyhk") + +[connection signal="pressed" from="VBoxContainer/tree_tools/tree_buttons/btn_tree_sync" to="." method="_on_tree_sync_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/errors/navigation/btn_error_up" to="." method="_on_btn_error_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/errors/navigation/btn_error_down" to="." method="_on_btn_error_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failures/navigation/btn_failure_up" to="." method="_on_failure_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failures/navigation/btn_failure_down" to="." method="_on_failure_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/navigation/btn_flaky_up" to="." method="_on_btn_flaky_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/navigation/btn_flaky_down" to="." method="_on_btn_flaky_down_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd index c3afbeca..d5872ee5 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -5,13 +5,13 @@ signal run_overall_pressed(debug: bool) signal run_pressed(debug: bool) signal stop_pressed() -@onready var _version_label := %version -@onready var _button_wiki := %help -@onready var _tool_button := %tool -@onready var _button_run_overall := %run_overall -@onready var _button_run := %run -@onready var _button_run_debug := %debug -@onready var _button_stop := %stop +@onready var _version_label: Control = %version +@onready var _button_wiki: Button = %help +@onready var _tool_button: Button = %tool +@onready var _button_run_overall: Button = %run_overall +@onready var _button_run: Button = %run +@onready var _button_run_debug: Button = %debug +@onready var _button_stop: Button = %stop @onready var settings_dlg := preload("res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn").instantiate() @@ -23,6 +23,7 @@ const SETTINGS_SHORTCUT_MAPPING := { } +@warning_ignore("return_value_discarded") func _ready() -> void: GdUnit4Version.init_version_label(_version_label) var command_handler := GdUnitCommandHandler.instance() @@ -53,6 +54,7 @@ func init_shortcuts(command_handler: GdUnitCommandHandler) -> void: _button_run_debug.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG) _button_stop.shortcut = command_handler.get_shortcut(GdUnitShortcut.ShortCut.STOP_TEST_RUN) # register for shortcut changes + @warning_ignore("return_value_discarded") GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed.bind(command_handler)) @@ -87,6 +89,7 @@ func _on_gdunit_settings_changed(_property: GdUnitProperty) -> void: func _on_wiki_pressed() -> void: + @warning_ignore("return_value_discarded") OS.shell_open("https://mikeschulze.github.io/gdUnit4/") diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd index aa385586..cb94fe88 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -14,8 +14,8 @@ const CONTEXT_MENU_EXPAND_ALL = 4 @onready var _report_list: Node = $report/ScrollContainer/list @onready var _report_template: RichTextLabel = $report/report_template @onready var _context_menu: PopupMenu = $contextMenu -@onready var _discover_hint := %discover_hint -@onready var _spinner := %spinner +@onready var _discover_hint: Control = %discover_hint +@onready var _spinner: Button = %spinner # loading tree icons @onready var ICON_SPINNER := GdUnitUiTools.get_spinner() @@ -23,6 +23,7 @@ const CONTEXT_MENU_EXPAND_ALL = 4 # gdscript icons @onready var ICON_GDSCRIPT_TEST_DEFAULT := GdUnitUiTools.get_icon("GDScript", Color.LIGHT_GRAY) @onready var ICON_GDSCRIPT_TEST_SUCCESS := GdUnitUiTools.get_GDScript_icon("StatusSuccess", Color.DARK_GREEN) +@onready var ICON_GDSCRIPT_TEST_FLAKY := GdUnitUiTools.get_GDScript_icon("CheckBox", Color.GREEN_YELLOW) @onready var ICON_GDSCRIPT_TEST_FAILED := GdUnitUiTools.get_GDScript_icon("StatusError", Color.SKY_BLUE) @onready var ICON_GDSCRIPT_TEST_ERROR := GdUnitUiTools.get_GDScript_icon("StatusError", Color.DARK_RED) @onready var ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN := GdUnitUiTools.get_GDScript_icon("Unlinked", Color.DARK_GREEN) @@ -51,6 +52,7 @@ enum STATE { RUNNING, SUCCESS, WARNING, + FLAKY, FAILED, ERROR, ABORDED, @@ -68,6 +70,7 @@ const META_GDUNIT_ORPHAN := "gdUnit_orphan" const META_GDUNIT_EXECUTION_TIME := "gdUnit_execution_time" const META_RESOURCE_PATH := "resource_path" const META_LINE_NUMBER := "line_number" +const META_SCRIPT_PATH := "script_path" const META_TEST_PARAM_INDEX := "test_param_index" var _tree_root: TreeItem @@ -110,50 +113,54 @@ func _find_by_resource_path(current: TreeItem, resource_path: String) -> TreeIte return null -func _find_first_failure(parent := _tree_root, reverse := false) -> TreeItem: +func _find_first_item_by_state(parent: TreeItem, item_state: STATE, reverse := false) -> TreeItem: var itmes := parent.get_children() if reverse: itmes.reverse() for item in itmes: - if is_test_case(item) and (is_state_error(item) or is_state_failed(item)): + if is_test_case(item) and (is_item_state(item, item_state)): return item - var failure_item := _find_first_failure(item, reverse) + var failure_item := _find_first_item_by_state(item, item_state, reverse) if failure_item != null: return failure_item return null -func _find_last_failure(parent := _tree_root) -> TreeItem: - return _find_first_failure(parent, true) +func _find_last_item_by_state(parent: TreeItem, item_state: STATE) -> TreeItem: + return _find_first_item_by_state(parent, item_state, true) -func _find_failure(current :TreeItem, prev := false) -> TreeItem: +func _find_item_by_state(current: TreeItem, item_state: STATE, prev := false) -> TreeItem: var next := current.get_prev_in_tree() if prev else current.get_next_in_tree() if next == null or next == _tree_root: return null - if is_test_case(next) and (is_state_error(next) or is_state_failed(next)): + if is_test_case(next) and is_item_state(next, item_state): return next - return _find_failure(next, prev) + return _find_item_by_state(next, item_state, prev) + + +func is_item_state(item: TreeItem, item_state: STATE) -> bool: + return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == item_state func is_state_running(item: TreeItem) -> bool: - return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.RUNNING + return is_item_state(item, STATE.RUNNING) func is_state_success(item: TreeItem) -> bool: - return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.SUCCESS + return is_item_state(item, STATE.SUCCESS) func is_state_warning(item: TreeItem) -> bool: - return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.WARNING + return is_item_state(item, STATE.WARNING) func is_state_failed(item: TreeItem) -> bool: - return item.has_meta(META_GDUNIT_STATE) and item.get_meta(META_GDUNIT_STATE) == STATE.FAILED + return is_item_state(item, STATE.FAILED) func is_state_error(item: TreeItem) -> bool: - return item.has_meta(META_GDUNIT_STATE) and (item.get_meta(META_GDUNIT_STATE) == STATE.ERROR or item.get_meta(META_GDUNIT_STATE) == STATE.ABORDED) + return is_item_state(item, STATE.ERROR) or is_item_state(item, STATE.ABORDED) func is_item_state_orphan(item: TreeItem) -> bool: @@ -172,6 +179,7 @@ func is_folder(item: TreeItem) -> bool: return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER +@warning_ignore("return_value_discarded") func _ready() -> void: _context_menu.set_item_icon(CONTEXT_MENU_RUN_ID, GdUnitUiTools.get_icon("Play")) _context_menu.set_item_icon(CONTEXT_MENU_DEBUG_ID, GdUnitUiTools.get_icon("PlayStart")) @@ -355,6 +363,7 @@ func set_state_running(item: TreeItem) -> void: if parent != _tree_root: set_state_running(parent) # force scrolling to current test case + @warning_ignore("return_value_discarded") select_item(item) @@ -366,6 +375,22 @@ func set_state_succeded(item: TreeItem) -> void: set_item_icon_by_state(item) +func set_state_flaky(item: TreeItem, event: GdUnitEvent) -> void: + # Do not overwrite higher states + if is_state_error(item): + return + var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) + item.set_meta(META_GDUNIT_STATE, STATE.FLAKY) + if retry_count > 1: + item.set_text(0, "%s (%s retries)" % [ + item.get_meta(META_GDUNIT_NAME), + retry_count]) + item.set_custom_color(0, Color.GREEN_YELLOW) + item.set_custom_color(1, Color.GREEN_YELLOW) + item.collapsed = false + set_item_icon_by_state(item) + + func set_state_skipped(item: TreeItem) -> void: item.set_meta(META_GDUNIT_STATE, STATE.SKIPPED) item.set_text(1, "(skipped)") @@ -387,10 +412,15 @@ func set_state_warnings(item: TreeItem) -> void: set_item_icon_by_state(item) -func set_state_failed(item: TreeItem) -> void: +func set_state_failed(item: TreeItem, event: GdUnitEvent) -> void: # Do not overwrite higher states if is_state_error(item): return + var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) + if retry_count > 1: + item.set_text(0, "%s (%s retries)" % [ + item.get_meta(META_GDUNIT_NAME), + retry_count]) item.set_meta(META_GDUNIT_STATE, STATE.FAILED) item.set_custom_color(0, Color.LIGHT_BLUE) item.set_custom_color(1, Color.LIGHT_BLUE) @@ -424,8 +454,9 @@ func set_state_orphan(item: TreeItem, event: GdUnitEvent) -> void: if item.has_meta(META_GDUNIT_ORPHAN): orphan_count += item.get_meta(META_GDUNIT_ORPHAN) item.set_meta(META_GDUNIT_ORPHAN, orphan_count) - item.set_custom_color(0, Color.YELLOW) - item.set_custom_color(1, Color.YELLOW) + if item.get_meta(META_GDUNIT_STATE) != STATE.FAILED: + item.set_custom_color(0, Color.YELLOW) + item.set_custom_color(1, Color.YELLOW) item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count) set_item_icon_by_state(item) @@ -434,20 +465,22 @@ func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> vo # we do not show the root if item == _tree_root: return - if is_state_running(item) and event.is_success(): + + if event.is_success() and event.is_flaky(): + set_state_flaky(item, event) + elif event.is_success(): set_state_succeded(item) - else: - if event.is_skipped(): - set_state_skipped(item) - elif event.is_error(): - set_state_error(item) - elif event.is_failed(): - set_state_failed(item) - elif event.is_warning(): - set_state_warnings(item) - if add_reports: - for report in event.reports(): - add_report(item, report) + elif event.is_skipped(): + set_state_skipped(item) + elif event.is_error(): + set_state_error(item) + elif event.is_failed(): + set_state_failed(item, event) + elif event.is_warning(): + set_state_warnings(item) + if add_reports: + for report in event.reports(): + add_report(item, report) set_state_orphan(item, event) if is_folder(item): update_state(item.get_parent(), event, false) @@ -469,26 +502,26 @@ func abort_running(items:=_tree_root.get_children()) -> void: func select_first_failure() -> TreeItem: - return select_item(_find_first_failure()) + return select_item(_find_first_item_by_state(_tree_root, STATE.FAILED)) -func select_next_failure() -> TreeItem: +func _on_select_next_item_by_state(item_state: int) -> TreeItem: var current_selected := _tree.get_selected() # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found - current_selected = _find_first_failure() if current_selected == null else _find_failure(current_selected) + current_selected = _find_first_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state) # If no next failure found, then we try to select first if current_selected == null: - current_selected = _find_first_failure() + current_selected = _find_first_item_by_state(_tree_root, item_state) return select_item(current_selected) -func select_previous_failure() -> TreeItem: +func _on_select_previous_item_by_state(item_state: int) -> TreeItem: var current_selected := _tree.get_selected() # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found - current_selected = _find_last_failure() if current_selected == null else _find_failure(current_selected, true) + current_selected = _find_last_item_by_state(_tree_root, item_state) if current_selected == null else _find_item_by_state(current_selected, item_state, true) # If no next failure found, then we try to select first last if current_selected == null: - current_selected = _find_last_failure() + current_selected = _find_last_item_by_state(_tree_root, item_state) return select_item(current_selected) @@ -498,6 +531,7 @@ func select_first_orphan() -> void: for item in parent.get_children(): if is_item_state_orphan(item): parent.set_collapsed(false) + @warning_ignore("return_value_discarded") select_item(item) return @@ -705,6 +739,8 @@ func get_icon_by_file_type(path: String, state: STATE, orphans: bool) -> Texture return ICON_GDSCRIPT_TEST_FAILED_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FAILED STATE.WARNING: return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_DEFAULT + STATE.FLAKY: + return ICON_GDSCRIPT_TEST_SUCCESS_ORPHAN if orphans else ICON_GDSCRIPT_TEST_FLAKY _: return ICON_GDSCRIPT_TEST_DEFAULT if path.get_extension() == "cs": @@ -776,6 +812,7 @@ func discover_test_removed(event: GdUnitEventTestDiscoverTestRemoved) -> void: parent.set_meta(META_GDUNIT_TOTAL_TESTS, test_count - 1) init_item_counter(parent) # finally remove the test + @warning_ignore("return_value_discarded") remove_tree_item(resource_path, event.test_name()) @@ -816,6 +853,7 @@ func add_test(parent: TreeItem, test_case: GdUnitTestCaseDto) -> void: item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) item.set_meta(META_GDUNIT_TOTAL_TESTS, test_case_names.size()) + item.set_meta(META_SCRIPT_PATH, test_case.script_path()) item.set_meta(META_LINE_NUMBER, test_case.line_number()) item.set_meta(META_TEST_PARAM_INDEX, -1) set_item_icon_by_state(item) @@ -838,6 +876,7 @@ func add_test_cases(parent: TreeItem, test_case_names: PackedStringArray) -> voi item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_CASE_PARAMETERIZED) item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) item.set_meta(META_RESOURCE_PATH, resource_path) + item.set_meta(META_SCRIPT_PATH, parent.get_meta(META_SCRIPT_PATH)) item.set_meta(META_LINE_NUMBER, parent.get_meta(META_LINE_NUMBER)) item.set_meta(META_TEST_PARAM_INDEX, index) set_item_icon_by_state(item) @@ -876,7 +915,10 @@ func _on_tree_item_mouse_selected(mouse_position: Vector2, mouse_button_index: i func _on_run_pressed(run_debug: bool) -> void: _context_menu.hide() - var item := _tree.get_selected() + var item: = _tree.get_selected() + if item == null: + print_rich("[color=GOLDENROD]Abort Testrun, no test suite selected![/color]") + return if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE or item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER: var resource_path: String = item.get_meta(META_RESOURCE_PATH) run_testsuite.emit([resource_path], run_debug) @@ -902,21 +944,27 @@ func _on_Tree_item_selected() -> void: # Opens the test suite func _on_Tree_item_activated() -> void: var selected_item := _tree.get_selected() - var resource_path: String = selected_item.get_meta(META_RESOURCE_PATH) - var line_number: int = selected_item.get_meta(META_LINE_NUMBER) - var resource := load(resource_path) - - if selected_item.has_meta(META_GDUNIT_REPORT): - var reports := get_item_reports(selected_item) - var report_line_number := reports[0].line_number() - # if number -1 we use original stored line number of the test case - # in non debug mode the line number is not available - if report_line_number != -1: - line_number = report_line_number - - EditorInterface.get_file_system_dock().navigate_to_path(resource_path) - EditorInterface.edit_resource(resource) - EditorInterface.get_script_editor().goto_line(line_number - 1) + if selected_item != null and selected_item.has_meta(META_LINE_NUMBER): + var script_path: String = ( + selected_item.get_meta(META_RESOURCE_PATH) if is_test_suite(selected_item) + else selected_item.get_meta(META_SCRIPT_PATH) + ) + var line_number: int = selected_item.get_meta(META_LINE_NUMBER) + var resource: Script = load(script_path) + + if selected_item.has_meta(META_GDUNIT_REPORT): + var reports := get_item_reports(selected_item) + var report_line_number := reports[0].line_number() + # if number -1 we use original stored line number of the test case + # in non debug mode the line number is not available + if report_line_number != -1: + line_number = report_line_number + + EditorInterface.get_file_system_dock().navigate_to_path(script_path) + EditorInterface.edit_script(resource, line_number) + elif selected_item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER: + # Toggle collapse if dir + selected_item.collapsed = not selected_item.collapsed ################################################################################ @@ -934,6 +982,9 @@ func _on_gdunit_runner_stop(_client_id: int) -> void: _context_menu.set_item_disabled(CONTEXT_MENU_DEBUG_ID, false) abort_running() sort_tree_items(_tree_root) + # wait until the tree redraw + await get_tree().process_frame + @warning_ignore("return_value_discarded") select_first_failure() @@ -951,13 +1002,13 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: #_dump_tree_as_json("tree_example_discovered") GdUnitEvent.DISCOVER_SUITE_ADDED: - discover_test_suite_added(event) + discover_test_suite_added(event as GdUnitEventTestDiscoverTestSuiteAdded) GdUnitEvent.DISCOVER_TEST_ADDED: - discover_test_added(event) + discover_test_added(event as GdUnitEventTestDiscoverTestAdded) GdUnitEvent.DISCOVER_TEST_REMOVED: - discover_test_removed(event) + discover_test_removed(event as GdUnitEventTestDiscoverTestRemoved) GdUnitEvent.INIT: if not GdUnitSettings.is_test_discover_enabled(): diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd index 4fa36e60..b5cc8370 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd @@ -13,7 +13,9 @@ func _ready() -> void: reset() self_modulate = Color.WHITE _tween = create_tween() + @warning_ignore("return_value_discarded") _tween.set_loops() + @warning_ignore("return_value_discarded") _tween.tween_property(%Label, "self_modulate", Color(1, 1, 1, .8), 1.0).from_current().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN_OUT) @@ -25,7 +27,8 @@ func _input(event: InputEvent) -> void: if not is_visible_in_tree(): return if event is InputEventKey and event.is_pressed() and not event.is_echo(): - match event.keycode: + var _event := event as InputEventKey + match _event.keycode: KEY_CTRL: _input_event.ctrl_pressed = true KEY_SHIFT: @@ -35,8 +38,8 @@ func _input(event: InputEvent) -> void: KEY_META: _input_event.meta_pressed = true _: - _input_event.keycode = event.keycode - _apply_input_modifiers(event) + _input_event.keycode = _event.keycode + _apply_input_modifiers(_event) accept_event() if event is InputEventKey and not event.is_pressed(): @@ -46,7 +49,8 @@ func _input(event: InputEvent) -> void: func _apply_input_modifiers(event: InputEvent) -> void: if event is InputEventWithModifiers: - _input_event.meta_pressed = event.meta_pressed or _input_event.meta_pressed - _input_event.alt_pressed = event.alt_pressed or _input_event.alt_pressed - _input_event.shift_pressed = event.shift_pressed or _input_event.shift_pressed - _input_event.ctrl_pressed = event.ctrl_pressed or _input_event.ctrl_pressed + var _event := event as InputEventWithModifiers + _input_event.meta_pressed = _event.meta_pressed or _input_event.meta_pressed + _input_event.alt_pressed = _event.alt_pressed or _input_event.alt_pressed + _input_event.shift_pressed = _event.shift_pressed or _input_event.shift_pressed + _input_event.ctrl_pressed = _event.ctrl_pressed or _input_event.ctrl_pressed diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd index 670e6ebf..d05f8216 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -27,6 +27,7 @@ func _ready() -> void: GdUnitSettings.setup() GdUnit4Version.init_version_label(_version_label) _font_size = GdUnitFonts.init_fonts(_version_label) + @warning_ignore("return_value_discarded") about_to_popup.connect(_do_setup_properties) @@ -54,25 +55,30 @@ func setup_properties(properties_parent: Node, property_category: String) -> voi theme_.set_constant("h_separation", "GridContainer", 12) var last_category := "!" var min_size_overall := 0.0 + var labels := [] + var inputs := [] + var info_labels := [] + var grid: GridContainer = null for p in category_properties: var min_size_ := 0.0 - var grid := GridContainer.new() - grid.columns = 4 - grid.theme = theme_ var property: GdUnitProperty = p var current_category := property.category() - if current_category != last_category: + if not grid or current_category != last_category: + grid = GridContainer.new() + grid.columns = 4 + grid.theme = theme_ + var sub_category: Node = _properties_template.get_child(3).duplicate() sub_category.get_child(0).text = current_category.capitalize() sub_category.custom_minimum_size.y = _font_size + 16 properties_parent.add_child(sub_category) + properties_parent.add_child(grid) last_category = current_category # property name var label: Label = _properties_template.get_child(0).duplicate() label.text = _to_human_readable(property.name()) - label.custom_minimum_size = Vector2(_font_size * 20, 0) + labels.append(label) grid.add_child(label) - min_size_ += label.size.x # property reset btn var reset_btn: Button = _properties_template.get_child(1).duplicate() @@ -83,21 +89,26 @@ func setup_properties(properties_parent: Node, property_category: String) -> voi # property type specific input element var input: Node = _create_input_element(property, reset_btn) - input.custom_minimum_size = Vector2(_font_size * 15, 0) + inputs.append(input) grid.add_child(input) - min_size_ += input.size.x + @warning_ignore("return_value_discarded") reset_btn.pressed.connect(_on_btn_property_reset_pressed.bind(property, input, reset_btn)) # property help text var info: Node = _properties_template.get_child(2).duplicate() info.text = property.help() + info_labels.append(info) grid.add_child(info) - min_size_ += info.text.length() * _font_size if min_size_overall < min_size_: min_size_overall = min_size_ - properties_parent.add_child(grid) + for controls: Array in [labels, inputs, info_labels]: + var _size: float = controls.map(func(c: Control) -> float: return c.size.x).max() + min_size_overall += _size + for control: Control in controls: + control.custom_minimum_size.x = _size properties_parent.custom_minimum_size.x = min_size_overall +@warning_ignore("return_value_discarded") func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node: if property.is_selectable_value(): var options := OptionButton.new() @@ -141,6 +152,7 @@ func to_shortcut(keys: PackedInt32Array) -> String: return input_event.as_text() +@warning_ignore("return_value_discarded") func to_keys(input_event: InputEventKey) -> PackedInt32Array: var keys := PackedInt32Array() if input_event.ctrl_pressed: @@ -206,10 +218,12 @@ func rescan(update_scripts:=false) -> void: func _on_btn_report_bug_pressed() -> void: + @warning_ignore("return_value_discarded") OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=bug&projects=projects%2F5&template=bug_report.yml&title=GD-XXX%3A+Describe+the+issue+briefly") func _on_btn_request_feature_pressed() -> void: + @warning_ignore("return_value_discarded") OS.shell_open("https://github.com/MikeSchulze/gdUnit4/issues/new?assignees=MikeSchulze&labels=enhancement&projects=&template=feature_request.md&title=") diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn index da26f62c..74a6cd6a 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -117,7 +117,7 @@ shadow_offset = Vector2(10, 10) [node name="Control" type="Window"] disable_3d = true gui_embed_subwindows = true -title = "GdUnitSettings" +title = "GdUnit4 Settings" initial_position = 1 size = Vector2i(1006, 723) visible = false @@ -157,7 +157,6 @@ offset_right = 590.0 offset_bottom = 25.0 size_flags_horizontal = 3 text = "Enables/disables the update notification " -clip_text = true max_lines_visible = 1 [node name="sub_category" type="Panel" parent="property_template"] @@ -310,7 +309,6 @@ layout_mode = 2 [node name="common-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Common"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(1557, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -322,7 +320,6 @@ layout_mode = 2 [node name="ui-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/UI"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(1361, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -334,7 +331,6 @@ layout_mode = 2 [node name="shortcut-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Shortcuts"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(983, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -346,7 +342,6 @@ layout_mode = 2 [node name="report-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Report"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(1249, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd index 72304815..be5c3528 100644 --- a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd @@ -30,10 +30,10 @@ func _notification(what :int) -> void: func setup_editor_colors() -> void: if not Engine.is_editor_hint(): return - var settings := EditorInterface.get_editor_settings() - var background_color :Color = settings.get_setting("text_editor/theme/highlighting/background_color") - var text_color :Color = settings.get_setting("text_editor/theme/highlighting/text_color") - var selection_color :Color = settings.get_setting("text_editor/theme/highlighting/selection_color") + + var background_color := get_editor_color("text_editor/theme/highlighting/background_color", Color(0.1155, 0.132, 0.1595, 1)) + var text_color := get_editor_color("text_editor/theme/highlighting/text_color", Color(0.8025, 0.81, 0.8225, 1)) + var selection_color := get_editor_color("text_editor/theme/highlighting/selection_color", Color(0.44, 0.73, 0.98, 0.4)) for e :CodeEdit in [_template_editor, _tags_editor]: var editor :CodeEdit = e @@ -41,20 +41,20 @@ func setup_editor_colors() -> void: editor.add_theme_color_override("font_color", text_color) editor.add_theme_color_override("font_readonly_color", text_color) editor.add_theme_color_override("font_selected_color", selection_color) - setup_highlighter(editor, settings) + setup_highlighter(editor) -func setup_highlighter(editor :CodeEdit, settings :EditorSettings) -> void: +func setup_highlighter(editor :CodeEdit) -> void: var highlighter := CodeHighlighter.new() editor.set_syntax_highlighter(highlighter) - var number_color :Color = settings.get_setting("text_editor/theme/highlighting/number_color") - var symbol_color :Color = settings.get_setting("text_editor/theme/highlighting/symbol_color") - var function_color :Color = settings.get_setting("text_editor/theme/highlighting/function_color") - var member_variable_color :Color = settings.get_setting("text_editor/theme/highlighting/member_variable_color") - var comment_color :Color = settings.get_setting("text_editor/theme/highlighting/comment_color") - var keyword_color :Color = settings.get_setting("text_editor/theme/highlighting/keyword_color") - var base_type_color :Color = settings.get_setting("text_editor/theme/highlighting/base_type_color") - var annotation_color :Color = settings.get_setting("text_editor/theme/highlighting/gdscript/annotation_color") + var number_color := get_editor_color("text_editor/theme/highlighting/number_color", Color(0.63, 1, 0.88, 1)) + var symbol_color := get_editor_color("text_editor/theme/highlighting/symbol_color", Color(0.67, 0.79, 1, 1)) + var function_color := get_editor_color("text_editor/theme/highlighting/function_color", Color(0.34, 0.7, 1, 1)) + var member_variable_color := get_editor_color("text_editor/theme/highlighting/member_variable_color", Color(0.736, 0.88, 1, 1)) + var comment_color := get_editor_color("text_editor/theme/highlighting/comment_color", Color(0.8025, 0.81, 0.8225, 0.5)) + var keyword_color := get_editor_color("text_editor/theme/highlighting/keyword_color", Color(1, 0.44, 0.52, 1)) + var base_type_color := get_editor_color("text_editor/theme/highlighting/base_type_color", Color(0.26, 1, 0.76, 1)) + var annotation_color := get_editor_color("text_editor/theme/highlighting/gdscript/annotation_color", Color(1, 0.7, 0.45, 1)) highlighter.clear_color_regions() highlighter.clear_keyword_colors() @@ -74,8 +74,16 @@ func setup_highlighter(editor :CodeEdit, settings :EditorSettings) -> void: highlighter.add_keyword_color(word, base_type_color) +## Using this function to avoid null references to colors on inital Godot installations. +## For more details show https://github.com/MikeSchulze/gdUnit4/issues/533 +func get_editor_color(property_name: String, default: Color) -> Color: + var settings := EditorInterface.get_editor_settings() + return settings.get_setting(property_name) if settings.has_setting(property_name) else default + + func setup_fonts() -> void: if _template_editor: + @warning_ignore("return_value_discarded") GdUnitFonts.init_fonts(_template_editor) var font_size := GdUnitFonts.init_fonts(_tags_editor) _title_bar.size.y = font_size + 16 diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd index 6a6594b4..f2081420 100644 --- a/addons/gdUnit4/src/update/GdMarkDownReader.gd +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -109,6 +109,7 @@ func regex(pattern :String) -> RegEx: func _init() -> void: + @warning_ignore("return_value_discarded") _img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]") @@ -116,6 +117,7 @@ func set_http_client(client :GdUnitUpdateClient) -> void: _client = client +@warning_ignore("return_value_discarded") func _notification(what :int) -> void: if what == NOTIFICATION_PREDELETE: # finally remove_at the downloaded images @@ -149,26 +151,29 @@ func to_bbcode(input :String) -> String: var regex_ :RegEx = pattern[0] var bb_replace :Variant = pattern[1] if bb_replace is Callable: + @warning_ignore("unsafe_method_access") input = await bb_replace.call(regex_, input) else: - input = regex_.sub(input, bb_replace, true) + @warning_ignore("unsafe_cast") + input = regex_.sub(input, bb_replace as String, true) return input + "\n" -func process_tables(input :String) -> String: - var bbcode := Array() - var lines := Array(input.split("\n")) +func process_tables(input: String) -> String: + var bbcode := PackedStringArray() + var lines: Array[String] = Array(input.split("\n") as Array, TYPE_STRING, "", null) while not lines.is_empty(): if is_table(lines[0]): bbcode.append_array(parse_table(lines)) continue - bbcode.append(lines.pop_front()) - return "\n".join(PackedStringArray(bbcode)) + @warning_ignore("return_value_discarded", "unsafe_cast") + bbcode.append(lines.pop_front() as String) + return "\n".join(bbcode) class Table: - var _columns :int - var _rows := Array() + var _columns: int + var _rows: Array[Row] = [] class Row: var _cells := PackedStringArray() @@ -176,6 +181,7 @@ class Table: func _init(cells :PackedStringArray, columns :int) -> void: _cells = cells for i in range(_cells.size(), columns): + @warning_ignore("return_value_discarded") _cells.append("") func to_bbcode(cell_sizes :PackedInt32Array, bold :bool) -> String: @@ -186,6 +192,7 @@ class Table: cell = create_line(cell_sizes[cell_index]) if bold: cell = "[b]%s[/b]" % cell + @warning_ignore("return_value_discarded") cells.append("[cell]%s[/cell]" % cell) return "|".join(cells) @@ -208,6 +215,7 @@ class Table: func calculate_max_cell_sizes() -> PackedInt32Array: var cells_size := PackedInt32Array() for column in _columns: + @warning_ignore("return_value_discarded") cells_size.append(0) for row_index in _rows.size(): @@ -219,6 +227,7 @@ class Table: cells_size[cell_index] = size return cells_size + @warning_ignore("return_value_discarded") func to_bbcode() -> PackedStringArray: var cell_sizes := calculate_max_cell_sizes() var bb_code := PackedStringArray() @@ -292,6 +301,7 @@ func process_image_references(p_regex :RegEx, p_input :String) -> String: return extracted_references +@warning_ignore("return_value_discarded") func process_image(p_regex :RegEx, p_input :String) -> String: var to_replace := PackedStringArray() var tool_tips := PackedStringArray() @@ -312,6 +322,7 @@ func process_image(p_regex :RegEx, p_input :String) -> String: func _process_external_image_resources(input :String) -> String: + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(image_download_folder) # scan all img for external resources and download it for value in _img_replace_regex.search_all(input): @@ -334,6 +345,7 @@ func _process_external_image_resources(input :String) -> String: var err := image.save_png(new_url) if err: push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)]) + @warning_ignore("return_value_discarded") _image_urls.append(new_url) input = input.replace(image_url, new_url) return input diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd index d6d5048c..bb508d39 100644 --- a/addons/gdUnit4/src/update/GdUnitPatcher.gd +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -42,6 +42,7 @@ func _collect_patch_versions(scan_path :String, current :GdUnit4Version) -> Pack var patches := Array() var dir := DirAccess.open(scan_path) if dir != null: + @warning_ignore("return_value_discarded") dir.list_dir_begin() # TODO GODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 var next := "." while next != "": @@ -59,6 +60,7 @@ func _scan_patches(path :String) -> PackedStringArray: var patches := Array() var dir := DirAccess.open(path) if dir != null: + @warning_ignore("return_value_discarded") dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 var next := "." while next != "": diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd index 7b7206c1..a807c751 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -50,6 +50,7 @@ func message_h4(message :String, color :Color) -> void: _progress_content.append_text("[font_size=16]%s[/font_size]" % _colored(message, color)) +@warning_ignore("return_value_discarded") func run_update() -> void: get_cancel_button().disabled = true get_ok_button().disabled = true @@ -92,6 +93,7 @@ func restart_godot() -> void: EditorInterface.restart_editor(true) +@warning_ignore("return_value_discarded") func enable_gdUnit() -> void: var enabled_plugins := PackedStringArray() if ProjectSettings.has_setting("editor_plugins/enabled"): @@ -110,6 +112,7 @@ const GDUNIT_TEMP := "user://tmp" func temp_dir() -> String: if not DirAccess.dir_exists_absolute(GDUNIT_TEMP): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) return GDUNIT_TEMP @@ -118,6 +121,7 @@ func create_temp_dir(folder_name :String) -> String: var new_folder := temp_dir() + "/" + folder_name delete_directory(new_folder) if not DirAccess.dir_exists_absolute(new_folder): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(new_folder) return new_folder @@ -125,6 +129,7 @@ func create_temp_dir(folder_name :String) -> String: func delete_directory(path :String, only_content := false) -> void: var dir := DirAccess.open(path) if dir != null: + @warning_ignore("return_value_discarded") dir.list_dir_begin() var file_name := "." while file_name != "": @@ -159,6 +164,7 @@ func copy_directory(from_dir :String, to_dir :String) -> bool: var source_dir := DirAccess.open(from_dir) var dest_dir := DirAccess.open(to_dir) if source_dir != null: + @warning_ignore("return_value_discarded") source_dir.list_dir_begin() var next := "." @@ -169,6 +175,7 @@ func copy_directory(from_dir :String, to_dir :String) -> bool: var source := source_dir.get_current_dir() + "/" + next var dest := dest_dir.get_current_dir() + "/" + next if source_dir.current_is_dir(): + @warning_ignore("return_value_discarded") copy_directory(source + "/", dest) continue var err := source_dir.copy(source, dest) @@ -195,10 +202,12 @@ func extract_zip(zip_package :String, dest_path :String) -> Variant: for zip_entry in zip_entries: var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "") if zip_entry.ends_with("/"): + @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(new_file_path) continue var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) file.store_buffer(zip.read_file(zip_entry)) + @warning_ignore("return_value_discarded") zip.close() return dest_path diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd index a3f56ce7..0cf6b928 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd @@ -17,6 +17,7 @@ class HttpResponse: func response() -> Variant: var test_json_conv := JSON.new() + @warning_ignore("return_value_discarded") test_json_conv.parse(_body.get_string_from_utf8()) return test_json_conv.get_data() @@ -28,6 +29,7 @@ var _http_request :HTTPRequest = HTTPRequest.new() func _ready() -> void: add_child(_http_request) + @warning_ignore("return_value_discarded") _http_request.request_completed.connect(_on_request_completed) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd index 67acabb2..19acfc8c 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -21,6 +21,7 @@ var _download_zip_url :String func _ready() -> void: _update_button.disabled = true _md_reader.set_http_client(_update_client) + @warning_ignore("return_value_discarded") GdUnitFonts.init_fonts(_content) await request_releases() @@ -91,7 +92,7 @@ func show_update() -> void: func extract_latest_version(response :GdUnitUpdateClient.HttpResponse) -> GdUnit4Version: var body :Array = response.response() - return GdUnit4Version.parse(body[0]["name"]) + return GdUnit4Version.parse(body[0]["name"] as String) func extract_zip_url(response :GdUnitUpdateClient.HttpResponse) -> String: @@ -103,7 +104,7 @@ func extract_releases(response :GdUnitUpdateClient.HttpResponse, current_version await get_tree().process_frame var result := "" for release :Dictionary in response.response(): - if GdUnit4Version.parse(release["tag_name"]).equals(current_version): + if GdUnit4Version.parse(release["tag_name"] as String).equals(current_version): break var release_description :String = release["body"] result += await _md_reader.to_bbcode(release_description) @@ -119,8 +120,8 @@ func rescan() -> void: while fs.is_scanning(): if OS.is_stdout_verbose(): progressBar(fs.get_scanning_progress() * 100 as int) - await Engine.get_main_loop().process_frame - await Engine.get_main_loop().process_frame + await get_tree().process_frame + await get_tree().process_frame await get_tree().create_timer(1).timeout @@ -132,6 +133,7 @@ func progressBar(p_progress :int) -> void: printraw("scan [%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "#").rpad(50, "-"), p_progress]) +@warning_ignore("return_value_discarded") func _on_update_pressed() -> void: _update_button.set_disabled(true) # close all opend scripts before start the update @@ -146,9 +148,9 @@ func _on_update_pressed() -> void: var dest := FileAccess.open("res://addons/.gdunit_update/GdUnitUpdate.tscn", FileAccess.WRITE) dest.store_string(content) hide() - var update :Variant = load("res://addons/.gdunit_update/GdUnitUpdate.tscn").instantiate() + var update: Node = load("res://addons/.gdunit_update/GdUnitUpdate.tscn").instantiate() update.setup(_update_client, _download_zip_url) - Engine.get_main_loop().root.add_child(update) + (Engine.get_main_loop() as SceneTree).root.add_child(update) update.popup_centered() @@ -163,13 +165,14 @@ func _on_cancel_pressed() -> void: func _on_content_meta_clicked(meta :String) -> void: var properties :Variant = str_to_var(meta) if properties.has("url"): - OS.shell_open(properties.get("url")) + @warning_ignore("return_value_discarded") + OS.shell_open(properties.get("url") as String) func _on_content_meta_hover_started(meta :String) -> void: var properties :Variant = str_to_var(meta) if properties.has("tool_tip"): - _content.set_tooltip_text(properties.get("tool_tip")) + _content.set_tooltip_text(properties.get("tool_tip") as String) @warning_ignore("unused_parameter") diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import index 0bc39e49..13b5eacb 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Bold.ttf-ea008af97d359b7630bd27123 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import index e7dab2b3..361617a7 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-BoldItalic.ttf-6e10905211cda810d47 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import index a8dc5f8c..161de559 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-ExtraLight.ttf-c8ac954f2ab584e7652 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import index 4011502d..7a2ef063 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-ExtraLightItalic.ttf-06133dd8b521e Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import index 40dab272..4da9f155 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Italic.ttf-328fe6d9b2ac5d629c43c33 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import index cddd89e2..eb2eaaf0 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Light.ttf-638f745780c834176c3bf996 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import index 4216e5dc..64afc1e9 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-LightItalic.ttf-473f0d613e289d058b Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import index 342bafe0..26c6d948 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Medium.ttf-f165ecef77d89557a95acac Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import index 604eddb0..bb7e6b38 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-MediumItalic.ttf-40c40d791914284c8 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import index 3d8905f6..4fce311d 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Regular.ttf-f5a7315540116b55ba9e01 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import index e264b7e4..2bd53b83 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-SemiBold.ttf-6012d0b71d40b9767a7b6 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import index d135b44a..831001f8 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-SemiBoldItalic.ttf-12b525223c8f2df Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import index e519d15e..90e7b876 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Thin.ttf-a3a6620deea1a01e153a2a60c Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import index f9aaedaf..3b105773 100644 --- a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import +++ b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-ThinItalic.ttf-e9ceff3e4cdfbfedd19 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/pandora/model/category.gd b/addons/pandora/model/category.gd index 9461fe30..4de25fd2 100644 --- a/addons/pandora/model/category.gd +++ b/addons/pandora/model/category.gd @@ -1,3 +1,4 @@ +@tool class_name PandoraCategory extends PandoraEntity # not persisted but computed at runtime diff --git a/addons/pandora/ui/editor/inspector/entity_category_browser_property.gd b/addons/pandora/ui/editor/inspector/entity_category_browser_property.gd new file mode 100644 index 00000000..80a4507a --- /dev/null +++ b/addons/pandora/ui/editor/inspector/entity_category_browser_property.gd @@ -0,0 +1,74 @@ +extends EditorProperty + +# The main control for editing the property. +var property_control := OptionButton.new() +var ids_to_categories = {} + + +func _init(class_data: Dictionary) -> void: + # Add the control as a direct child of EditorProperty node. + add_child.call_deferred(property_control) + # Make sure the control is able to retain the focus. + add_focusable(property_control) + property_control.get_popup().id_pressed.connect(_on_id_selected) + + var id_counter = 0 + var all_categories = _find_all_categories(class_data["path"]) + var editor_plugin: EditorPlugin = Engine.get_meta("PandoraEditorPlugin", null) + # Prevent button from expanding to selected icon size. + property_control.set_expand_icon(true) + + for category in all_categories: + property_control.get_popup().add_icon_item( + load(category.get_icon_path()), category.get_entity_name(), id_counter + ) + if editor_plugin: + property_control.get_popup().set_item_icon_max_width(id_counter, editor_plugin.get_editor_interface().get_editor_scale() * 16) + # Godot 4.1+ + if property_control.get_popup().has_method("set_item_icon_modulate"): + property_control.get_popup().set_item_icon_modulate(id_counter, category.get_icon_color()) + ids_to_categories[id_counter] = category + id_counter += 1 + + +func _on_id_selected(id: int) -> void: + var category = ids_to_categories[id] as PandoraEntity + var current_category = get_edited_object()[get_edited_property()] as PandoraEntity + + property_control.modulate = ( + current_category.get_icon_color() if current_category != null else Color.WHITE + ) + if current_category != null and category.get_entity_id() == current_category.get_entity_id(): + # skip current entities + return + + emit_changed(get_edited_property(), category) + + +func _update_property() -> void: + _update_deferred() + + +func _update_deferred() -> void: + var current_category = get_edited_object()[get_edited_property()] as PandoraEntity + if current_category == null: + property_control.select(-1) + return + for id in ids_to_categories.keys(): + if ids_to_categories[id].get_entity_id() == current_category.get_entity_id(): + property_control.select(id) + property_control.modulate = current_category.get_icon_color() + break + + +## Looks up all categories who are eligible for the given script path +func _find_all_categories(script_path: String) -> Array[PandoraEntity]: + # lookup entity data + var categories = Pandora.get_all_categories() + var all_categories: Array[PandoraEntity] = [] + for category in categories: + if category._script_path == script_path: + all_categories.append(category) + if all_categories.is_empty(): + all_categories = Pandora.get_all_categories() + return all_categories diff --git a/addons/pandora/ui/editor/inspector/entity_instance_browser_property.gd b/addons/pandora/ui/editor/inspector/entity_instance_browser_property.gd index 2200460a..4cb66e14 100644 --- a/addons/pandora/ui/editor/inspector/entity_instance_browser_property.gd +++ b/addons/pandora/ui/editor/inspector/entity_instance_browser_property.gd @@ -34,6 +34,7 @@ func _init(class_data: Dictionary) -> void: func _on_id_selected(id: int) -> void: var entity = ids_to_entities[id] as PandoraEntity var current_entity = get_edited_object()[get_edited_property()] as PandoraEntity + property_control.modulate = ( current_entity.get_icon_color() if current_entity != null else Color.WHITE ) diff --git a/addons/pandora/ui/editor/inspector/entity_instance_inspector.gd b/addons/pandora/ui/editor/inspector/entity_instance_inspector.gd index 1de3d40f..98f6ca29 100644 --- a/addons/pandora/ui/editor/inspector/entity_instance_inspector.gd +++ b/addons/pandora/ui/editor/inspector/entity_instance_inspector.gd @@ -1,9 +1,10 @@ extends EditorInspectorPlugin -const BrowserProperty = preload( - "res://addons/pandora/ui/editor/inspector/entity_instance_browser_property.gd" -) +const EntityBrowserProperty = preload("./entity_instance_browser_property.gd") +const CategoryBrowserProperty = preload("./entity_category_browser_property.gd") + const PANDORA_ENTITY_CLASS = &"PandoraEntity" +const PANDORA_CATEGORY_CLASS = &"PandoraCategory" # ClassName -> Dictionary var _global_class_cache = {} @@ -18,9 +19,12 @@ func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wi for global_class in ProjectSettings.get_global_class_list(): _global_class_cache[global_class["class"]] = global_class if type == TYPE_OBJECT: - var test_instance = ClassDB - if _is_pandora_entity(hint_string): - var inspector_property := BrowserProperty.new(_global_class_cache[hint_string]) + if _is_pandora_category(hint_string): + var inspector_property := CategoryBrowserProperty.new(_global_class_cache[hint_string]) + add_property_editor(name, inspector_property) + return true + elif _is_pandora_entity(hint_string): + var inspector_property := EntityBrowserProperty.new(_global_class_cache[hint_string]) add_property_editor(name, inspector_property) return true return false @@ -38,6 +42,19 @@ func _is_pandora_entity(clazz: String) -> bool: if parent == PANDORA_ENTITY_CLASS: return true return _is_pandora_entity(parent) + + +func _is_pandora_category(clazz: String) -> bool: + if clazz == PANDORA_CATEGORY_CLASS: + return true + if clazz == "": + return false + var parent = _get_parent_class(clazz) + if parent == null: + return false + if parent == PANDORA_CATEGORY_CLASS: + return true + return _is_pandora_category(parent) func _get_parent_class(clazz_name: String) -> String: diff --git a/examples/inventory/ui/inventory_ui.gd b/examples/inventory/ui/inventory_ui.gd index f356843f..f5a44f68 100644 --- a/examples/inventory/ui/inventory_ui.gd +++ b/examples/inventory/ui/inventory_ui.gd @@ -2,3 +2,4 @@ extends GridContainer @export var inventory:Inventory +@export var category:PandoraCategory diff --git a/examples/inventory/ui/inventory_ui.tscn b/examples/inventory/ui/inventory_ui.tscn index 849cf259..f9f04d04 100644 --- a/examples/inventory/ui/inventory_ui.tscn +++ b/examples/inventory/ui/inventory_ui.tscn @@ -1,7 +1,12 @@ -[gd_scene load_steps=3 format=3 uid="uid://2meu4gdjnodt"] +[gd_scene load_steps=5 format=3 uid="uid://2meu4gdjnodt"] [ext_resource type="Script" path="res://examples/inventory/ui/inventory_ui.gd" id="1_0oe7e"] [ext_resource type="Texture2D" uid="uid://cb6str3hxrsdi" path="res://addons/pandora/icons/KeyValue.svg" id="2_hk48w"] +[ext_resource type="Script" path="res://examples/inventory/item.gd" id="2_u024b"] + +[sub_resource type="Resource" id="Resource_aygen"] +script = ExtResource("2_u024b") +_id = "49" [node name="InventoryUI" type="GridContainer"] anchors_preset = 15 @@ -13,6 +18,7 @@ theme_override_constants/h_separation = 10 theme_override_constants/v_separation = 10 columns = 3 script = ExtResource("1_0oe7e") +category = SubResource("Resource_aygen") [node name="Slot1" type="TextureRect" parent="."] layout_mode = 2 diff --git a/mock/custom_mock_entity.gd b/mock/custom_mock_entity.gd index 1a55825c..f15b8a11 100644 --- a/mock/custom_mock_entity.gd +++ b/mock/custom_mock_entity.gd @@ -1 +1,2 @@ +@tool class_name CustomMockEntity extends PandoraEntity diff --git a/mock/custom_mock_entity_alternative.gd b/mock/custom_mock_entity_alternative.gd index 32393d38..370935de 100644 --- a/mock/custom_mock_entity_alternative.gd +++ b/mock/custom_mock_entity_alternative.gd @@ -1 +1,2 @@ +@tool class_name CustomMockAltEntity extends PandoraEntity diff --git a/mock/mock_scene.gd b/mock/mock_scene.gd index ba2e9324..6a7ee4a9 100644 --- a/mock/mock_scene.gd +++ b/mock/mock_scene.gd @@ -2,6 +2,7 @@ extends Node2D @export var entity:CustomMockEntity +@export var category:PandoraCategory var _instance:CustomMockEntity @@ -13,3 +14,7 @@ func _ready(): func get_entity_instance() -> CustomMockEntity: return _instance + + +func get_category() -> PandoraCategory: + return category diff --git a/mock/mock_scene.tscn b/mock/mock_scene.tscn index 54537300..6f35f623 100644 --- a/mock/mock_scene.tscn +++ b/mock/mock_scene.tscn @@ -1,12 +1,18 @@ -[gd_scene load_steps=4 format=3 uid="uid://c3j2xs0rnqdst"] +[gd_scene load_steps=6 format=3 uid="uid://c3j2xs0rnqdst"] [ext_resource type="Script" path="res://mock/mock_scene.gd" id="1_m4lrk"] [ext_resource type="Script" path="res://mock/custom_mock_entity.gd" id="2_mijc7"] +[ext_resource type="Script" path="res://addons/pandora/model/category.gd" id="3_dox3s"] -[sub_resource type="Resource" id="Resource_6ucbm"] +[sub_resource type="Resource" id="Resource_qaqi5"] script = ExtResource("2_mijc7") _id = "55" +[sub_resource type="Resource" id="Resource_dgwox"] +script = ExtResource("3_dox3s") +_id = "3" + [node name="MockScene" type="Node2D"] script = ExtResource("1_m4lrk") -entity = SubResource("Resource_6ucbm") +entity = SubResource("Resource_qaqi5") +category = SubResource("Resource_dgwox") diff --git a/project.godot b/project.godot index 8e49c636..224bcb43 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ config/tags=PackedStringArray("addon", "godot4", "rpg", "data") run/main_scene="res://TestScene.tscn" config/use_custom_user_dir=true config/custom_user_dir_name="pandora" -config/features=PackedStringArray("4.2", "Forward Plus") +config/features=PackedStringArray("4.3", "Forward Plus") boot_splash/image="res://splash.png" config/icon="res://addons/pandora/icons/icon.png" @@ -30,3 +30,8 @@ enabled=PackedStringArray("res://addons/pandora/plugin.cfg", "res://addons/gdUni [filesystem] import/blender/enabled=false + +[gdunit4] + +settings/test/test_discovery=true +ui/inspector/tree_sort_mode=1 diff --git a/test/scene/mock_scene_test.gd b/test/scene/mock_scene_test.gd index 1d0582b8..2d2eb7a8 100644 --- a/test/scene/mock_scene_test.gd +++ b/test/scene/mock_scene_test.gd @@ -16,3 +16,4 @@ func test_instantiate_mock_data_via_scene() -> void: assert_that(tree.get_entity_instance()).is_not_null() assert_bool(tree.get_entity_instance() is CustomMockEntity).is_true() + assert_bool(tree.get_category() is PandoraCategory).is_true() diff --git a/test/scene_test.gd b/test/scene_test.gd index 65e7e4fd..ffbf621f 100644 --- a/test/scene_test.gd +++ b/test/scene_test.gd @@ -30,3 +30,4 @@ func test_initialize_scene() -> void: await runner.simulate_frames(1) assert_that(scene.get_entity_instance()).is_not_null() + assert_that(scene.get_category()).is_not_null()