From 238231069e793892c4edf073bdade2cfd7a3ce11 Mon Sep 17 00:00:00 2001 From: bitbrain Date: Wed, 30 Oct 2024 07:29:36 +0000 Subject: [PATCH] Ability to export categories via property editor --- addons/gdUnit4/GdUnitRunner.cfg | 2 +- addons/gdUnit4/bin/GdUnitBuildTool.gd | 4 +- addons/gdUnit4/bin/GdUnitCmdTool.gd | 93 ++-- addons/gdUnit4/bin/GdUnitCopyLog.gd | 151 +++--- addons/gdUnit4/plugin.cfg | 2 +- addons/gdUnit4/plugin.gd | 9 +- addons/gdUnit4/src/GdUnitAssert.gd | 6 +- addons/gdUnit4/src/GdUnitAwaiter.gd | 13 +- addons/gdUnit4/src/GdUnitFloatAssert.gd | 8 +- addons/gdUnit4/src/GdUnitFuncAssert.gd | 12 +- addons/gdUnit4/src/GdUnitGodotErrorAssert.gd | 8 +- addons/gdUnit4/src/GdUnitIntAssert.gd | 9 +- addons/gdUnit4/src/GdUnitSceneRunner.gd | 360 ++++++++++---- addons/gdUnit4/src/GdUnitSignalAssert.gd | 4 +- addons/gdUnit4/src/GdUnitTestSuite.gd | 95 +++- .../gdUnit4/src/asserts/GdAssertMessages.gd | 114 +++-- addons/gdUnit4/src/asserts/GdAssertReports.gd | 3 +- .../src/asserts/GdUnitArrayAssertImpl.gd | 272 ++++++---- .../gdUnit4/src/asserts/GdUnitAssertImpl.gd | 5 +- .../gdUnit4/src/asserts/GdUnitAssertions.gd | 1 + .../src/asserts/GdUnitBoolAssertImpl.gd | 16 +- .../src/asserts/GdUnitDictionaryAssertImpl.gd | 39 +- .../src/asserts/GdUnitFailureAssertImpl.gd | 14 +- .../src/asserts/GdUnitFileAssertImpl.gd | 23 +- .../src/asserts/GdUnitFloatAssertImpl.gd | 22 +- .../src/asserts/GdUnitFuncAssertImpl.gd | 24 +- .../src/asserts/GdUnitGodotErrorAssertImpl.gd | 1 + .../src/asserts/GdUnitIntAssertImpl.gd | 16 +- .../src/asserts/GdUnitObjectAssertImpl.gd | 39 +- .../src/asserts/GdUnitResultAssertImpl.gd | 59 +-- .../src/asserts/GdUnitSignalAssertImpl.gd | 19 +- .../src/asserts/GdUnitStringAssertImpl.gd | 58 ++- .../src/asserts/GdUnitVectorAssertImpl.gd | 28 +- addons/gdUnit4/src/asserts/ValueProvider.gd | 4 + addons/gdUnit4/src/cmd/CmdArgumentParser.gd | 9 +- addons/gdUnit4/src/cmd/CmdCommand.gd | 1 + addons/gdUnit4/src/cmd/CmdCommandHandler.gd | 6 +- addons/gdUnit4/src/cmd/CmdConsole.gd | 18 +- addons/gdUnit4/src/core/GdArrayTools.gd | 12 +- addons/gdUnit4/src/core/GdDiffTool.gd | 11 +- addons/gdUnit4/src/core/GdFunctionDoubler.gd | 56 ++- addons/gdUnit4/src/core/GdObjects.gd | 159 +++--- addons/gdUnit4/src/core/GdUnit4Version.gd | 10 +- addons/gdUnit4/src/core/GdUnitClassDoubler.gd | 27 +- addons/gdUnit4/src/core/GdUnitFileAccess.gd | 12 + .../src/core/GdUnitObjectInteractions.gd | 11 +- .../core/GdUnitObjectInteractionsTemplate.gd | 3 +- addons/gdUnit4/src/core/GdUnitProperty.gd | 10 +- addons/gdUnit4/src/core/GdUnitResult.gd | 9 +- addons/gdUnit4/src/core/GdUnitRunner.gd | 8 +- addons/gdUnit4/src/core/GdUnitRunnerConfig.gd | 24 +- .../gdUnit4/src/core/GdUnitSceneRunnerImpl.gd | 370 ++++++++++---- addons/gdUnit4/src/core/GdUnitSettings.gd | 117 +++-- .../gdUnit4/src/core/GdUnitSignalAwaiter.gd | 12 +- .../gdUnit4/src/core/GdUnitSignalCollector.gd | 14 +- addons/gdUnit4/src/core/GdUnitSignals.gd | 9 +- addons/gdUnit4/src/core/GdUnitSingleton.gd | 7 +- .../src/core/GdUnitTestSuiteBuilder.gd | 2 + .../src/core/GdUnitTestSuiteScanner.gd | 94 ++-- addons/gdUnit4/src/core/GdUnitTools.gd | 53 +- addons/gdUnit4/src/core/LocalTime.gd | 1 + addons/gdUnit4/src/core/_TestCase.gd | 32 +- .../gdUnit4/src/core/assets/touch-button.png | Bin 0 -> 3292 bytes .../src/core/assets/touch-button.png.import | 34 ++ .../src/core/command/GdUnitCommandHandler.gd | 25 +- .../core/discovery/GdUnitTestDiscoverGuard.gd | 7 +- .../core/discovery/GdUnitTestDiscoverer.gd | 7 +- addons/gdUnit4/src/core/event/GdUnitEvent.gd | 19 +- .../core/execution/GdUnitExecutionContext.gd | 282 ++++++++--- .../core/execution/GdUnitMemoryObserver.gd | 23 +- .../execution/GdUnitTestReportCollector.gd | 12 +- .../core/execution/GdUnitTestSuiteExecutor.gd | 6 +- .../stages/GdUnitTestCaseAfterStage.gd | 86 +--- .../stages/GdUnitTestCaseBeforeStage.gd | 10 +- .../stages/GdUnitTestCaseExecutionStage.gd | 17 + .../stages/GdUnitTestSuiteAfterStage.gd | 15 +- .../stages/GdUnitTestSuiteBeforeStage.gd | 2 +- .../stages/GdUnitTestSuiteExecutionStage.gd | 20 +- .../execution/stages/IGdUnitExecutionStage.gd | 2 +- .../GdUnitTestCaseFuzzedExecutionStage.gd | 14 +- .../fuzzed/GdUnitTestCaseFuzzedTestStage.gd | 4 +- .../GdUnitTestCaseParameterSetTestStage.gd | 10 + .../GdUnitTestCaseParameterizedTestStage.gd | 66 +-- .../GdUnitTestCaseSingleExecutionStage.gd | 14 +- .../src/core/parse/GdClassDescriptor.gd | 9 - .../src/core/parse/GdDefaultValueDecoder.gd | 38 +- .../src/core/parse/GdFunctionArgument.gd | 86 +++- .../src/core/parse/GdFunctionDescriptor.gd | 130 +++-- .../gdUnit4/src/core/parse/GdScriptParser.gd | 343 +++++-------- .../src/core/parse/GdUnitExpressionRunner.gd | 60 ++- .../parse/GdUnitTestParameterSetResolver.gd | 37 +- .../test_suite/GdUnitTestSuiteTemplate.gd | 6 +- .../src/core/thread/GdUnitThreadContext.gd | 3 +- .../src/core/thread/GdUnitThreadManager.gd | 4 +- addons/gdUnit4/src/doubler/CallableDoubler.gd | 210 ++++++++ .../extractors/GdUnitFuncValueExtractor.gd | 18 +- addons/gdUnit4/src/fuzzers/StringFuzzer.gd | 1 + .../src/matchers/AnyClazzArgumentMatcher.gd | 4 +- .../src/matchers/ChainedArgumentMatcher.gd | 8 +- addons/gdUnit4/src/mocking/GdUnitMock.gd | 17 +- .../gdUnit4/src/mocking/GdUnitMockBuilder.gd | 28 +- .../src/mocking/GdUnitMockFunctionDoubler.gd | 59 +-- addons/gdUnit4/src/mocking/GdUnitMockImpl.gd | 3 + addons/gdUnit4/src/monitor/ErrorLogEntry.gd | 39 +- .../src/monitor/GodotGdErrorMonitor.gd | 21 +- .../src/mono/GdUnit4CSharpApiLoader.gd | 10 +- addons/gdUnit4/src/network/GdUnitServer.gd | 1 + addons/gdUnit4/src/network/GdUnitTcpClient.gd | 6 +- addons/gdUnit4/src/network/GdUnitTcpServer.gd | 13 +- addons/gdUnit4/src/network/rpc/RPC.gd | 2 +- .../src/network/rpc/dtos/GdUnitResourceDto.gd | 1 + .../src/network/rpc/dtos/GdUnitTestCaseDto.gd | 16 +- .../network/rpc/dtos/GdUnitTestSuiteDto.gd | 1 + .../gdUnit4/src/report/GdUnitByPathReport.gd | 27 +- .../gdUnit4/src/report/GdUnitHtmlPatterns.gd | 94 +++- addons/gdUnit4/src/report/GdUnitHtmlReport.gd | 131 +++-- .../gdUnit4/src/report/GdUnitReportSummary.gd | 69 ++- .../src/report/GdUnitTestCaseReport.gd | 48 +- .../src/report/GdUnitTestSuiteReport.gd | 74 ++- addons/gdUnit4/src/report/JUnitXmlReport.gd | 48 +- addons/gdUnit4/src/report/XmlElement.gd | 1 + .../src/report/template/css/breadcrumb.css | 29 +- .../gdUnit4/src/report/template/css/icon.png | Bin 13817 -> 0 bytes .../gdUnit4/src/report/template/css/logo.png | Bin 0 -> 49775 bytes .../css/{icon.png.import => logo.png.import} | 8 +- .../gdUnit4/src/report/template/css/style.css | 312 ------------ .../src/report/template/css/styles.css | 468 ++++++++++++++++++ .../src/report/template/folder_report.html | 176 ++++--- addons/gdUnit4/src/report/template/index.html | 204 ++++---- .../src/report/template/suite_report.html | 203 +++++--- addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd | 53 +- .../src/spy/GdUnitSpyFunctionDoubler.gd | 60 ++- addons/gdUnit4/src/ui/GdUnitConsole.gd | 47 +- addons/gdUnit4/src/ui/GdUnitFonts.gd | 4 +- addons/gdUnit4/src/ui/GdUnitInspector.gd | 12 +- addons/gdUnit4/src/ui/GdUnitInspector.tscn | 12 +- addons/gdUnit4/src/ui/ScriptEditorControls.gd | 5 +- .../EditorFileSystemContextMenuHandler.gd | 6 +- .../ui/menu/ScriptEditorContextMenuHandler.gd | 5 +- .../gdUnit4/src/ui/parts/InspectorMonitor.gd | 9 +- .../src/ui/parts/InspectorProgressBar.gd | 25 +- .../src/ui/parts/InspectorStatusBar.gd | 76 ++- .../src/ui/parts/InspectorStatusBar.tscn | 390 +++++++++++---- .../gdUnit4/src/ui/parts/InspectorToolBar.gd | 17 +- .../src/ui/parts/InspectorTreeMainPanel.gd | 165 +++--- .../src/ui/settings/GdUnitInputCapture.gd | 18 +- .../src/ui/settings/GdUnitSettingsDialog.gd | 34 +- .../src/ui/settings/GdUnitSettingsDialog.tscn | 7 +- .../src/ui/templates/TestSuiteTemplate.gd | 36 +- addons/gdUnit4/src/update/GdMarkDownReader.gd | 28 +- addons/gdUnit4/src/update/GdUnitPatcher.gd | 2 + addons/gdUnit4/src/update/GdUnitUpdate.gd | 9 + .../gdUnit4/src/update/GdUnitUpdateClient.gd | 2 + .../gdUnit4/src/update/GdUnitUpdateNotify.gd | 19 +- .../fonts/static/RobotoMono-Bold.ttf.import | 1 + .../static/RobotoMono-BoldItalic.ttf.import | 1 + .../static/RobotoMono-ExtraLight.ttf.import | 1 + .../RobotoMono-ExtraLightItalic.ttf.import | 1 + .../fonts/static/RobotoMono-Italic.ttf.import | 1 + .../fonts/static/RobotoMono-Light.ttf.import | 1 + .../static/RobotoMono-LightItalic.ttf.import | 1 + .../fonts/static/RobotoMono-Medium.ttf.import | 1 + .../static/RobotoMono-MediumItalic.ttf.import | 1 + .../static/RobotoMono-Regular.ttf.import | 1 + .../static/RobotoMono-SemiBold.ttf.import | 1 + .../RobotoMono-SemiBoldItalic.ttf.import | 1 + .../fonts/static/RobotoMono-Thin.ttf.import | 1 + .../static/RobotoMono-ThinItalic.ttf.import | 1 + addons/pandora/model/category.gd | 1 + .../entity_category_browser_property.gd | 74 +++ .../entity_instance_browser_property.gd | 1 + .../inspector/entity_instance_inspector.gd | 29 +- examples/inventory/ui/inventory_ui.gd | 1 + examples/inventory/ui/inventory_ui.tscn | 8 +- mock/custom_mock_entity.gd | 1 + mock/custom_mock_entity_alternative.gd | 1 + mock/mock_scene.gd | 5 + mock/mock_scene.tscn | 12 +- project.godot | 7 +- test/scene/mock_scene_test.gd | 1 + test/scene_test.gd | 1 + 181 files changed, 4904 insertions(+), 2664 deletions(-) create mode 100644 addons/gdUnit4/src/core/assets/touch-button.png create mode 100644 addons/gdUnit4/src/core/assets/touch-button.png.import create mode 100644 addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd create mode 100644 addons/gdUnit4/src/doubler/CallableDoubler.gd delete mode 100644 addons/gdUnit4/src/report/template/css/icon.png create mode 100644 addons/gdUnit4/src/report/template/css/logo.png rename addons/gdUnit4/src/report/template/css/{icon.png.import => logo.png.import} (68%) delete mode 100644 addons/gdUnit4/src/report/template/css/style.css create mode 100644 addons/gdUnit4/src/report/template/css/styles.css create mode 100644 addons/pandora/ui/editor/inspector/entity_category_browser_property.gd 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 0000000000000000000000000000000000000000..23f46eff090a7337cffc2a274924209f53e5028b GIT binary patch literal 3292 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 aj{gH;ZIYKdEcc-R0000 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 eeac2924071bb62e97d1f372ca66c0e7e1a256f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13817 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%0AKr(9Uc>BSL)Bn(%Dc$$Ky(%WMe% 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 + - - - 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()