From a0376f7441789a73c22efa85d8449593514a16d5 Mon Sep 17 00:00:00 2001 From: miguel Date: Sat, 2 Dec 2023 14:21:49 +0000 Subject: [PATCH] update gdunit to 4.2 (#157) --- addons/gdUnit4/bin/GdUnitCmdTool.gd | 30 +- addons/gdUnit4/bin/GdUnitCopyLog.gd | 8 +- addons/gdUnit4/bin/ProjectScanner.gd | 51 ++- addons/gdUnit4/plugin.gd | 2 + addons/gdUnit4/runtest.cmd | 2 +- addons/gdUnit4/src/GdUnitAwaiter.gd | 10 +- addons/gdUnit4/src/GdUnitTestSuite.gd | 31 +- .../gdUnit4/src/asserts/GdAssertMessages.gd | 40 +- addons/gdUnit4/src/asserts/GdAssertReports.gd | 3 +- .../gdUnit4/src/asserts/GdUnitAssertions.gd | 30 ++ .../src/asserts/GdUnitFuncAssertImpl.gd | 91 ++-- .../src/asserts/GdUnitResultAssertImpl.gd | 6 +- addons/gdUnit4/src/cmd/CmdArgumentParser.gd | 10 +- addons/gdUnit4/src/cmd/CmdCommandHandler.gd | 10 +- addons/gdUnit4/src/core/GdObjects.gd | 42 +- addons/gdUnit4/src/core/GdUnitClassDoubler.gd | 32 +- addons/gdUnit4/src/core/GdUnitExecutor.gd | 403 ------------------ addons/gdUnit4/src/core/GdUnitMemoryPool.gd | 135 ------ .../core/GdUnitObjectInteractionsTemplate.gd | 2 - .../src/core/{Result.gd => GdUnitResult.gd} | 28 +- addons/gdUnit4/src/core/GdUnitRunner.gd | 23 +- addons/gdUnit4/src/core/GdUnitRunner.tscn | 6 +- addons/gdUnit4/src/core/GdUnitRunnerConfig.gd | 18 +- .../gdUnit4/src/core/GdUnitSceneRunnerImpl.gd | 12 +- addons/gdUnit4/src/core/GdUnitScriptType.gd | 7 +- addons/gdUnit4/src/core/GdUnitSettings.gd | 94 ++-- addons/gdUnit4/src/core/GdUnitSignals.gd | 2 +- addons/gdUnit4/src/core/GdUnitSingleton.gd | 4 +- .../src/core/GdUnitStaticDictionary.gd | 66 --- .../src/core/GdUnitTestSuiteBuilder.gd | 10 +- .../src/core/GdUnitTestSuiteScanner.gd | 46 +- addons/gdUnit4/src/core/GdUnitTools.gd | 45 +- .../gdUnit4/src/core/GodotVersionFixures.gd | 11 + addons/gdUnit4/src/core/_TestCase.gd | 63 ++- .../src/core/command/GdUnitCommandHandler.gd | 31 +- addons/gdUnit4/src/core/event/GdUnitEvent.gd | 4 +- .../core/execution/GdUnitExecutionContext.gd | 171 ++++++++ .../core/execution/GdUnitMemoryObserver.gd | 136 ++++++ .../execution/GdUnitTestReportCollector.gd | 70 +++ .../core/execution/GdUnitTestSuiteExecutor.gd | 26 ++ .../stages/GdUnitTestCaseAfterStage.gd | 101 +++++ .../stages/GdUnitTestCaseBeforeStage.gd | 28 ++ .../stages/GdUnitTestCaseExecutionStage.gd | 31 ++ .../stages/GdUnitTestSuiteAfterStage.gd | 28 ++ .../stages/GdUnitTestSuiteBeforeStage.gd | 14 + .../stages/GdUnitTestSuiteExecutionStage.gd | 114 +++++ .../execution/stages/IGdUnitExecutionStage.gd | 39 ++ .../GdUnitTestCaseFuzzedExecutionStage.gd | 21 + .../fuzzed/GdUnitTestCaseFuzzedTestStage.gd | 53 +++ ...UnitTestCaseParameterizedExecutionStage.gd | 22 + .../GdUnitTestCaseParameterizedTestStage.gd | 52 +++ .../GdUnitTestCaseSingleExecutionStage.gd | 22 + .../single/GdUnitTestCaseSingleTestStage.gd | 11 + .../src/core/parse/GdDefaultValueDecoder.gd | 242 +++++++++-- .../src/core/parse/GdFunctionArgument.gd | 4 +- .../src/core/parse/GdFunctionDescriptor.gd | 34 +- .../gdUnit4/src/core/parse/GdScriptParser.gd | 6 +- .../src/core/report/GdUnitReportCollector.gd | 118 ----- .../src/core/thread/GdUnitThreadContext.gd | 41 +- .../src/core/thread/GdUnitThreadManager.gd | 66 ++- .../extractors/GdUnitFuncValueExtractor.gd | 26 +- addons/gdUnit4/src/fuzzers/Fuzzer.gd | 2 +- addons/gdUnit4/src/fuzzers/IntFuzzer.gd | 2 +- addons/gdUnit4/src/fuzzers/StringFuzzer.gd | 2 +- addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd | 2 +- addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd | 2 +- .../gdUnit4/src/mocking/GdUnitMockBuilder.gd | 36 +- .../src/mocking/GdUnitMockFunctionDoubler.gd | 15 +- addons/gdUnit4/src/mocking/GdUnitMockImpl.gd | 99 ++++- .../gdUnit4/src/monitor/GdUnitMemMonitor.gd | 26 -- .../src/monitor/GdUnitOrphanNodesMonitor.gd | 27 ++ addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs | 5 - addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd | 41 -- addons/gdUnit4/src/mono/GdUnit4MonoApi.cs | 17 + .../gdUnit4/src/mono/GdUnit4MonoApiLoader.gd | 64 +++ addons/gdUnit4/src/network/GdUnitTask.gd | 2 +- addons/gdUnit4/src/network/GdUnitTcpClient.gd | 8 +- addons/gdUnit4/src/network/GdUnitTcpServer.gd | 12 +- .../src/network/rpc/dtos/GdUnitTestCaseDto.gd | 8 +- .../gdUnit4/src/report/GdUnitByPathReport.gd | 14 +- addons/gdUnit4/src/report/GdUnitHtmlReport.gd | 60 +-- .../src/report/GdUnitTestCaseReport.gd | 6 +- .../src/report/GdUnitTestSuiteReport.gd | 4 +- addons/gdUnit4/src/report/JUnitXmlReport.gd | 6 +- addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd | 21 +- addons/gdUnit4/src/spy/GdUnitSpyImpl.gd | 19 +- addons/gdUnit4/src/ui/GdUnitConsole.gd | 17 +- addons/gdUnit4/src/ui/GdUnitInspector.gd | 4 +- .../EditorFileSystemContextMenuHandler.gd | 2 +- .../src/ui/menu/GdUnitContextMenuItem.gd | 4 +- .../ui/menu/ScriptEditorContextMenuHandler.gd | 6 +- .../src/ui/parts/InspectorStatusBar.gd | 6 +- .../gdUnit4/src/ui/parts/InspectorToolBar.gd | 12 +- .../src/ui/parts/InspectorTreeMainPanel.gd | 24 +- .../src/ui/settings/GdUnitSettingsDialog.gd | 40 +- .../src/ui/settings/GdUnitSettingsDialog.tscn | 44 +- .../gdUnit4/src/update/GdUnitUpdateNotify.gd | 3 +- data.pandora | 2 +- examples/inventory/world/world_item.gd | 2 +- {test/mock => mock}/custom_mock_entity.gd | 0 .../custom_mock_entity_alternative.gd | 0 .../mock => mock}/entity-compliation-error.gd | 0 {test/mock => mock}/entity-wrong-init.gd | 0 {test/mock => mock}/mock_scene.gd | 0 {test/mock => mock}/mock_scene.tscn | 4 +- {test/mock => mock}/non-entity.gd | 0 project.godot | 2 +- test/backend/entity_backend_test.gd | 10 +- test/scene/mock_scene_test.gd | 2 +- test/scene_test.gd | 4 +- 110 files changed, 2152 insertions(+), 1420 deletions(-) create mode 100644 addons/gdUnit4/src/asserts/GdUnitAssertions.gd delete mode 100644 addons/gdUnit4/src/core/GdUnitExecutor.gd delete mode 100644 addons/gdUnit4/src/core/GdUnitMemoryPool.gd rename addons/gdUnit4/src/core/{Result.gd => GdUnitResult.gd} (72%) delete mode 100644 addons/gdUnit4/src/core/GdUnitStaticDictionary.gd create mode 100644 addons/gdUnit4/src/core/GodotVersionFixures.gd create mode 100644 addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd create mode 100644 addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd create mode 100644 addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd create mode 100644 addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd create mode 100644 addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd delete mode 100644 addons/gdUnit4/src/core/report/GdUnitReportCollector.gd delete mode 100644 addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd create mode 100644 addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd delete mode 100644 addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs delete mode 100644 addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd create mode 100644 addons/gdUnit4/src/mono/GdUnit4MonoApi.cs create mode 100644 addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd rename {test/mock => mock}/custom_mock_entity.gd (100%) rename {test/mock => mock}/custom_mock_entity_alternative.gd (100%) rename {test/mock => mock}/entity-compliation-error.gd (100%) rename {test/mock => mock}/entity-wrong-init.gd (100%) rename {test/mock => mock}/mock_scene.gd (100%) rename {test/mock => mock}/mock_scene.tscn (61%) rename {test/mock => mock}/non-entity.gd (100%) diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index d1df8e7d..6ab26f88 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -23,12 +23,12 @@ class CLIRunner extends Node: var _state = READY var _test_suites_to_process :Array var _executor + var _cs_executor var _report :GdUnitHtmlReport var _report_dir: String var _report_max: int = DEFAULT_REPORT_COUNT var _runner_config := GdUnitRunnerConfig.new() var _console := CmdConsole.new() - var _cs_executor var _cmd_options: = CmdOptions.new([ CmdOption.new("-a, --add", "-a ", "Adds the given test suite or directory to the execution pipeline.", TYPE_STRING), CmdOption.new("-i, --ignore", "-i ", "Adds the given test suite or test case to the ignore list.", TYPE_STRING), @@ -50,19 +50,18 @@ class CLIRunner extends Node: func _ready(): _state = INIT _report_dir = GdUnitTools.current_dir() + "reports" - _executor = load("res://addons/gdUnit4/src/core/GdUnitExecutor.gd").new() + _executor = load("res://addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd").new() # stop checked first test failure to fail fast _executor.fail_fast(true) - if GdUnitTools.is_mono_supported(): - _cs_executor = GdUnit3MonoAPI.create_executor(self) - + if GdUnit4MonoApiLoader.is_mono_supported(): + prints("GdUnit4Mono Version %s loaded." % GdUnit4MonoApiLoader.version()) + _cs_executor = GdUnit4MonoApiLoader.create_executor(self) var err = GdUnitSignals.instance().gdunit_event.connect(Callable(self, "_on_gdunit_event")) if err != OK: prints("gdUnitSignals failed") push_error("Error checked startup, can't connect executor for 'send_event'") quit(RETURN_ERROR) - add_child(_executor) func _process(_delta): @@ -78,10 +77,11 @@ class CLIRunner extends Node: set_process(false) # process next test suite var test_suite := _test_suites_to_process.pop_front() as Node - add_child(test_suite) - var executor = _cs_executor if GdObjects.is_cs_test_suite(test_suite) else _executor - executor.Execute(test_suite) - await executor.ExecutionCompleted + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) set_process(true) STOP: _state = EXIT @@ -90,9 +90,8 @@ class CLIRunner extends Node: func quit(code :int) -> void: - if is_instance_valid(_executor): - _executor.free() GdUnitTools.dispose_all() + await GdUnitMemoryObserver.gc_on_guarded_instances() await get_tree().physics_frame get_tree().quit(code) @@ -290,8 +289,8 @@ class CLIRunner extends Node: return total - func PublishEvent(data) -> void: - _on_gdunit_event(GdUnitEvent.new().deserialize(data.AsDictionary())) + func PublishEvent(data :Dictionary) -> void: + _on_gdunit_event(GdUnitEvent.new().deserialize(data)) func _on_gdunit_event(event :GdUnitEvent): @@ -399,9 +398,6 @@ func _initialize(): func _finalize(): prints("Finallize ..") - _cli_runner.free() prints("-Orphan nodes report-----------------------") Window.print_orphan_nodes() - prints("-SceneTree report-----------------------") - root.print_tree_pretty() prints("Finallize .. done") diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd index 01495eef..5f57806f 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -91,13 +91,13 @@ func _patch_report(report_path :String, godot_log :String) -> void: index_file.seek(0) index_file.store_string(content) -func _copy_and_pach(from_file: String, to_dir: String) -> Result: +func _copy_and_pach(from_file: String, to_dir: String) -> GdUnitResult: var result := GdUnitTools.copy_file(from_file, to_dir) if result.is_error(): return result var file := FileAccess.open(from_file, FileAccess.READ) if file == null: - return Result.error("Can't find file '%s'. Error: %s" % [from_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) + return GdUnitResult.error("Can't find file '%s'. Error: %s" % [from_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) var content := file.get_as_text() # patch out console format codes for color_index in range(0, 256): @@ -110,9 +110,9 @@ func _copy_and_pach(from_file: String, to_dir: String) -> Result: var to_file := to_dir + "/" + from_file.get_file() file = FileAccess.open(to_file, FileAccess.WRITE) if file == null: - return Result.error("Can't open to write '%s'. Error: %s" % [to_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) + return GdUnitResult.error("Can't open to write '%s'. Error: %s" % [to_file, GdUnitTools.error_as_string(FileAccess.get_open_error())]) file.store_string(content) - return Result.empty() + return GdUnitResult.empty() func reports_available() -> bool: return DirAccess.dir_exists_absolute(_report_root_path) diff --git a/addons/gdUnit4/bin/ProjectScanner.gd b/addons/gdUnit4/bin/ProjectScanner.gd index 75070b29..74bbbcf4 100644 --- a/addons/gdUnit4/bin/ProjectScanner.gd +++ b/addons/gdUnit4/bin/ProjectScanner.gd @@ -4,28 +4,28 @@ extends SceneTree const CmdConsole = preload("res://addons/gdUnit4/src/cmd/CmdConsole.gd") -var scanner := ProjectScanner.new() +var scanner := SourceScanner.new() func _initialize(): + set_auto_accept_quit(false) root.add_child(scanner) func _finalize(): - if Engine.get_version_info().hex < 0x40100 or Engine.get_version_info().hex > 0x40101: - print("Finalize scanner ..") - scanner.free() - if Engine.get_version_info().hex < 0x40100 or Engine.get_version_info().hex > 0x40101: - prints("done") + prints("__finalize") -class ProjectScanner extends Node: + +class SourceScanner extends Node: enum { INIT, SCAN, - QUIT + QUIT, + DONE } + var _counter = 0 var WAIT_TIME_IN_MS = 5.000 var _state = INIT @@ -46,7 +46,9 @@ class ProjectScanner extends Node: _console.prints_color("Running project scan:", Color.CORNFLOWER_BLUE) await scan_project() set_process(true) + _state = QUIT if _state == QUIT or _counter >= WAIT_TIME_IN_MS: + _state = DONE _console.prints_color("Scan project done.", Color.CORNFLOWER_BLUE) _console.prints_color("======================================", Color.CORNFLOWER_BLUE) _console.new_line() @@ -58,24 +60,31 @@ class ProjectScanner extends Node: var plugin := EditorPlugin.new() var fs := plugin.get_editor_interface().get_resource_filesystem() - _console.prints_color("Scan :", Color.SANDY_BROWN) - _console.progressBar(0) - fs.scan() - await get_tree().process_frame - while fs.is_scanning(): - await get_tree().process_frame - _console.progressBar(fs.get_scanning_progress() * 100 as int) - _console.progressBar(100) - _console.new_line() - + if fs.has_method("reimport_files--"): + _console.prints_color("Reimport images :", Color.SANDY_BROWN) + for source in ["res://addons/gdUnit4/src/ui/assets/orphan", "res://addons/gdUnit4/src/ui/assets/spinner", "res://addons/gdUnit4/src/ui/assets/"]: + var image_files := Array(DirAccess.get_files_at(source)) + #_console.prints_color("%s" % image_files, Color.SANDY_BROWN) + var files := image_files.map(func full_path(file_name): + return "%s/%s" % [source, file_name] )\ + .filter(func filter_import_files(path :String): + return path.get_extension() != "import") + prints(files) + fs.reimport_files(files) + _console.prints_color("Scan sources: ", Color.SANDY_BROWN) - _console.progressBar(0) fs.scan_sources() + await get_tree().create_timer(5).timeout + await get_tree().process_frame + + _console.prints_color("Scan: ", Color.SANDY_BROWN) + fs.scan() await get_tree().process_frame while fs.is_scanning(): await get_tree().process_frame _console.progressBar(fs.get_scanning_progress() * 100 as int) _console.progressBar(100) _console.new_line() - plugin.free() - _state = QUIT + await get_tree().process_frame + plugin.queue_free() + await get_tree().process_frame diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index 2d4b1e82..065e0318 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -23,6 +23,8 @@ func _enter_tree(): if GdUnitSettings.is_update_notification_enabled(): var update_tool = load("res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn").instantiate() Engine.get_main_loop().root.call_deferred("add_child", update_tool) + if GdUnit4MonoApiLoader.is_mono_supported(): + prints("GdUnit4Mono Version %s loaded." % GdUnit4MonoApiLoader.version()) func _exit_tree(): diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd index 5c70548d..fbd410e1 100644 --- a/addons/gdUnit4/runtest.cmd +++ b/addons/gdUnit4/runtest.cmd @@ -18,7 +18,7 @@ IF "%GODOT_TYPE%" == "mono" ( %GODOT_BIN% -s -d .\addons\gdUnit4\bin\GdUnitCmdTool.gd %* SET exit_code=%errorlevel% -%GODOT_BIN% --no-window --quiet -s -d .\addons\gdUnit4\bin\GdUnitCopyLog.gd %* +%GODOT_BIN% --headless --quiet -s -d .\addons\gdUnit4\bin\GdUnitCopyLog.gd %* ECHO %exit_code% diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd index c8fb2577..c1fc2ccf 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -9,7 +9,7 @@ const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertI # signal_name: signal name # args: the expected signal arguments as an array # timeout: the timeout in ms, default is set to 2000ms -static func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: +func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: # fail fast if the given source instance invalid var line_number := GdUnitAssert._get_line_number() if not is_instance_valid(source): @@ -34,7 +34,7 @@ static func await_signal_on(source :Object, signal_name :String, args :Array = [ # signal_name: signal name # args: the expected signal arguments as an array # timeout: the timeout in ms, default is set to 2000ms -static func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: +func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant: var line_number := GdUnitAssert._get_line_number() # fail fast if the given source instance invalid if not is_instance_valid(source): @@ -54,17 +54,17 @@ static func await_signal_idle_frames(source :Object, signal_name :String, args : # # waits for 100ms # await GdUnitAwaiter.await_millis(myNode, 100).completed # use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out -static func await_millis(milliSec :int) -> void: +func await_millis(milliSec :int) -> void: var timer :Timer = Timer.new() timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id()) Engine.get_main_loop().root.add_child(timer) timer.add_to_group("GdUnitTimers") timer.set_one_shot(true) - timer.start(milliSec * 0.001) + timer.start(milliSec / 1000.0) await timer.timeout timer.queue_free() # Waits until the next idle frame -static func await_idle_frame() -> void: +func await_idle_frame() -> void: await Engine.get_main_loop().process_frame diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd index 55310039..30734918 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -21,6 +21,10 @@ const NO_ARG :Variant = GdUnitConstants.NO_ARG var __is_skipped := false @warning_ignore("unused_private_class_variable") var __skip_reason :String = "Unknow." +var __active_test_case :String +var __awaiter := __gdunit_awaiter() +# holds the actual execution context +var __execution_context ### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" @@ -28,7 +32,7 @@ var __skip_reason :String = "Unknow." # We go this hard way to increase the loading performance to avoid reparsing all the used scripts # for more detailed info -> https://github.com/godotengine/godot/issues/67400 func __lazy_load(script_path :String) -> GDScript: - return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + return GdUnitAssertions.__lazy_load(script_path) func __gdunit_assert() -> GDScript: @@ -39,8 +43,8 @@ func __gdunit_tools() -> GDScript: return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd") -func __gdunit_awaiter() -> GDScript: - return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd") +func __gdunit_awaiter() -> Object: + return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new() func __gdunit_argument_matchers(): @@ -79,7 +83,6 @@ func is_failure(_expected_failure :String = NO_ARG) -> bool: return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false -var __active_test_case :String func set_active_test_case(test_case :String) -> void: __active_test_case = test_case @@ -93,8 +96,14 @@ 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: - var GdUnitMemoryPool = __lazy_load("res://addons/gdUnit4/src/core/GdUnitMemoryPool.gd") - return GdUnitMemoryPool.register_auto_free(obj, get_meta(GdUnitMemoryPool.META_PARAM)) + 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() ## Discard the error message triggered by a timeout (interruption).[br] @@ -151,12 +160,12 @@ 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: - return await __gdunit_awaiter().await_signal_on(source, signal_name, args, timeout) + return await __awaiter.await_signal_on(source, signal_name, args, timeout) ## Waits until the next idle frame func await_idle_frame(): - await __gdunit_awaiter().await_idle_frame() + await __awaiter.await_idle_frame() ## Waits for for a given amount of milliseconds[br] @@ -167,7 +176,7 @@ func await_idle_frame(): ## [/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): - await __gdunit_awaiter().await_millis(timeout) + await __awaiter.await_millis(timeout) ## Creates a new scene runner to allow simulate interactions checked a scene.[br] @@ -198,12 +207,12 @@ const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB ## Creates a mock for given class name func mock(clazz, mock_mode := RETURN_DEFAULTS) -> Object: - return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(self, clazz, mock_mode) + return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode) ## Creates a spy checked given object instance func spy(instance) -> Object: - return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(self, instance) + return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance) ## Configures a return value for the specified function and used arguments.[br] diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd index 005f015b..42a8ceb5 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -225,7 +225,7 @@ static func error_not_same_error(current, expected) -> String: return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)] -static func error_is_instanceof(current: Result, expected :Result) -> String: +static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String: return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\ _colored_value(expected.or_else(null)), _colored_value(current.or_else(null))] @@ -408,20 +408,20 @@ static func error_contains_key_value(key, value, current_value, compare_mode :Gd # - ResultAssert specific errors ---------------------------------------------------- -static func error_result_is_empty(current :Result) -> String: - return _result_error_message(current, Result.EMPTY) +static func error_result_is_empty(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.EMPTY) -static func error_result_is_success(current :Result) -> String: - return _result_error_message(current, Result.SUCCESS) +static func error_result_is_success(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.SUCCESS) -static func error_result_is_warning(current :Result) -> String: - return _result_error_message(current, Result.WARN) +static func error_result_is_warning(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.WARN) -static func error_result_is_error(current :Result) -> String: - return _result_error_message(current, Result.ERROR) +static func error_result_is_error(current :GdUnitResult) -> String: + return _result_error_message(current, GdUnitResult.ERROR) static func error_result_has_message(current :String, expected :String) -> String: @@ -429,14 +429,14 @@ static func error_result_has_message(current :String, expected :String) -> Strin static func error_result_has_message_on_success(expected :String) -> String: - return "%s\n %s\n but the Result is a success." % [_error("Expecting:"), _colored_value(expected)] + return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)] static func error_result_is_value(current, expected) -> String: return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)] -static func _result_error_message(current :Result, expected_type :int) -> String: +static func _result_error_message(current :GdUnitResult, expected_type :int) -> String: if current == null: return _error("Expecting the result must be a %s but was ." % result_type(expected_type)) if current.is_success(): @@ -468,19 +468,19 @@ static func error_await_signal_on_invalid_instance(source, signal_name :String, static func result_type(type :int) -> String: match type: - Result.SUCCESS: return "SUCCESS" - Result.WARN: return "WARNING" - Result.ERROR: return "ERROR" - Result.EMPTY: return "EMPTY" + GdUnitResult.SUCCESS: return "SUCCESS" + GdUnitResult.WARN: return "WARNING" + GdUnitResult.ERROR: return "ERROR" + GdUnitResult.EMPTY: return "EMPTY" return "UNKNOWN" -static func result_message(result :Result) -> String: +static func result_message(result :GdUnitResult) -> String: match result._state: - Result.SUCCESS: return "" - Result.WARN: return result.warn_message() - Result.ERROR: return result.error_message() - Result.EMPTY: return "" + GdUnitResult.SUCCESS: return "" + GdUnitResult.WARN: return result.warn_message() + GdUnitResult.ERROR: return result.error_message() + GdUnitResult.EMPTY: return "" return "UNKNOWN" # ----------------------------------------------------------------------------------- diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd index 8e822173..dc427ed3 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd @@ -55,4 +55,5 @@ static func current_failure() -> String: static func send_report(report :GdUnitReport) -> void: - GdUnitSignals.instance().gdunit_report.emit(report) + var execution_context_id := GdUnitThreadManager.get_current_context().get_execution_context_id() + GdUnitSignals.instance().gdunit_report.emit(execution_context_id, report) diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd new file mode 100644 index 00000000..0f7219b3 --- /dev/null +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -0,0 +1,30 @@ +# Preloads all GdUnit assertions +class_name GdUnitAssertions +extends RefCounted + + +func _init(): + # preload all gdunit assertions to speedup testsuite loading time + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd") + GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd") + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +static func __lazy_load(script_path :String) -> GDScript: + return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd index 0ad5dd6d..0c9e4b9f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -1,18 +1,16 @@ extends GdUnitFuncAssert -signal value_provided(value) const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") - const DEFAULT_TIMEOUT := 2000 + var _current_value_provider :ValueProvider var _current_error_message :String = "" var _custom_failure_message :String = "" var _line_number := -1 var _timeout := DEFAULT_TIMEOUT var _interrupted := false - var _sleep_timer :Timer = null @@ -24,21 +22,28 @@ func _init(instance :Object, func_name :String, args := Array()): # verify at first the function name exists if not instance.has_method(func_name): report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance]) + _interrupted = true else: _current_value_provider = CallBackValueProvider.new(instance, func_name, args) func _notification(_what): - if is_instance_valid(self): - dispose() + if is_instance_valid(_current_value_provider): + _current_value_provider.dispose() + _current_value_provider = null + if is_instance_valid(_sleep_timer): + Engine.get_main_loop().root.remove_child(_sleep_timer) + _sleep_timer.stop() + _sleep_timer.free() + _sleep_timer = null -func report_success() -> GdUnitAssert: +func report_success() -> GdUnitFuncAssert: GdAssertReports.report_success() return self -func report_error(error_message :String) -> GdUnitAssert: +func report_error(error_message :String) -> GdUnitFuncAssert: _current_error_message = error_message if _custom_failure_message == "" else _custom_failure_message GdAssertReports.report_error(_current_error_message, _line_number) return self @@ -67,30 +72,49 @@ func wait_until(timeout := 2000) -> GdUnitFuncAssert: func is_null() -> GdUnitFuncAssert: - return await _validate_callback(func is_null(c, _e): return c == null) + await _validate_callback(__is_null) + return self func is_not_null() -> GdUnitFuncAssert: - return await _validate_callback(func is_not_null(c, _e): return c != null) + await _validate_callback(__is_not_null) + return self func is_false() -> GdUnitFuncAssert: - return await _validate_callback(func is_false(c, _e): return c == false) + await _validate_callback(__is_false) + return self func is_true() -> GdUnitFuncAssert: - return await _validate_callback(func is_true(c, _e): return c == true) + await _validate_callback(__is_true) + return self func is_equal(expected) -> GdUnitFuncAssert: - return await _validate_callback(func is_equal(c, e): return GdObjects.equals(c, e), expected) + await _validate_callback(__is_equal, expected) + return self func is_not_equal(expected) -> GdUnitFuncAssert: - return await _validate_callback(func is_not_equal(c, e): return not GdObjects.equals(c, e), expected) + await _validate_callback(__is_not_equal, expected) + return self + +# we need actually to define this Callable as functions otherwise we results into leaked scripts here +# this is actually a Godot bug and needs this kind of workaround +func __is_null(c, _e): return c == null +func __is_not_null(c, _e): return c != null +func __is_false(c, _e): return c == false +func __is_true(c, _e): return c == true +func __is_equal(c, e): return GdObjects.equals(c,e) +func __is_not_equal(c, e): return not GdObjects.equals(c, e) -func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAssert: + +func _validate_callback(predicate :Callable, expected = null): + if _interrupted: + return + GdUnitMemoryObserver.guard_instance(self) var time_scale = Engine.get_time_scale() var timer := Timer.new() timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id()) @@ -98,8 +122,7 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser timer.add_to_group("GdUnitTimers") timer.timeout.connect(func do_interrupt(): _interrupted = true - value_provided.emit(null) - , CONNECT_REFERENCE_COUNTED) + , CONNECT_DEFERRED) timer.set_one_shot(true) timer.start((_timeout/1000.0)*time_scale) _sleep_timer = Timer.new() @@ -107,12 +130,9 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser Engine.get_main_loop().root.add_child(_sleep_timer) while true: - next_current_value() - var current = await value_provided - if _interrupted: - break - var is_success = predicate.call(current, expected) - if is_success: + var current = await next_current_value() + # is interupted or predicate success + if _interrupted or predicate.call(current, expected): break if is_instance_valid(_sleep_timer): _sleep_timer.start(0.05) @@ -120,31 +140,20 @@ func _validate_callback(predicate :Callable, expected = null) -> GdUnitFuncAsser _sleep_timer.stop() await Engine.get_main_loop().process_frame - dispose() if _interrupted: # https://github.com/godotengine/godot/issues/73052 #var predicate_name = predicate.get_method() - var predicate_name = str(predicate).split('(')[0] - report_error(GdAssertMessages.error_interrupted(predicate_name, expected, LocalTime.elapsed(_timeout))) + var predicate_name :String = str(predicate).split('::')[1] + report_error(GdAssertMessages.error_interrupted(predicate_name.strip_edges().trim_prefix("__"), expected, LocalTime.elapsed(_timeout))) else: report_success() - return self + _sleep_timer.free() + timer.free() + GdUnitMemoryObserver.unguard_instance(self) -func next_current_value(): +func next_current_value() -> Variant: @warning_ignore("redundant_await") if is_instance_valid(_current_value_provider): - var current = await _current_value_provider.get_value() - call_deferred("emit_signal", "value_provided", current) - - -# it is important to free all references/connections to prevent orphan nodes -func dispose(): - GdUnitTools._release_connections(self) - if is_instance_valid(_current_value_provider): - _current_value_provider.dispose() - _current_value_provider = null - if is_instance_valid(_sleep_timer): - _sleep_timer.stop() - _sleep_timer.free() - _sleep_timer = null + return await _current_value_provider.get_value() + return "invalid value" diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index a6cebba1..ccc92e56 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -19,11 +19,11 @@ func _notification(event): func __validate_value_type(value) -> bool: - return value == null or value is Result + return value == null or value is GdUnitResult -func __current() -> Result: - return _base.__current() as Result +func __current() -> GdUnitResult: + return _base.__current() as GdUnitResult func report_success() -> GdUnitResultAssert: diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd index 96293226..74adebb1 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd @@ -11,7 +11,7 @@ func _init(p_options :CmdOptions, p_tool_name :String): _tool_name = p_tool_name -func parse(args :Array, ignore_unknown_cmd := false) -> Result: +func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult: _parsed_commands.clear() # parse until first program argument @@ -21,7 +21,7 @@ func parse(args :Array, ignore_unknown_cmd := false) -> Result: break if args.is_empty(): - return Result.empty() + return GdUnitResult.empty() # now parse all arguments while not args.is_empty(): @@ -30,10 +30,10 @@ func parse(args :Array, ignore_unknown_cmd := false) -> Result: if option: if _parse_cmd_arguments(option, args) == -1: - return Result.error("The '%s' command requires an argument!" % option.short_command()) + return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command()) elif not ignore_unknown_cmd: - return Result.error("Unknown '%s' command!" % cmd) - return Result.success(_parsed_commands.values()) + return GdUnitResult.error("Unknown '%s' command!" % cmd) + return GdUnitResult.success(_parsed_commands.values()) func options() -> CmdOptions: diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd index 7105893f..e32557ec 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd @@ -47,7 +47,7 @@ func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler: return self -func _validate() -> Result: +func _validate() -> GdUnitResult: var errors: = PackedStringArray() var registered_cbs: = Dictionary() @@ -66,12 +66,12 @@ func _validate() -> Result: else: registered_cbs[cb_method] = cmd_name if errors.is_empty(): - return Result.success(true) + return GdUnitResult.success(true) else: - return Result.error("\n".join(errors)) + return GdUnitResult.error("\n".join(errors)) -func execute(commands :Array) -> Result: +func execute(commands :Array) -> GdUnitResult: var result := _validate() if result.is_error(): return result @@ -89,4 +89,4 @@ func execute(commands :Array) -> Result: cb_s.call(cmd.arguments()[CB_SINGLE_ARG]) else: cb_m.callv(cmd.arguments()) - return Result.success(true) + return GdUnitResult.success(true) diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index 12f2a03e..ee4bf792 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -375,7 +375,7 @@ static func is_script(value) -> bool: static func is_test_suite(script :Script) -> bool: - return is_gd_testsuite(script) or GdUnit3MonoAPI.is_test_suite(script.resource_path) + return is_gd_testsuite(script) or GdUnit4MonoApiLoader.is_test_suite(script.resource_path) static func is_native_class(value) -> bool: @@ -399,10 +399,6 @@ static func is_cs_script(script :Script) -> bool: return str(script).find("CSharpScript") != -1 -static func is_cs_test_suite(instance :Node) -> bool: - return instance.get("IsCsTestSuite") == true - - static func is_gd_testsuite(script :Script) -> bool: if is_gd_script(script): var stack := [script] @@ -449,30 +445,30 @@ static func can_be_instantiate(obj :Variant) -> bool: return obj.has_method("new") -static func create_instance(clazz) -> Result: +static func create_instance(clazz) -> GdUnitResult: match typeof(clazz): TYPE_OBJECT: # test is given clazz already an instance if is_instance(clazz): - return Result.success(clazz) - return Result.success(clazz.new()) + return GdUnitResult.success(clazz) + return GdUnitResult.success(clazz.new()) TYPE_STRING: if ClassDB.class_exists(clazz): if Engine.has_singleton(clazz): - return Result.error("Not allowed to create a instance for singelton '%s'." % clazz) + return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz) if not ClassDB.can_instantiate(clazz): - return Result.error("Can't instance Engine class '%s'." % clazz) - return Result.success(ClassDB.instantiate(clazz)) + return GdUnitResult.error("Can't instance Engine class '%s'." % clazz) + return GdUnitResult.success(ClassDB.instantiate(clazz)) else: var clazz_path :String = extract_class_path(clazz)[0] if not FileAccess.file_exists(clazz_path): - return Result.error("Class '%s' not found." % clazz) + return GdUnitResult.error("Class '%s' not found." % clazz) var script = load(clazz_path) if script != null: - return Result.success(script.new()) + return GdUnitResult.success(script.new()) else: - return Result.error("Can't create instance for '%s'." % clazz) - return Result.error("Can't create instance for class '%s'." % clazz) + return GdUnitResult.error("Can't create instance for '%s'." % clazz) + return GdUnitResult.error("Can't create instance for class '%s'." % clazz) static func extract_class_path(clazz) -> PackedStringArray: @@ -517,38 +513,38 @@ static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> return clazz_name -static func extract_class_name(clazz) -> Result: +static func extract_class_name(clazz) -> GdUnitResult: if clazz == null: - return Result.error("Can't extract class name form a null value.") + return GdUnitResult.error("Can't extract class name form a null value.") if is_instance(clazz): # is instance a script instance? var script := clazz.script as GDScript if script != null: return extract_class_name(script) - return Result.success(clazz.get_class()) + return GdUnitResult.success(clazz.get_class()) # extract name form full qualified class path if clazz is String: if ClassDB.class_exists(clazz): - return Result.success(clazz) + return GdUnitResult.success(clazz) var source_sript :Script = load(clazz) var clazz_name = load("res://addons/gdUnit4/src/core/parse/GdScriptParser.gd").new().get_class_name(source_sript) - return Result.success(to_pascal_case(clazz_name)) + return GdUnitResult.success(to_pascal_case(clazz_name)) if is_primitive_type(clazz): - return Result.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) + return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz))) if is_script(clazz): if clazz.resource_path.is_empty(): var class_path = extract_class_name_from_class_path(extract_class_path(clazz)) - return Result.success(class_path); + return GdUnitResult.success(class_path); return extract_class_name(clazz.resource_path) # need to create an instance for a class typ the extract the class name var instance = clazz.new() if instance == null: - return Result.error("Can't create a instance for class '%s'" % clazz) + return GdUnitResult.error("Can't create a instance for class '%s'" % clazz) var result := extract_class_name(instance) GdUnitTools.free_instance(instance) return result diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd index d8ff4942..3ff12e9f 100644 --- a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd +++ b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd @@ -2,6 +2,9 @@ class_name GdUnitClassDoubler extends RefCounted + +const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" +const DOUBLER_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd") const EXCLUDE_VIRTUAL_FUNCTIONS = [ # we have to exclude notifications because NOTIFICATION_PREDELETE is try # to delete already freed spy/mock resources and will result in a conflict @@ -11,7 +14,6 @@ const EXCLUDE_VIRTUAL_FUNCTIONS = [ "get_path", "duplicate", ] - # define functions to be exclude when spy or mock checked a scene const EXLCUDE_SCENE_FUNCTIONS = [ # needs to exclude get/set script functions otherwise it endsup in recursive endless loop @@ -20,28 +22,30 @@ const EXLCUDE_SCENE_FUNCTIONS = [ # needs to exclude otherwise verify fails checked collection arguments checked calling to string "_to_string", ] - const EXCLUDE_FUNCTIONS = ["new", "free", "get_instance_id", "get_tree"] +static func check_leaked_instances() -> void: + ## we check that all registered spy/mock instances are removed from the engine meta data + for key in Engine.get_meta_list(): + if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): + var instance = Engine.get_meta(key) + push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + + # loads the doubler template # class_info = { "class_name": <>, "class_path" : <>} -static func load_template(template :Object, class_info :Dictionary, instance :Object) -> PackedStringArray: - var source_code = template.new().get_script().source_code +static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray: # store instance id - source_code = source_code.replace("${instance_id}", "instance_%d" % instance.get_instance_id()) + var source_code = template\ + .replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\ + .replace("${source_class}", class_info.get("class_name")) var lines := GdScriptParser.to_unix_format(source_code).split("\n") # replace template class_name with Doubled name and extends form source class - lines.remove_at(2) - lines.insert(2, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) - lines.insert(3, extends_clazz(class_info)) - - var eol := lines.size() + lines.insert(0, "class_name Doubled%s" % class_info.get("class_name").replace(".", "_")) + lines.insert(1, extends_clazz(class_info)) # append Object interactions stuff - source_code = GdUnitObjectInteractionsTemplate.new().get_script().source_code - lines.append_array(GdScriptParser.to_unix_format(source_code).split("\n")) - # remove_at the class header from GdUnitObjectInteractionsTemplate - lines.remove_at(eol) + lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n")) return lines diff --git a/addons/gdUnit4/src/core/GdUnitExecutor.gd b/addons/gdUnit4/src/core/GdUnitExecutor.gd deleted file mode 100644 index daf045eb..00000000 --- a/addons/gdUnit4/src/core/GdUnitExecutor.gd +++ /dev/null @@ -1,403 +0,0 @@ -extends Node - -signal ExecutionCompleted() - -const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const GdUnitMemoryPool = preload("res://addons/gdUnit4/src/core/GdUnitMemoryPool.gd") - -const INIT = 0 -const STAGE_TEST_SUITE_BEFORE = GdUnitReportCollector.STAGE_TEST_SUITE_BEFORE -const STAGE_TEST_SUITE_AFTER = GdUnitReportCollector.STAGE_TEST_SUITE_AFTER -const STAGE_TEST_CASE_BEFORE = GdUnitReportCollector.STAGE_TEST_CASE_BEFORE -const STAGE_TEST_CASE_EXECUTE = GdUnitReportCollector.STAGE_TEST_CASE_EXECUTE -const STAGE_TEST_CASE_AFTER = GdUnitReportCollector.STAGE_TEST_CASE_AFTER - -var _testsuite_timer :LocalTime -var _testcase_timer :LocalTime - -var _memory_pool := GdUnitMemoryPool.new() -var _report_errors_enabled :bool -var _report_collector := GdUnitReportCollector.new() -var _expression_runner := GdUnitExpressionRunner.new() - - -var _total_test_execution_orphans :int -var _total_test_warnings :int -var _total_test_failed :int -var _total_test_errors :int -var _fail_fast := false -var _debug := false - - -func _init(debug := false): - set_name("GdUnitExecutor%s" % ("Debug" if debug else "")) - _debug = debug - - -func _ready(): - _report_errors_enabled = GdUnitSettings.is_report_push_errors() - - -func fail_fast(enabled :bool) -> void: - _fail_fast = enabled - - -func set_stage(stage :int) -> void: - _report_collector.set_stage(stage) - - -func set_consume_reports(enabled :bool) -> void: - _report_collector.set_consume_reports(enabled) - - -func fire_event(event :GdUnitEvent) -> void: - if _debug: - GdUnitSignals.instance().gdunit_event_debug.emit(event) - else: - GdUnitSignals.instance().gdunit_event.emit(event) - - -func fire_test_skipped(test_suite :GdUnitTestSuite, test_case :_TestCase): - set_stage(STAGE_TEST_CASE_BEFORE) - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name())) - var statistics = { - GdUnitEvent.ORPHAN_NODES: 0, - GdUnitEvent.ELAPSED_TIME: 0, - GdUnitEvent.WARNINGS: false, - GdUnitEvent.ERRORS: false, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: false, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED: true, - GdUnitEvent.SKIPPED_COUNT: 1, - } - set_stage(STAGE_TEST_CASE_AFTER) - 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.get_name(), statistics, [report])) - - -func fire_test_suite_skipped(test_suite :GdUnitTestSuite): - var skip_count := test_suite.get_child_count() - set_stage(STAGE_TEST_SUITE_BEFORE) - fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), skip_count)) - var statistics = { - GdUnitEvent.ORPHAN_NODES: 0, - GdUnitEvent.ELAPSED_TIME: 0, - GdUnitEvent.WARNINGS: false, - GdUnitEvent.ERRORS: false, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: false, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED_COUNT: skip_count, - GdUnitEvent.SKIPPED: true - } - set_stage(STAGE_TEST_SUITE_AFTER) - 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])) - - -func suite_before(test_suite :GdUnitTestSuite): - set_stage(STAGE_TEST_SUITE_BEFORE) - fire_event(GdUnitEvent.new()\ - .suite_before(test_suite.get_script().resource_path, test_suite.get_name(), test_suite.get_child_count())) - _testsuite_timer = LocalTime.now() - _total_test_errors = 0 - _total_test_failed = 0 - _total_test_warnings = 0 - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTSUITE, true) - @warning_ignore("redundant_await") - await test_suite.before() - _memory_pool.monitor_stop() - - -func suite_after(test_suite :GdUnitTestSuite): - set_stage(STAGE_TEST_SUITE_AFTER) - GdUnitTools.clear_tmp() - - var is_warning := _total_test_warnings != 0 - var is_skipped := test_suite.__is_skipped - var skip_count := test_suite.get_child_count() - var orphan_nodes := 0 - var reports := _report_collector.get_reports(STAGE_TEST_SUITE_BEFORE) - - if not is_skipped: - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTSUITE) - skip_count = 0 - @warning_ignore("redundant_await") - await test_suite.after() - reports.append_array(_report_collector.get_reports(STAGE_TEST_SUITE_AFTER)) - _memory_pool.free_pool() - _memory_pool.monitor_stop() - orphan_nodes = _memory_pool.orphan_nodes() - if orphan_nodes > 0: - reports.push_front(GdUnitReport.new() \ - .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphan_nodes))) - - var is_error := _total_test_errors != 0 or _report_collector.has_errors(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - var is_failed := _total_test_failed != 0 or _report_collector.has_failures(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - # create report - var statistics = { - GdUnitEvent.ORPHAN_NODES: orphan_nodes, - GdUnitEvent.ELAPSED_TIME: _testsuite_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: is_warning, - GdUnitEvent.ERRORS: is_error, - GdUnitEvent.ERROR_COUNT: _report_collector.count_errors(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER), - GdUnitEvent.FAILED: is_failed, - GdUnitEvent.FAILED_COUNT: _report_collector.count_failures(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER), - GdUnitEvent.SKIPPED_COUNT: skip_count, - GdUnitEvent.SKIPPED: is_skipped - } - fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), statistics, reports)) - _report_collector.clear_reports(STAGE_TEST_SUITE_BEFORE|STAGE_TEST_SUITE_AFTER) - - -func test_before(test_suite :GdUnitTestSuite, test_case_name :String, do_fire_event := true): - set_stage(STAGE_TEST_CASE_BEFORE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTCASE, true) - - _total_test_execution_orphans = 0 - if do_fire_event: - _testcase_timer = LocalTime.now() - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name)) - - @warning_ignore("redundant_await") - await test_suite.before_test() - _memory_pool.monitor_stop() - - -func test_after(test_suite :GdUnitTestSuite, test_case :_TestCase, test_case_name :String, do_fire_event := true): - _memory_pool.free_pool() - # give objects time to finallize - await get_tree().process_frame - _memory_pool.monitor_stop() - var execution_orphan_nodes = _memory_pool.orphan_nodes() - if execution_orphan_nodes > 0: - _total_test_execution_orphans += execution_orphan_nodes - _total_test_warnings += 1 - _report_collector.push_front(STAGE_TEST_CASE_EXECUTE, GdUnitReport.new() \ - .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(execution_orphan_nodes))) - - var is_error := false - if test_case.is_interupted() and not test_case.is_expect_interupted(): - _report_collector.add_report(STAGE_TEST_CASE_EXECUTE, test_case.report()) - is_error = true - - set_stage(STAGE_TEST_CASE_AFTER) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.TESTCASE) - @warning_ignore("redundant_await") - await test_suite.after_test() - _memory_pool.free_pool() - _memory_pool.monitor_stop() - var test_setup_orphan_nodes = _memory_pool.orphan_nodes() - if test_setup_orphan_nodes > 0: - _total_test_warnings += 1 - _total_test_execution_orphans += test_setup_orphan_nodes - _report_collector.push_front(STAGE_TEST_CASE_AFTER, GdUnitReport.new() \ - .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(test_setup_orphan_nodes))) - - var reports := _report_collector.get_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - var error_count := _report_collector.count_errors(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) if is_error else 0 - var failure_count := _report_collector.count_failures(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - var is_warning := _report_collector.has_warnings(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - - _total_test_errors += error_count - _total_test_failed += failure_count - var statistics = { - GdUnitEvent.ORPHAN_NODES: _total_test_execution_orphans, - GdUnitEvent.ELAPSED_TIME: _testcase_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: is_warning, - GdUnitEvent.ERRORS: is_error, - GdUnitEvent.ERROR_COUNT: error_count, - GdUnitEvent.FAILED: failure_count > 0, - GdUnitEvent.FAILED_COUNT: failure_count, - GdUnitEvent.SKIPPED: test_case.is_skipped(), - GdUnitEvent.SKIPPED_COUNT: int(test_case.is_skipped()), - } - - if do_fire_event: - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case_name, statistics, reports.duplicate())) - _report_collector.clear_reports(STAGE_TEST_CASE_BEFORE|STAGE_TEST_CASE_EXECUTE|STAGE_TEST_CASE_AFTER) - - -func execute_test_case_single(test_suite :GdUnitTestSuite, test_case :_TestCase): - await test_before(test_suite, test_case.get_name()) - - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - test_case.generate_seed() - await test_case.execute() - test_case.dispose() - await test_after(test_suite, test_case, test_case.get_name()) - - -func execute_test_case_iterative(test_suite :GdUnitTestSuite, test_case :_TestCase): - test_case.generate_seed() - var fuzzers := create_fuzzers(test_suite, test_case) - var is_failure := false - for iteration in test_case.iterations(): - # call before_test for each iteration - await test_before(test_suite, test_case.get_name(), iteration==0) - - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - await test_case.execute(fuzzers, iteration) - - var reports := _report_collector.get_reports(STAGE_TEST_CASE_EXECUTE) - # interrupt at first failure - if not reports.is_empty(): - is_failure = true - var report :GdUnitReport = _report_collector.pop_front(STAGE_TEST_CASE_EXECUTE) - _report_collector.add_report(STAGE_TEST_CASE_EXECUTE, GdUnitReport.new() \ - .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) - - if test_case.is_interupted(): - is_failure = true - - # call after_test for each iteration - await test_after(test_suite, test_case, test_case.get_name(), iteration==test_case.iterations()-1 or is_failure) - - if test_case.is_interupted() or is_failure: - break - test_case.dispose() - - -func execute_test_case_parameterized(test_suite :GdUnitTestSuite, test_case :_TestCase): - var testcase_timer = LocalTime.now() - fire_event(GdUnitEvent.new()\ - .test_before(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name())) - - var current_error_count = _total_test_errors - var current_failed_count = _total_test_failed - var current_warning_count =_total_test_warnings - var test_case_parameters := test_case.test_parameters() - var test_parameter_index := test_case.test_parameter_index() - var test_case_names := test_case.test_case_names() - for test_case_index in test_case.test_parameters().size(): - # is test_parameter_index is set, we run this parameterized test only - if test_parameter_index != -1 and test_parameter_index != test_case_index: - continue - await test_before(test_suite, test_case_names[test_case_index]) - set_stage(STAGE_TEST_CASE_EXECUTE) - _memory_pool.set_pool(test_suite, GdUnitMemoryPool.POOL.EXECUTE, true) - await test_case.execute(test_case_parameters[test_case_index]) - await test_after(test_suite, test_case, test_case_names[test_case_index]) - if test_case.is_interupted(): - break - test_case.dispose() - - var statistics = { - GdUnitEvent.ORPHAN_NODES: _total_test_execution_orphans, - GdUnitEvent.ELAPSED_TIME: testcase_timer.elapsed_since_ms(), - GdUnitEvent.WARNINGS: current_warning_count != _total_test_warnings, - GdUnitEvent.ERRORS: current_error_count != _total_test_errors, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: current_failed_count != _total_test_failed, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED: test_case.is_skipped(), - GdUnitEvent.SKIPPED_COUNT: int(test_case.is_skipped()), - } - fire_event(GdUnitEvent.new()\ - .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_case.get_name(), statistics, [])) - - -func execute(test_suite :GdUnitTestSuite): - await Execute(test_suite) - - -func Execute(test_suite :GdUnitTestSuite) -> void: - var context := GdUnitThreadManager.get_current_context() - context.init() - - # stop checked first error if fail fast enabled - if _fail_fast and _total_test_failed > 0: - test_suite.free() - await get_tree().process_frame - ExecutionCompleted.emit() - return - var ts := test_suite - if not ts.__is_skipped and ts.get_child_count() != 0: - await suite_before(ts) - - for test_case_index in ts.get_child_count(): - var test_case := ts.get_child(test_case_index) as _TestCase - # only iterate over test case, we need to filter because of possible adding other child types checked before() or before_test() - if not test_case is _TestCase: - continue - # stop checked first error if fail fast enabled - if _fail_fast and _total_test_failed > 0: - break - ts.set_active_test_case(test_case.get_name()) - if test_case.is_skipped(): - fire_test_skipped(ts, test_case) - await get_tree().process_frame - else: - if test_case.is_parameterized(): - await execute_test_case_parameterized(ts, test_case) - elif test_case.has_fuzzer(): - await execute_test_case_iterative(ts, test_case) - else: - await execute_test_case_single(ts, test_case) - if test_case.is_interupted(): - # it needs to go this hard way to kill the outstanding yields of a test case when the test timed out - # we delete the current test suite where is execute the current test case to kill the function state - # and replace it by a clone without function state - ts = await clone_test_suite(ts) - - await suite_after(ts) - else: - fire_test_suite_skipped(ts) - # needs at least one yielding otherwise the waiting function is blocked - await get_tree().process_frame - ts.free() - context.clear() - ExecutionCompleted.emit() - - -func copy_properties(source :Object, target :Object): - if not source is _TestCase and not source is GdUnitTestSuite: - return - for property in source.get_property_list(): - var property_name = property["name"] - target.set(property_name, source.get(property_name)) - - -# clones a test suite and moves the test cases to new instance -func clone_test_suite(test_suite :GdUnitTestSuite) -> GdUnitTestSuite: - dispose_timers(test_suite) - var parent := test_suite.get_parent() - var _test_suite = test_suite.duplicate() - copy_properties(test_suite, _test_suite) - for child in test_suite.get_children(): - copy_properties(child, _test_suite.find_child(child.get_name(), true, false)) - # finally free current test suite instance - parent.remove_child(test_suite) - await get_tree().process_frame - test_suite.free() - parent.add_child(_test_suite) - return _test_suite - - -func dispose_timers(test_suite :GdUnitTestSuite): - GdUnitTools.release_timers() - for child in test_suite.get_children(): - if child is Timer: - child.stop() - test_suite.remove_child(child) - child.free() - - -func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: - if not test_case.has_fuzzer(): - return Array() - 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()) - fuzzer._iteration_index = 0 - fuzzer._iteration_limit = test_case.iterations() - fuzzers.append(fuzzer) - return fuzzers diff --git a/addons/gdUnit4/src/core/GdUnitMemoryPool.gd b/addons/gdUnit4/src/core/GdUnitMemoryPool.gd deleted file mode 100644 index f568c52c..00000000 --- a/addons/gdUnit4/src/core/GdUnitMemoryPool.gd +++ /dev/null @@ -1,135 +0,0 @@ -extends GdUnitSingleton - -const META_PARAM := "MEMORY_POOL" - - -enum POOL { - TESTSUITE, - TESTCASE, - EXECUTE, - UNIT_TEST_ONLY, - ALL, -} - - -var _monitors := { - POOL.TESTSUITE : GdUnitMemMonitor.new("TESTSUITE"), - POOL.TESTCASE : GdUnitMemMonitor.new("TESTCASE"), - POOL.EXECUTE : GdUnitMemMonitor.new("EXECUTE"), - POOL.UNIT_TEST_ONLY : GdUnitMemMonitor.new("UNIT_TEST_ONLY"), -} - - -class MemoryStore extends RefCounted: - var _store :Array[Variant] = [] - - - func _notification(what): - if what == NOTIFICATION_PREDELETE: - while not _store.is_empty(): - var value :Variant = _store.pop_front() - GdUnitTools.free_instance(value) - - - static func pool(p_pool :POOL) -> MemoryStore: - var pool_name :String = POOL.keys()[p_pool] - return GdUnitSingleton.instance(pool_name, func(): return MemoryStore.new()) - - - static func append(p_pool :POOL, value :Variant) -> void: - pool(p_pool)._store.append(value) - - - static func contains(p_pool :POOL, value :Variant) -> bool: - return pool(p_pool)._store.has(value) - - - static func push_front(p_pool :POOL, value :Variant) -> void: - pool(p_pool)._store.push_front(value) - - - static func release_objects(p_pool :POOL) -> void: - var store := pool(p_pool)._store - while not store.is_empty(): - var value :Variant = store.pop_front() - GdUnitTools.free_instance(value) - - -var _current :POOL -var _orphan_detection_enabled :bool = true - - -func _init(): - configure(GdUnitSettings.is_verbose_orphans()) - - -func configure(orphan_detection :bool) -> void: - _orphan_detection_enabled = orphan_detection - if not _orphan_detection_enabled: - prints("!!! Reporting orphan nodes is disabled. Please check GdUnit settings.") - - -func set_pool(obj :Object, pool_id :POOL, reset_monitor: bool = false) -> void: - _current = pool_id - obj.set_meta(META_PARAM, pool_id) - var monitor := get_monitor(pool_id) - if reset_monitor: - monitor.reset() - monitor.start() - - -func monitor_stop() -> void: - var monitor := get_monitor(_current) - monitor.stop() - - -func free_pool() -> void: - @warning_ignore("static_called_on_instance") - run_auto_free(_current) - - -func get_monitor(pool_id :POOL) -> GdUnitMemMonitor: - return _monitors.get(pool_id) - - -func orphan_nodes() -> int: - if _orphan_detection_enabled: - return _monitors.get(_current).orphan_nodes() - return 0 - - -# register an instance to be freed when a test suite is finished -static func register_auto_free(obj, pool :POOL) -> Variant: - # do not register on GDScriptNativeClass - if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : - return obj - if obj is GDScript or obj is ScriptExtension: - return obj - if obj is MainLoop: - push_error("avoid to add mainloop to auto_free queue %s" % obj) - return - # only register pure objects - if obj is GdUnitSceneRunner: - MemoryStore.push_front(pool, obj) - else: - MemoryStore.append(pool, obj) - return obj - - -# runs over all registered objects and frees it -static func run_auto_free(pool :POOL) -> void: - MemoryStore.release_objects(pool) - - -# tests if given object is registered for auto freeing -static func is_auto_free_registered(obj, pool :POOL = POOL.ALL) -> bool: - # only register real object values - if not is_instance_valid(obj): - return false - # check all pools? - if pool == POOL.ALL: - return is_auto_free_registered(obj, POOL.TESTSUITE)\ - or is_auto_free_registered(obj, POOL.TESTCASE)\ - or is_auto_free_registered(obj, POOL.EXECUTE) - # check checked a specific pool - return MemoryStore.contains(pool, obj) diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd index 566809ad..5d455abb 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd @@ -1,5 +1,3 @@ -class_name GdUnitObjectInteractionsTemplate - const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") var __expected_interactions :int = -1 diff --git a/addons/gdUnit4/src/core/Result.gd b/addons/gdUnit4/src/core/GdUnitResult.gd similarity index 72% rename from addons/gdUnit4/src/core/Result.gd rename to addons/gdUnit4/src/core/GdUnitResult.gd index 6b709537..cda3d51a 100644 --- a/addons/gdUnit4/src/core/Result.gd +++ b/addons/gdUnit4/src/core/GdUnitResult.gd @@ -1,4 +1,4 @@ -class_name Result +class_name GdUnitResult extends RefCounted enum { @@ -14,32 +14,32 @@ var _error_message := "" var _value :Variant = null -static func empty() -> Result: - var result := Result.new() +static func empty() -> GdUnitResult: + var result := GdUnitResult.new() result._state = EMPTY return result -static func success(p_value :Variant) -> Result: +static func success(p_value :Variant) -> GdUnitResult: assert(p_value != null, "The value must not be NULL") - var result := Result.new() + var result := GdUnitResult.new() result._value = p_value result._state = SUCCESS return result -static func warn(p_warn_message :String, p_value :Variant = null) -> Result: +static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResult: assert(not p_warn_message.is_empty()) #,"The message must not be empty") - var result := Result.new() + var result := GdUnitResult.new() result._value = p_value result._warn_message = p_warn_message result._state = WARN return result -static func error(p_error_message :String) -> Result: +static func error(p_error_message :String) -> GdUnitResult: assert(not p_error_message.is_empty(), "The message must not be empty") - var result := Result.new() + var result := GdUnitResult.new() result._value = null result._error_message = p_error_message result._state = ERROR @@ -81,12 +81,12 @@ func warn_message() -> String: func _to_string() -> String: - return str(Result.serialize(self)) + return str(GdUnitResult.serialize(self)) -static func serialize(result :Result) -> Dictionary: +static func serialize(result :GdUnitResult) -> Dictionary: if result == null: - push_error("Can't serialize a Null object from type Result") + push_error("Can't serialize a Null object from type GdUnitResult") return { "state" : result._state, "value" : var_to_str(result._value), @@ -95,8 +95,8 @@ static func serialize(result :Result) -> Dictionary: } -static func deserialize(config :Dictionary) -> Result: - var result := Result.new() +static func deserialize(config :Dictionary) -> GdUnitResult: + var result := GdUnitResult.new() result._value = str_to_var(config.get("value", "")) result._warn_message = config.get("warn_msg", null) result._error_message = config.get("err_msg", null) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd index 0e61f7ea..c383fc3f 100644 --- a/addons/gdUnit4/src/core/GdUnitRunner.gd +++ b/addons/gdUnit4/src/core/GdUnitRunner.gd @@ -2,10 +2,9 @@ extends Node signal sync_rpc_id_result_received -const GdUnitExecutor = preload("res://addons/gdUnit4/src/core/GdUnitExecutor.gd") @onready var _client :GdUnitTcpClient = $GdUnitTcpClient -@onready var _executor :GdUnitExecutor = $GdUnitExecutor +@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new() enum { INIT, @@ -25,13 +24,13 @@ var _cs_executor func _init(): # minimize scene window checked debug mode if OS.get_cmdline_args().size() == 1: - DisplayServer.window_set_title("GdUnit3 Runner (Debug Mode)") + DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") else: - DisplayServer.window_set_title("GdUnit3 Runner (Release Mode)") + DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) # store current runner instance to engine meta data to can be access in as a singleton Engine.set_meta(GDUNIT_RUNNER, self) - _cs_executor = GdUnit3MonoAPI.create_executor(self) + _cs_executor = GdUnit4MonoApiLoader.create_executor(self) func _ready(): @@ -79,10 +78,11 @@ func _process(_delta): # process next test suite set_process(false) var test_suite :Node = _test_suites_to_process.pop_front() - add_child(test_suite) - var executor = _cs_executor if GdObjects.is_cs_test_suite(test_suite) else _executor - executor.Execute(test_suite) - await executor.ExecutionCompleted + if _cs_executor != null and _cs_executor.IsExecutable(test_suite): + _cs_executor.Execute(test_suite) + await _cs_executor.ExecutionCompleted + else: + await _executor.execute(test_suite) set_process(true) STOP: _state = EXIT @@ -162,6 +162,7 @@ func _on_gdunit_event(event :GdUnitEvent): _client.rpc_send(RPCGdUnitEvent.of(event)) -func PublishEvent(data) -> void: - var event := GdUnitEvent.new().deserialize(data.AsDictionary()) +# Event bridge from C# GdUnit4.ITestEventListener.cs +func PublishEvent(data :Dictionary) -> void: + var event := GdUnitEvent.new().deserialize(data) _client.rpc_send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.tscn b/addons/gdUnit4/src/core/GdUnitRunner.tscn index 99586a51..c1f67b15 100644 --- a/addons/gdUnit4/src/core/GdUnitRunner.tscn +++ b/addons/gdUnit4/src/core/GdUnitRunner.tscn @@ -1,14 +1,10 @@ -[gd_scene load_steps=4 format=3 uid="uid://belidlfknh74r"] +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] [ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitRunner.gd" id="1"] [ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] -[ext_resource type="Script" path="res://addons/gdUnit4/src/core/GdUnitExecutor.gd" id="3"] [node name="Control" type="Node"] script = ExtResource("1") -[node name="GdUnitExecutor" type="Node" parent="."] -script = ExtResource("3") - [node name="GdUnitTcpClient" type="Node" parent="."] script = ExtResource("2") diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd index 70fc5bf1..f9816cc4 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -104,35 +104,35 @@ func skipped() -> Dictionary: return _config.get(SKIPPED, PackedStringArray()) -func save_config(path :String = CONFIG_FILE) -> Result: +func save_config(path :String = CONFIG_FILE) -> GdUnitResult: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: var error = FileAccess.get_open_error() - return Result.error("Can't write test runner configuration '%s'! %s" % [path, GdUnitTools.error_as_string(error)]) + return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, GdUnitTools.error_as_string(error)]) _config[VERSION] = CONFIG_VERSION file.store_string(JSON.stringify(_config)) - return Result.success(path) + return GdUnitResult.success(path) -func load_config(path :String = CONFIG_FILE) -> Result: +func load_config(path :String = CONFIG_FILE) -> GdUnitResult: if not FileAccess.file_exists(path): - return Result.error("Can't find test runner configuration '%s'! Please select a test to run." % path) + return GdUnitResult.error("Can't find test runner configuration '%s'! Please select a test to run." % path) var file := FileAccess.open(path, FileAccess.READ) if file == null: var error = FileAccess.get_open_error() - return Result.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, GdUnitTools.error_as_string(error)]) + return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, GdUnitTools.error_as_string(error)]) var content := file.get_as_text() if not content.is_empty() and content[0] == '{': # Parse as json var test_json_conv := JSON.new() var error := test_json_conv.parse(content) if error != OK: - return Result.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) _config = test_json_conv.get_data() as Dictionary if not _config.has(VERSION): - return Result.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + 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 Result.success(path) + return GdUnitResult.success(path) func fix_value_types(): diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index 0f4f1d0b..ed6dffaa 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -3,7 +3,6 @@ class_name GdUnitSceneRunnerImpl extends GdUnitSceneRunner -var GdUnitMemoryPool := ResourceLoader.load("res://addons/gdUnit4/src/core/GdUnitMemoryPool.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) @@ -21,6 +20,7 @@ const MAP_MOUSE_BUTTON_MASKS := { var _scene_tree :SceneTree = 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 @@ -78,7 +78,7 @@ func _notification(what): if is_instance_valid(_current_scene): _scene_tree.root.remove_child(_current_scene) # don't free already memory managed instances - if not GdUnitMemoryPool.is_auto_free_registered(_current_scene): + if not GdUnitMemoryObserver.is_marked_auto_free(_current_scene): _current_scene.free() _scene_tree = null _current_scene = null @@ -213,13 +213,13 @@ func simulate_frames(frames: int, delta_milli :int = -1) -> GdUnitSceneRunner: func simulate_until_signal(signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) - await GdUnitAwaiter.await_signal_idle_frames(_current_scene, signal_name, args, 10000) + await _awaiter.await_signal_idle_frames(_current_scene, signal_name, args, 10000) return self func simulate_until_object_signal(source :Object, signal_name :String, arg0=NO_ARG, arg1=NO_ARG, arg2=NO_ARG, arg3=NO_ARG, arg4=NO_ARG, arg5=NO_ARG, arg6=NO_ARG, arg7=NO_ARG, arg8=NO_ARG, arg9=NO_ARG) -> GdUnitSceneRunner: var args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) - await GdUnitAwaiter.await_signal_idle_frames(source, signal_name, args, 10000) + await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) return self @@ -232,11 +232,11 @@ func await_func_on(instance :Object, func_name :String, args := []) -> GdUnitFun func await_signal(signal_name :String, args := [], timeout := 2000 ): - await GdUnitAwaiter.await_signal_on(_current_scene, signal_name, args, timeout) + await _awaiter.await_signal_on(_current_scene, signal_name, args, timeout) func await_signal_on(source :Object, signal_name :String, args := [], timeout := 2000 ): - await GdUnitAwaiter.await_signal_on(source, signal_name, args, timeout) + await _awaiter.await_signal_on(source, signal_name, args, timeout) # maximizes the window to bring the scene visible diff --git a/addons/gdUnit4/src/core/GdUnitScriptType.gd b/addons/gdUnit4/src/core/GdUnitScriptType.gd index 8a691016..7e1be519 100644 --- a/addons/gdUnit4/src/core/GdUnitScriptType.gd +++ b/addons/gdUnit4/src/core/GdUnitScriptType.gd @@ -4,18 +4,13 @@ extends RefCounted const UNKNOWN := "" const CS := "cs" const GD := "gd" -const NATIVE := "gdns" -const VS := "vs" + static func type_of(script :Script) -> String: if script == null: return UNKNOWN if GdObjects.is_gd_script(script): return GD - if GdObjects.is_vs_script(script): - return VS - if GdObjects.is_native_script(script): - return NATIVE if GdObjects.is_cs_script(script): return CS return UNKNOWN diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd index 598f0725..d9466a12 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -12,7 +12,7 @@ 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_ROOT_FOLDER = GROUP_TEST + "/test_root_folder" +const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" const TEST_SITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention" @@ -74,7 +74,10 @@ const DEFAULT_SERVER_TIMEOUT :int = 30 # test case runtime timeout in seconds const DEFAULT_TEST_TIMEOUT :int = 60*5 # the folder to create new test-suites -const DEFAULT_TEST_ROOT_FOLDER := "test" +const DEFAULT_TEST_LOOKUP_FOLDER := "test" + +# help texts +const HELP_TEST_LOOKUP_FOLDER := "Sets the subfolder for the search/creation of test suites. (leave empty to use source folder)" enum NAMING_CONVENTIONS { AUTO_DETECT, @@ -87,7 +90,7 @@ static func setup(): create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Enables/Disables the update notification checked startup.") create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Sets the server connection timeout in minutes.") create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Sets the test case runtime timeout in seconds.") - create_property_if_need(TEST_ROOT_FOLDER, DEFAULT_TEST_ROOT_FOLDER, "Sets the root folder where test-suites located/generated.") + 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(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!") @@ -99,6 +102,17 @@ static func setup(): create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false, "Shows/Hides the 'Run overall Tests' button in the inspector toolbar.") create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Defines the test suite template") create_shortcut_properties_if_need() + migrate_properties() + + +static func migrate_properties() -> void: + var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder" + if get_property(TEST_ROOT_FOLDER) != null: + migrate_property(TEST_ROOT_FOLDER,\ + TEST_LOOKUP_FOLDER,\ + DEFAULT_TEST_LOOKUP_FOLDER,\ + HELP_TEST_LOOKUP_FOLDER,\ + func(value): return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value) static func create_shortcut_properties_if_need() -> void: @@ -118,18 +132,21 @@ static func create_shortcut_properties_if_need() -> void: static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void: if not ProjectSettings.has_setting(name): - #prints("GdUnit3: Set inital settings '%s' to '%s'." % [name, str(default)]) + #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)]) ProjectSettings.set_setting(name, default) - + ProjectSettings.set_initial_value(name, default) - var hint_string := help + ("" if value_set.is_empty() else " %s" % value_set) - var info = { - "name": name, - "type": typeof(default), - "hint": PROPERTY_HINT_TYPE_STRING, - "hint_string": hint_string - } - ProjectSettings.add_property_info(info) + help += "" if value_set.is_empty() else " %s" % value_set + set_help(name, default, help) + + +static func set_help(property_name :String, value :Variant, help :String) -> void: + ProjectSettings.add_property_info({ + "name": property_name, + "type": typeof(value), + "hint": PROPERTY_HINT_TYPE_STRING, + "hint_string": help + }) static func get_setting(name :String, default :Variant) -> Variant: @@ -171,7 +188,7 @@ static func test_timeout() -> int: # the root folder to store/generate test-suites static func test_root_folder() -> String: - return get_setting(TEST_ROOT_FOLDER, DEFAULT_TEST_ROOT_FOLDER) + return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER) static func is_verbose_assert_warnings() -> bool: @@ -233,31 +250,57 @@ static func extract_value_set_from_help(value :String) -> PackedStringArray: return values.replacen(" ", "").replacen("\"", "").split(",", false) -static func update_property(property :GdUnitProperty) -> void: - if get_property(property.name()).value() != property.value(): +static func update_property(property :GdUnitProperty) -> Variant: + var current_value :Variant = ProjectSettings.get_setting(property.name()) + if current_value != property.value(): + var error :Variant = validate_property_value(property) + if error != null: + return error ProjectSettings.set_setting(property.name(), property.value()) GdUnitSignals.instance().gdunit_settings_changed.emit(property) - save() + _save_settings() + return null static func reset_property(property :GdUnitProperty) -> void: ProjectSettings.set_setting(property.name(), property.default()) GdUnitSignals.instance().gdunit_settings_changed.emit(property) - save() + _save_settings() + + +static func validate_property_value(property :GdUnitProperty) -> Variant: + match property.name(): + TEST_LOOKUP_FOLDER: + return validate_lookup_folder(property.value()) + _: return null + + +static func validate_lookup_folder(value :String) -> Variant: + if value.is_empty() or value == "/": + return null + if value.contains("res:"): + return "Test Lookup Folder: do not allowed to contains 'res://'" + if not value.is_valid_filename(): + return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)" + return null static func save_property(name :String, value) -> void: ProjectSettings.set_setting(name, value) - save() + _save_settings() -static func save() -> void: - var err := ProjectSettings.save() +static func _save_settings() -> void: + var err = ProjectSettings.save() if err != OK: - push_error("Save GdUnit3 settings failed : %s" % error_string(err)) + push_error("Save GdUnit4 settings failed : %s" % error_string(err)) return +static func has_property(name :String) -> bool: + return ProjectSettings.get_property_list().any( func(property): return property["name"] == name) + + static func get_property(name :String) -> GdUnitProperty: for property in ProjectSettings.get_property_list(): var property_name = property["name"] @@ -270,14 +313,15 @@ static func get_property(name :String) -> GdUnitProperty: return null -static func migrate_property(old_property :String, new_property :String, converter := Callable()) -> void: +static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void: var property := get_property(old_property) if property == null: - prints("Migration not possible, property '%s' not found", old_property) + prints("Migration not possible, property '%s' not found" % old_property) return var value = converter.call(property.value()) if converter.is_valid() else property.value() - create_property_if_need(new_property, property.default(), property.help(), property.value_set()) ProjectSettings.set_setting(new_property, value) + ProjectSettings.set_initial_value(new_property, default_value) + set_help(new_property, value, help) ProjectSettings.clear(old_property) prints("Succesfull migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value]) diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd index 9dccdf9c..faf089f4 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -9,7 +9,7 @@ signal gdunit_event(event :GdUnitEvent) signal gdunit_event_debug(event :GdUnitEvent) signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) signal gdunit_message(message :String) -signal gdunit_report(report :GdUnitReport) +signal gdunit_report(execution_context_id :int, report :GdUnitReport) signal gdunit_set_test_failed(is_failed :bool) signal gdunit_settings_changed(property :GdUnitProperty) diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd index 8b755d60..4fb05619 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -31,10 +31,10 @@ static func unregister(p_singleton :String) -> void: var index := singletons.find(p_singleton) singletons.remove_at(index) var instance_ :Variant = Engine.get_meta(p_singleton) - GdUnitTools.prints_verbose(" Free singeleton instance '%s:%s'" % [p_singleton, instance_]) + GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) GdUnitTools.free_instance(instance_) Engine.remove_meta(p_singleton) - GdUnitTools.prints_verbose(" Succesfully freed '%s'" % p_singleton) + GdUnitTools.prints_verbose(" Successfully freed '%s'" % p_singleton) Engine.set_meta(MEATA_KEY, singletons) diff --git a/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd b/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd deleted file mode 100644 index 1fa3606e..00000000 --- a/addons/gdUnit4/src/core/GdUnitStaticDictionary.gd +++ /dev/null @@ -1,66 +0,0 @@ -# implements a Dictionary with static accessors -class_name GdUnitStaticDictionary -extends GdUnitSingleton - - -static func __data() -> Dictionary: - return instance("GdUnitStaticVariables", func(): return {}) - - -static func add_value(key : Variant, value : Variant, overwrite := false) -> Variant: - var data :Dictionary = __data() - if overwrite and data.has(key): - push_error("An value already exists with key: %s" % key) - return null - data[key] = value - #Engine.set_meta("GdUnitStaticVariables", data) - return value - - -static func erase(key: Variant) -> bool: - var data :Dictionary = __data() - if data.has(key): - data.erase(key) - #Engine.set_meta("GdUnitStaticVariables", data) - return true - return false - - -static func clear() -> void: - Engine.set_meta("GdUnitStaticVariables", {}) - - -func find_key(value: Variant) -> Variant: - return GdUnitStaticDictionary.__data().find_key(value) - - -static func get_value(key: Variant, default: Variant = null) -> Variant: - return GdUnitStaticDictionary.__data().get(key, default) - - -static func has_key(key: Variant) -> bool: - return __data().has(key) - - -static func has_keys(keys_: Array) -> bool: - return __data().has_all(keys_) - - -static func is_empty() -> bool: - return __data().is_empty() - - -static func keys() -> Array: - return __data().keys() - - -static func size() -> int: - return __data().size() - - -static func values() -> Array: - return __data().values() - - -func _to_string() -> String: - return str(GdUnitStaticDictionary.__data().keys()) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd index 0fe3677f..7cd5dfdd 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -2,19 +2,17 @@ class_name GdUnitTestSuiteBuilder extends RefCounted -static func create(source :Script, line_number :int) -> Result: +static func create(source :Script, line_number :int) -> GdUnitResult: var test_suite_path := GdUnitTestSuiteScanner.resolve_test_suite_path(source.resource_path, GdUnitSettings.test_root_folder()) # we need to save and close the testsuite and source if is current opened before modify ScriptEditorControls.save_an_open_script(source.resource_path) - ScriptEditorControls.save_an_open_script(test_suite_path, true) - + ScriptEditorControls.save_an_open_script(test_suite_path, true) if GdObjects.is_cs_script(source): - return GdUnit3MonoAPI.create_test_suite(source.resource_path, line_number+1, test_suite_path) - + return GdUnit4MonoApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) var parser := GdScriptParser.new() var lines := source.source_code.split("\n") var current_line := lines[line_number] var func_name := parser.parse_func_name(current_line) if func_name.is_empty(): - return Result.error("No function found at line: %d." % line_number) + return GdUnitResult.error("No function found at line: %d." % line_number) return GdUnitTestSuiteScanner.create_test_case(test_suite_path, func_name, source.resource_path) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd index 652ec447..ff7ab6df 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -8,6 +8,14 @@ func test_${func_name}() -> void: assert_not_yet_implemented() """ + +# we exclude the gdunit source directorys by default +const exclude_scan_directories = [ + "res://addons/gdUnit4/bin", + "res://addons/gdUnit4/src", + "res://reports"] + + var _script_parser := GdScriptParser.new() var _extends_test_suite_classes := Array() var _expression_runner := GdUnitExpressionRunner.new() @@ -39,6 +47,8 @@ func scan(resource_path :String) -> Array[Node]: func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[Node]: + if exclude_scan_directories.has(dir.get_current_dir()): + return collected_suites prints("Scanning for test suites in:", dir.get_current_dir()) dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 var file_name := dir.get_next() @@ -53,7 +63,7 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N var test_suite := _parse_is_test_suite(resource_path) if test_suite: collected_suites.append(test_suite) - if time.elapsed_since_ms() > 300: + if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since()) file_name = dir.get_next() return collected_suites @@ -69,8 +79,8 @@ static func _file(dir :DirAccess, file_name :String) -> String: func _parse_is_test_suite(resource_path :String) -> Node: if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path): return null - if GdUnit3MonoAPI.is_test_suite(resource_path): - return GdUnit3MonoAPI.parse_test_suite(resource_path) + if GdUnit4MonoApiLoader.is_test_suite(resource_path): + return GdUnit4MonoApiLoader.parse_test_suite(resource_path) var script :Script = ResourceLoader.load(resource_path) if not GdObjects.is_test_suite(script): return null @@ -83,7 +93,7 @@ static func _is_script_format_supported(resource_path :String) -> bool: var ext := resource_path.get_extension() if ext == "gd": return true - return GdUnit3MonoAPI.is_csharp_file(resource_path) + return GdUnit4MonoApiLoader.is_csharp_file(resource_path) func _parse_test_suite(script :GDScript) -> GdUnitTestSuite: @@ -220,12 +230,12 @@ static func _to_naming_convention(file_name :String) -> String: static func resolve_test_suite_path(source_script_path :String, test_root_folder :String = "test") -> String: var file_name = source_script_path.get_basename().get_file() var suite_name := _to_naming_convention(file_name) - if test_root_folder.is_empty(): + if test_root_folder.is_empty() or test_root_folder == "/": return source_script_path.replace(file_name, suite_name) # is user tmp if source_script_path.begins_with("user://tmp"): - return source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder).replace(file_name, suite_name) + return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name) # at first look up is the script under a "src" folder located var test_suite_path :String @@ -244,21 +254,25 @@ static func resolve_test_suite_path(source_script_path :String, test_root_folder test_suite_path = paths[0] + "//" + test_root_folder for index in range(1, paths.size()): test_suite_path += "/" + paths[index] - return test_suite_path.replace(file_name, suite_name) + return normalize_path(test_suite_path).replace(file_name, suite_name) + + +static func normalize_path(path :String) -> String: + return path.replace("///", "/") -static func create_test_suite(test_suite_path :String, source_path :String) -> Result: +static func create_test_suite(test_suite_path :String, source_path :String) -> GdUnitResult: # create directory if not exists if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()): var error := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir()) if error != OK: - return Result.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error]) + return GdUnitResult.error("Can't create directoy at: %s. Error code %s" % [test_suite_path.get_base_dir(), error]) var script := GDScript.new() script.source_code = GdUnitTestSuiteTemplate.build_template(source_path) var error := ResourceSaver.save(script, test_suite_path) if error != OK: - return Result.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) - return Result.success(test_suite_path) + return GdUnitResult.error("Can't create test suite at: %s. Error code %s" % [test_suite_path, error]) + return GdUnitResult.success(test_suite_path) static func get_test_case_line_number(resource_path :String, func_name :String) -> int: @@ -278,7 +292,7 @@ static func get_test_case_line_number(resource_path :String, func_name :String) return -1 -static func add_test_case(resource_path :String, func_name :String) -> Result: +static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult: var script := load(resource_path) as GDScript # count all exiting lines and add two as space to add new test case var line_number := count_lines(script) + 2 @@ -293,8 +307,8 @@ static func add_test_case(resource_path :String, func_name :String) -> Result: script.source_code += func_body var error := ResourceSaver.save(script, resource_path) if error != OK: - return Result.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) - return Result.success({ "path" : resource_path, "line" : line_number}) + return GdUnitResult.error("Can't add test case at: %s to '%s'. Error code %s" % [func_name, resource_path, error]) + return GdUnitResult.success({ "path" : resource_path, "line" : line_number}) static func count_lines(script : GDScript) -> int: @@ -313,10 +327,10 @@ static func test_case_exists(test_suite_path :String, func_name :String) -> bool return true return false -static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> Result: +static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> GdUnitResult: if test_case_exists(test_suite_path, func_name): var line_number := get_test_case_line_number(test_suite_path, func_name) - return Result.success({ "path" : test_suite_path, "line" : line_number}) + return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number}) if not test_suite_exists(test_suite_path): var result := create_test_suite(test_suite_path, source_script_path) diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index a55de319..1e4f660a 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -56,16 +56,16 @@ static func delete_directory(path :String, only_content := false) -> void: push_error("Delete %s failed: %s" % [path, error_as_string(err)]) -static func copy_file(from_file :String, to_dir :String) -> Result: +static func copy_file(from_file :String, to_dir :String) -> GdUnitResult: var dir := DirAccess.open(to_dir) if dir != null: var to_file := to_dir + "/" + from_file.get_file() prints("Copy %s to %s" % [from_file, to_file]) var error = dir.copy(from_file, to_file) if error != OK: - return Result.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_as_string(error)]) - return Result.success(to_file) - return Result.error("Directory not found: " + to_dir) + return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_as_string(error)]) + return GdUnitResult.success(to_file) + return GdUnitResult.error("Directory not found: " + to_dir) static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool: @@ -203,7 +203,7 @@ static func prints_verbose(message :String) -> void: prints(message) -static func free_instance(instance :Variant) -> bool: +static func free_instance(instance :Variant, is_stdout_verbose :=false) -> bool: if instance is Array: for element in instance: free_instance(element) @@ -215,20 +215,28 @@ static func free_instance(instance :Variant) -> bool: # do not free a class refernece if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): return false - if is_instance_valid(instance) and instance is RefCounted: + if is_stdout_verbose: + print_verbose("GdUnit4:gc():free instance ", instance) + release_double(instance) + if instance is RefCounted: instance.notification(Object.NOTIFICATION_PREDELETE) + await Engine.get_main_loop().process_frame return true else: - # is instance already freed? - if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): - return false - release_double(instance) + # is instance already freed? + #if not is_instance_valid(instance) or ClassDB.class_get_property(instance, "new"): + # return false #release_connections(instance) if instance is Timer: instance.stop() - #instance.queue_free() instance.call_deferred("free") + await Engine.get_main_loop().process_frame return true + if instance is Node and instance.get_parent() != null: + if is_stdout_verbose: + print_verbose("GdUnit4:gc():remove node from parent ", instance.get_parent(), instance) + instance.get_parent().remove_child(instance) + instance.set_owner(null) instance.free() return !is_instance_valid(instance) @@ -251,9 +259,9 @@ static func _release_connections(instance :Object): static func release_timers(): # we go the new way to hold all gdunit timers in group 'GdUnitTimers' for node in Engine.get_main_loop().root.get_children(): - if node.is_in_group("GdUnitTimers"): - #prints("found gdunit timer artifact", node, is_instance_valid(node)) + if is_instance_valid(node) and node.is_in_group("GdUnitTimers"): if is_instance_valid(node): + Engine.get_main_loop().root.remove_child(node) node.stop() node.free() @@ -271,11 +279,6 @@ static func release_double(instance :Object) -> void: instance.call("__release_double") -# test is Godot mono running -static func is_mono_supported() -> bool: - return ClassDB.class_exists("CSharpScript") - - static func make_qualified_path(path :String) -> String: if not path.begins_with("res://"): if path.begins_with("//"): @@ -300,11 +303,11 @@ static func register_expect_interupted_by_timeout(test_suite :Node, test_case_na test_case.expect_to_interupt() -static func extract_zip(zip_package :String, dest_path :String) -> Result: +static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult: var zip: ZIPReader = ZIPReader.new() var err := zip.open(zip_package) if err != OK: - return Result.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) + return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err]) var zip_entries: PackedStringArray = zip.get_files() # Get base path and step over archive folder var archive_path = zip_entries[0] @@ -318,4 +321,4 @@ static func extract_zip(zip_package :String, dest_path :String) -> Result: var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE) file.store_buffer(zip.read_file(zip_entry)) zip.close() - return Result.success(dest_path) + return GdUnitResult.success(dest_path) diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd new file mode 100644 index 00000000..d0ce4e14 --- /dev/null +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -0,0 +1,11 @@ +## This service class contains helpers to wrap Godot functions and handle them carefully depending on the current Godot version +class_name GodotVersionFixures +extends RefCounted + + + +## Returns the icon property defined by name and theme_type, if it exists. +static func get_icon(control :Control, icon_name :String) -> Texture2D: + if Engine.get_version_info().hex >= 040200: + return control.get_theme_icon(icon_name, "EditorIcons") + return control.theme.get_icon(icon_name, "EditorIcons") diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd index 376679ac..47ea60ff 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -23,7 +23,6 @@ var _expect_to_interupt := false var _timer : Timer var _interupted :bool = false var _failed := false -var _timeout :int var _report :GdUnitReport = null @@ -36,22 +35,31 @@ var monitor : GodotGdErrorMonitor = null: return monitor +var timeout : int = DEFAULT_TIMEOUT: + set (value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + timeout = GdUnitSettings.test_timeout() + return timeout + + @warning_ignore("shadowed_variable_base_class") -func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout :int = DEFAULT_TIMEOUT, p_fuzzers :Array = [], p_iterations: int = 1, p_seed :int = -1) -> _TestCase: +func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout :int = DEFAULT_TIMEOUT, p_fuzzers :Array[GdFunctionArgument] = [], p_iterations: int = 1, p_seed :int = -1) -> _TestCase: set_name(p_name) _line_number = p_line_number _fuzzers = p_fuzzers _iterations = p_iterations _seed = p_seed _script_path = p_script_path - _timeout = p_timeout if p_timeout != DEFAULT_TIMEOUT else GdUnitSettings.test_timeout() + timeout = p_timeout return self func execute(p_test_parameter := Array(), p_iteration := 0): _failure_received(false) _current_iteration = p_iteration - 1 - if p_iteration == 0: + if _current_iteration == -1: _set_failure_handler() set_timeout() monitor.start() @@ -68,19 +76,37 @@ func execute(p_test_parameter := Array(), p_iteration := 0): _interupted = true +func execute_paramaterized(p_test_parameter :Array): + _failure_received(false) + set_timeout() + monitor.start() + _execute_test_case(name, p_test_parameter) + await completed + monitor.stop() + for report_ in monitor.reports(): + if report_.is_error(): + _report = report_ + _interupted = true + + +var _is_disposed := false + func dispose(): - # unreference last used assert form the test to prevent memory leaks - GdUnitThreadManager.get_current_context().set_assert(null) + if _is_disposed: + return + _is_disposed = true + Engine.remove_meta("GD_TEST_FAILURE") stop_timer() _remove_failure_handler() _fuzzers.clear() + _report = null @warning_ignore("shadowed_variable_base_class", "redundant_await") func _execute_test_case(name :String, test_parameter :Array): - # needs at least on await otherwise it braks the awaiting chain + # needs at least on await otherwise it breaks the awaiting chain await get_parent().callv(name, test_parameter) - await get_tree().create_timer(0.0001).timeout + await Engine.get_main_loop().process_frame completed.emit() @@ -91,18 +117,20 @@ func update_fuzzers(input_values :Array, iteration :int): func set_timeout(): - var time :float = _timeout * 0.001 + if is_instance_valid(_timer): + return + var time :float = timeout / 1000.0 _timer = Timer.new() add_child(_timer) _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) _timer.timeout.connect(func do_interrupt(): - if has_fuzzer(): + 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())) + _report = GdUnitReport.new().create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout)) _interupted = true completed.emit() - , CONNECT_REFERENCE_COUNTED) + , CONNECT_DEFERRED) _timer.set_one_shot(true) _timer.set_wait_time(time) _timer.set_autostart(false) @@ -132,6 +160,7 @@ func stop_timer() : if is_instance_valid(_timer): _timer.stop() _timer.call_deferred("free") + _timer = null func expect_to_interupt() -> void: @@ -170,15 +199,11 @@ func iterations() -> int: return _iterations -func timeout() -> int: - return _timeout - - func seed_value() -> int: return _seed -func has_fuzzer() -> bool: +func is_fuzzed() -> bool: return not _fuzzers.is_empty() @@ -224,9 +249,9 @@ func test_case_names() -> PackedStringArray: var test_cases := PackedStringArray() var test_name = get_name() for index in _test_parameters.size(): - test_cases.append("%s:%d %s" % [test_name, index, str(_test_parameters[index]).replace('"', "'")]) + test_cases.append("%s:%d %s" % [test_name, index, str(_test_parameters[index]).replace('"', "'").replace("&'", "'")]) return test_cases func _to_string(): - return "%s :%d (%dms)" % [get_name(), _line_number, _timeout] + return "%s :%d (%dms)" % [get_name(), _line_number, timeout] diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd index 1541fecb..ffe3c125 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -56,7 +56,8 @@ func _init(): assert_shortcut_mappings(SETTINGS_SHORTCUT_MAPPING) if Engine.is_editor_hint(): - _editor_interface = Engine.get_meta("GdUnitEditorPlugin").get_editor_interface() + var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = editor.get_editor_interface() GdUnitSignals.instance().gdunit_event.connect(_on_event) GdUnitSignals.instance().gdunit_client_connected.connect(_on_client_connected) GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_client_disconnected) @@ -65,8 +66,8 @@ func _init(): _runner_config.load_config() init_shortcuts() - var is_running = func(_script :GDScript) : return _is_running - var is_not_running = func(_script :GDScript) : return !_is_running + var is_running = func(_script :Script) : return _is_running + var is_not_running = func(_script :Script) : return !_is_running register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG)) @@ -188,7 +189,7 @@ func cmd_run_test_case(test_suite_resource_path :String, test_case :String, test func cmd_run_overall(debug :bool) -> void: - var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://", []) + var test_suite_paths :PackedStringArray = GdUnitCommandHandler.scan_test_directorys("res://" , GdUnitSettings.test_root_folder(), []) var result := _runner_config.clear()\ .add_test_suites(test_suite_paths)\ .save_config() @@ -261,20 +262,30 @@ func cmd_create_test() -> void: ScriptEditorControls.edit_script(info.get("path"), info.get("line")) -static func scan_test_directorys(base_directory :String, test_suite_paths :PackedStringArray) -> PackedStringArray: - prints("Scannning for test directories", base_directory) +static func scan_test_directorys(base_directory :String, test_directory: String, test_suite_paths :PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) for directory in DirAccess.get_directories_at(base_directory): if directory.begins_with("."): continue - var current_directory := base_directory + "/" + directory - if directory == "test": - prints(".. ", current_directory) + var current_directory := normalize_path(base_directory + "/" + directory) + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + prints("Collect tests at:", current_directory) test_suite_paths.append(current_directory) else: - scan_test_directorys(current_directory, test_suite_paths) + scan_test_directorys(current_directory, test_directory, test_suite_paths) return test_suite_paths +static func normalize_path(path :String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory :String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" + + func run_debug_mode(): _editor_interface.play_custom_scene("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") _is_running = true diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd index 173f64d7..dbfcc326 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -11,7 +11,7 @@ const ERROR_COUNT = "error_count" const FAILED_COUNT = "failed_count" const SKIPPED_COUNT = "skipped_count" -enum { +enum { INIT, STOP, TESTSUITE_BEFORE, @@ -139,7 +139,7 @@ func reports() -> Array: func _to_string(): - return "Event: %d %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] + return "Event: %s %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] func serialize() -> Dictionary: diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd new file mode 100644 index 00000000..b55c125f --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -0,0 +1,171 @@ +## The execution context +## 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 _test_case_name: StringName +var _name :String + + +var test_suite : GdUnitTestSuite = null: + set (value): + test_suite = value + get: + if _parent_context != null: + return _parent_context.test_suite + return test_suite + + +var test_case : _TestCase = null: + get: + if _test_case_name.is_empty(): + return null + return test_suite.find_child(_test_case_name, false, false) + + +func _init(name :String, 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() + _report_collector = GdUnitTestReportCollector.new(get_instance_id()) + if parent_context != null: + parent_context._sub_context.append(self) + + +func dispose() -> void: + _timer = null + _orphan_monitor = null + _report_collector = null + _memory_observer = null + _parent_context = null + test_suite = null + test_case = null + for context in _sub_context: + context.dispose() + _sub_context.clear() + + +func set_active() -> void: + test_suite.__execution_context = self + GdUnitThreadManager.get_current_context().set_execution_context(self) + + +static func of_test_suite(test_suite_ :GdUnitTestSuite) -> GdUnitExecutionContext: + assert(test_suite_, "test_suite is null") + var context := GdUnitExecutionContext.new(test_suite_.get_name()) + context.test_suite = test_suite_ + context.set_active() + return context + + +static func of_test_case(pe :GdUnitExecutionContext, test_case_name :StringName) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(test_case_name, pe) + context._test_case_name = test_case_name + context.set_active() + return context + + +static func of(pe :GdUnitExecutionContext) -> GdUnitExecutionContext: + var context := GdUnitExecutionContext.new(pe._test_case_name, pe) + context._test_case_name = pe._test_case_name + context.set_active() + return context + + +func test_failed() -> bool: + return has_failures() or has_errors() + + +func orphan_monitor_start() -> void: + _orphan_monitor.start() + + +func orphan_monitor_stop() -> void: + _orphan_monitor.stop() + + +func reports() -> Array[GdUnitReport]: + return _report_collector.reports() + + +func build_report_statistics(orphans :int, recursive := true) -> Dictionary: + return { + GdUnitEvent.ORPHAN_NODES: orphans, + 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) + } + + +func has_failures() -> bool: + return _sub_context.any(func(c): return c.has_failures()) or _report_collector.has_failures() + + +func has_errors() -> bool: + return _sub_context.any(func(c): return c.has_errors()) or _report_collector.has_errors() + + +func has_warnings() -> bool: + return _sub_context.any(func(c): return c.has_warnings()) or _report_collector.has_warnings() + + +func has_skipped() -> bool: + return _sub_context.any(func(c): return c.has_skipped()) or _report_collector.has_skipped() + + +func count_failures(recursive :bool) -> int: + if not recursive: + return _report_collector.count_failures() + return _sub_context\ + .map(func(c): return c.count_failures(recursive))\ + .reduce(sum, _report_collector.count_failures()) + + +func count_errors(recursive :bool) -> int: + if not recursive: + return _report_collector.count_errors() + return _sub_context\ + .map(func(c): return c.count_errors(recursive))\ + .reduce(sum, _report_collector.count_errors()) + + +func count_skipped(recursive :bool) -> int: + if not recursive: + return _report_collector.count_skipped() + return _sub_context\ + .map(func(c): return c.count_skipped(recursive))\ + .reduce(sum, _report_collector.count_skipped()) + + +func count_orphans() -> int: + var orphans := 0 + for c in _sub_context: + orphans += c._orphan_monitor.orphan_nodes() + return _orphan_monitor.orphan_nodes() - orphans + + +func sum(accum :int, number :int) -> int: + return accum + number + + +func register_auto_free(obj :Variant) -> Variant: + return _memory_observer.register_auto_free(obj) + + +## Runs the gdunit garbage collector to free registered object +func gc() -> void: + await _memory_observer.gc() + orphan_monitor_stop() diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd new file mode 100644 index 00000000..6709c136 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -0,0 +1,136 @@ +## The memory watcher for objects that have been registered and are released when 'gc' is called. +class_name GdUnitMemoryObserver +extends RefCounted + +const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_" +const TAG_AUTO_FREE = "GdUnit4_marked_auto_free" +const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +var _store :Array[Variant] = [] +var _orphan_detection_enabled :bool = true +# enable for debugging purposes +var _is_stdout_verbose := false +const _show_debug := false + + +func _init(): + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +## Registration of an instance to be released when an execution phase is completed +func register_auto_free(obj) -> Variant: + if not is_instance_valid(obj): + return obj + # do not register on GDScriptNativeClass + if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") : + return obj + #if obj is GDScript or obj is ScriptExtension: + # return obj + if obj is MainLoop: + push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj) + return + if _is_stdout_verbose: + print_verbose("GdUnit4:gc():register auto_free(%s)" % obj) + # only register pure objects + if obj is GdUnitSceneRunner: + _store.push_back(obj) + else: + _store.append(obj) + _tag_object(obj) + return obj + + +# to disable instance guard when run into issues. +static func _is_instance_guard_enabled() -> bool: + return false + + +static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: + if not _show_debug: + return + var script :GDScript= obj if obj is GDScript else obj.get_script() + if script: + var base_script :GDScript = script.get_base_script() + prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) + if base_script: + debug_observe("+", base_script, indent+1) + else: + prints(name, obj, obj.get_class(), obj.get_name()) + + +static func guard_instance(obj :Object) -> Object: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if Engine.has_meta(tag): + return + debug_observe("Gard on instance", obj) + Engine.set_meta(tag, obj) + return obj + + +static func unguard_instance(obj :Object, verbose := true) -> void: + if not _is_instance_guard_enabled(): + return + var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id())) + if verbose: + debug_observe("unguard instance", obj) + if Engine.has_meta(tag): + Engine.remove_meta(tag) + + +static func gc_guarded_instance(name :String, instance :Object) -> void: + if not _is_instance_guard_enabled(): + return + await Engine.get_main_loop().process_frame + unguard_instance(instance, false) + if is_instance_valid(instance) and instance is RefCounted: + # finally do this very hacky stuff + # we need to manually unreferece to avoid leaked scripts + # but still leaked GDScriptFunctionState exists + #var script :GDScript = instance.get_script() + #if script: + # var base_script :GDScript = script.get_base_script() + # if base_script: + # base_script.unreference() + debug_observe(name, instance) + instance.unreference() + await Engine.get_main_loop().process_frame + + +static func gc_on_guarded_instances() -> void: + if not _is_instance_guard_enabled(): + return + for tag in Engine.get_meta_list(): + if tag.begins_with(TAG_OBSERVE_INSTANCE): + var instance = Engine.get_meta(tag) + await gc_guarded_instance("Leaked instance detected:", instance) + await GdUnitTools.free_instance(instance, false) + + +# 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 + tagged_object.append(obj) + Engine.set_meta(TAG_AUTO_FREE, tagged_object) + + +## Runs over all registered objects and releases them +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 + 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 + while not _store.is_empty(): + var value :Variant = _store.pop_front() + tagged_objects.erase(value) + await GdUnitTools.free_instance(value, _is_stdout_verbose) + + +## Checks whether the specified object is registered for automatic release +static func is_marked_auto_free(obj) -> bool: + return Engine.get_meta(TAG_AUTO_FREE, []).has(obj) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd new file mode 100644 index 00000000..cde5cee2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -0,0 +1,70 @@ +# Collects all reports seperated as warnings, failures and errors +class_name GdUnitTestReportCollector +extends RefCounted + + +var _execution_context_id :int +var _reports :Array[GdUnitReport] = [] + + +static func __filter_is_error(report :GdUnitReport) -> bool: + return report.is_error() + + +static func __filter_is_failure(report :GdUnitReport) -> bool: + return report.is_failure() + + +static func __filter_is_warning(report :GdUnitReport) -> bool: + return report.is_warning() + + +static func __filter_is_skipped(report :GdUnitReport) -> bool: + return report.is_skipped() + + +func _init(execution_context_id :int): + _execution_context_id = execution_context_id + GdUnitSignals.instance().gdunit_report.connect(on_reports) + + +func count_failures() -> int: + return _reports.filter(__filter_is_failure).size() + + +func count_errors() -> int: + return _reports.filter(__filter_is_error).size() + + +func count_warnings() -> int: + return _reports.filter(__filter_is_warning).size() + + +func count_skipped() -> int: + return _reports.filter(__filter_is_skipped).size() + + +func has_failures() -> bool: + return _reports.any(__filter_is_failure) + + +func has_errors() -> bool: + return _reports.any(__filter_is_error) + + +func has_warnings() -> bool: + return _reports.any(__filter_is_warning) + + +func has_skipped() -> bool: + return _reports.any(__filter_is_skipped) + + +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) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd new file mode 100644 index 00000000..47c02fb8 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -0,0 +1,26 @@ +## The executor to run a test-suite +class_name GdUnitTestSuiteExecutor + + +# preload all asserts here +@warning_ignore("unused_private_class_variable") +var _assertions := GdUnitAssertions.new() +var _executeStage :IGdUnitExecutionStage = GdUnitTestSuiteExecutionStage.new() + + +func _init(debug_mode :bool = false): + _executeStage.set_debug_mode(debug_mode) + + +func execute(test_suite :GdUnitTestSuite) -> void: + var orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + 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 + await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) + + +func fail_fast(enabled :bool) -> void: + _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd new file mode 100644 index 00000000..d8db2c44 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -0,0 +1,101 @@ +## The test case shutdown hook implementation.[br] +## It executes the 'test_after()' block from the test-suite. +class_name GdUnitTestCaseAfterStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.after_test() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + + 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): + _test_name = test_name + + +func fire_test_ended(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_name := context._test_case_name if _test_name.is_empty() else _test_name + var reports := collect_reports(context) + var orphans := collect_orphans(context, reports) + + fire_event(GdUnitEvent.new()\ + .test_after(test_suite.get_script().resource_path, test_suite.get_name(), test_name, context.build_report_statistics(orphans), reports)) + + +func collect_orphans(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := 0 + if not context._sub_context.is_empty(): + orphans += add_orphan_report_test(context._sub_context[0], reports) + orphans += add_orphan_report_teststage(context, reports) + return orphans + + +func collect_reports(context :GdUnitExecutionContext) -> Array[GdUnitReport]: + var reports := context.reports() + var test_case := context.test_case + if test_case.is_interupted() and not test_case.is_expect_interupted(): + reports.push_back(test_case.report()) + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + if not context._sub_context.is_empty(): + reports.append_array(context._sub_context[0].reports()) + # needs finally to clean the test reports to avoid counting twice + context._sub_context[0].reports().clear() + return reports + + +func add_orphan_report_test(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) + return orphans + + +func add_orphan_report_teststage(context :GdUnitExecutionContext, reports :Array[GdUnitReport]) -> int: + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) + return orphans + + +func fire_test_skipped(context :GdUnitExecutionContext): + var test_suite := context.test_suite + var test_case := context.test_case + var test_case_name := context._test_case_name if _test_name.is_empty() else _test_name + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new().create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(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])) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd new file mode 100644 index 00000000..0abc581f --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -0,0 +1,28 @@ +## The test case startup hook implementation.[br] +## It executes the 'test_before()' block from the test-suite. +class_name GdUnitTestCaseBeforeStage +extends IGdUnitExecutionStage + + +var _test_name :StringName = "" +var _call_stage :bool + + +func _init(call_stage := true): + _call_stage = call_stage + + +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)) + + if _call_stage: + @warning_ignore("redundant_await") + await test_suite.before_test() + + +func set_test_name(test_name :StringName): + _test_name = test_name diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd new file mode 100644 index 00000000..dc8c53d2 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -0,0 +1,31 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseExecutionStage +extends IGdUnitExecutionStage + + +var _stage_single_test :IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() +var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterizedExecutionStage.new() + + +## Executes the test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_before() [br] +## -> test_case() [br] +## -> test_after() [br] +@warning_ignore("redundant_await") +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + if test_case.is_parameterized(): + await _stage_parameterized_test.execute(context) + elif test_case.is_fuzzed(): + await _stage_fuzzer_test.execute(context) + else: + await _stage_single_test.execute(context) + + +func set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_single_test.set_debug_mode(debug_mode) + _stage_fuzzer_test.set_debug_mode(debug_mode) + _stage_parameterized_test.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 new file mode 100644 index 00000000..dab18da1 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -0,0 +1,28 @@ +## The test suite shutdown hook implementation.[br] +## It executes the 'after()' block from the test-suite. +class_name GdUnitTestSuiteAfterStage +extends IGdUnitExecutionStage + + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + + @warning_ignore("redundant_await") + await test_suite.after() + # unreference last used assert form the test to prevent memory leaks + GdUnitThreadManager.get_current_context().set_assert(null) + await context.gc() + + var reports := context.reports() + var orphans := context.count_orphans() + if orphans > 0: + reports.push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) + fire_event(GdUnitEvent.new().suite_after(test_suite.get_script().resource_path, test_suite.get_name(), context.build_report_statistics(orphans, false), reports)) + + GdUnitTools.clear_tmp() + # Guard that checks if all doubled (spy/mock) objects are released + GdUnitClassDoubler.check_leaked_instances() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd new file mode 100644 index 00000000..869f5adc --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd @@ -0,0 +1,14 @@ +## The test suite startup hook implementation.[br] +## It executes the 'before()' block from the test-suite. +class_name GdUnitTestSuiteBeforeStage +extends IGdUnitExecutionStage + + +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())) + + @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 new file mode 100644 index 00000000..9e796c35 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -0,0 +1,114 @@ +## The test suite main execution stage.[br] +class_name GdUnitTestSuiteExecutionStage +extends IGdUnitExecutionStage + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _stage_before :IGdUnitExecutionStage = GdUnitTestSuiteBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestSuiteAfterStage.new() +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseExecutionStage.new() +var _fail_fast := false + + +## Executes all tests of an test suite.[br] +## It executes synchronized following stages[br] +## -> before() [br] +## -> run all test cases [br] +## -> after() [br] +func _execute(context :GdUnitExecutionContext) -> void: + if context.test_suite.__is_skipped: + await fire_test_suite_skipped(context) + else: + GdUnitMemoryObserver.guard_instance(context.test_suite.__awaiter) + await _stage_before.execute(context) + for test_case_index in context.test_suite.get_child_count(): + # iterate only over test cases + var test_case := context.test_suite.get_child(test_case_index) as _TestCase + if not is_instance_valid(test_case): + continue + context.test_suite.set_active_test_case(test_case.get_name()) + await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case.get_name())) + # stop on first error or if fail fast is enabled + if _fail_fast and context.test_failed(): + 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 + # we delete the current test suite where is execute the current test case to kill the function state + # and replace it by a clone without function state + 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 + 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 + dispose_timers(test_suite) + await GdUnitMemoryObserver.gc_guarded_instance("Manually free on awaiter", test_suite.__awaiter) + var parent := test_suite.get_parent() + var _test_suite = GdUnitTestSuite.new() + parent.remove_child(test_suite) + copy_properties(test_suite, _test_suite) + for child in test_suite.get_children(): + test_suite.remove_child(child) + _test_suite.add_child(child) + parent.add_child(_test_suite) + GdUnitMemoryObserver.guard_instance(_test_suite.__awaiter) + # finally free current test suite instance + test_suite.free() + await Engine.get_main_loop().process_frame + return _test_suite + + +func dispose_timers(test_suite :GdUnitTestSuite): + GdUnitTools.release_timers() + for child in test_suite.get_children(): + if child is Timer: + child.stop() + test_suite.remove_child(child) + child.free() + + +func copy_properties(source :Object, target :Object): + if not source is _TestCase and not source is GdUnitTestSuite: + return + for property in source.get_property_list(): + var property_name = property["name"] + if property_name == "__awaiter": + continue + target.set(property_name, source.get(property_name)) + + +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)) + var statistics = { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED_COUNT: skip_count, + 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 + + +func set_debug_mode(debug_mode :bool = false): + 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) + + +func fail_fast(enabled :bool) -> void: + _fail_fast = enabled diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd new file mode 100644 index 00000000..0f6ae93a --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd @@ -0,0 +1,39 @@ +## The interface of execution stage.[br] +## An execution stage is defined as an encapsulated task that can execute 1-n substages covered by its own execution context.[br] +## Execution stage are always called synchronously. +class_name IGdUnitExecutionStage +extends RefCounted + +var _debug_mode := false + + +## Executes synchronized the implemented stage in its own execution context.[br] +## example:[br] +## [codeblock] +## # waits for 100ms +## await MyExecutionStage.new().execute() +## [/codeblock][br] +func execute(context :GdUnitExecutionContext) -> void: + context.set_active() + @warning_ignore("redundant_await") + await _execute(context) + + +## Sends the event to registered listeners +func fire_event(event :GdUnitEvent) -> void: + if _debug_mode: + GdUnitSignals.instance().gdunit_event_debug.emit(event) + else: + GdUnitSignals.instance().gdunit_event.emit(event) + + +## Internal testing stuff.[br] +## Sets the executor into debug mode to emit `GdUnitEvent` via signal `gdunit_event_debug` +func set_debug_mode(debug_mode :bool) -> void: + _debug_mode = debug_mode + + +## The execution phase to be carried out. +func _execute(_context :GdUnitExecutionContext) -> void: + @warning_ignore("assert_always_false") + assert(false, "The execution stage is not implemented") diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd new file mode 100644 index 00000000..269a9ea8 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -0,0 +1,21 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseFuzzedExecutionStage +extends IGdUnitExecutionStage + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +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) + + +func set_debug_mode(debug_mode :bool = false): + 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/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd new file mode 100644 index 00000000..6b91d588 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -0,0 +1,53 @@ +## The fuzzed test case execution stage.[br] +class_name GdUnitTestCaseFuzzedTestStage +extends IGdUnitExecutionStage + +var _expression_runner := GdUnitExpressionRunner.new() + + +## Executes a test case with given fuzzers 'test_()' iterative.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_suite := context.test_suite + var test_case := context.test_case + var fuzzers := create_fuzzers(test_suite, test_case) + + # guard on fuzzers + for fuzzer in fuzzers: + GdUnitMemoryObserver.guard_instance(fuzzer) + + for iteration in test_case.iterations(): + @warning_ignore("redundant_await") + await test_suite.before_test() + await test_case.execute(fuzzers, iteration) + @warning_ignore("redundant_await") + await test_suite.after_test() + if test_case.is_interupted(): + break + # interrupt at first failure + var reports := context.reports() + if not reports.is_empty(): + var report :GdUnitReport = reports.pop_front() + reports.append(GdUnitReport.new() \ + .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) + break + await context.gc() + + # unguard on fuzzers + if not test_case.is_interupted(): + for fuzzer in fuzzers: + GdUnitMemoryObserver.unguard_instance(fuzzer) + + +func create_fuzzers(test_suite :GdUnitTestSuite, test_case :_TestCase) -> Array[Fuzzer]: + if not test_case.is_fuzzed(): + return 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()) + fuzzer._iteration_index = 0 + fuzzer._iteration_limit = test_case.iterations() + fuzzers.append(fuzzer) + return fuzzers diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd new file mode 100644 index 00000000..52ccdc47 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseParameterizedExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) +var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseParamaterizedTestStage.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) + + +func set_debug_mode(debug_mode :bool = false): + 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/parameterized/GdUnitTestCaseParameterizedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd new file mode 100644 index 00000000..49202e77 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd @@ -0,0 +1,52 @@ +## The parameterized test case execution stage.[br] +class_name GdUnitTestCaseParamaterizedTestStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() + + +## Executes a paramaterized test case.[br] +## It executes synchronized following stages[br] +## -> test_case( ) [br] +func _execute(context :GdUnitExecutionContext) -> void: + var test_case := context.test_case + var test_case_parameters := test_case.test_parameters() + var test_parameter_index := test_case.test_parameter_index() + var test_case_names := test_case.test_case_names() + var is_fail := false + var is_error := false + var failing_index := 0 + + for test_case_index in test_case.test_parameters().size(): + # is test_parameter_index is set, we run this parameterized test only + if test_parameter_index != -1 and test_parameter_index != test_case_index: + continue + + _stage_before.set_test_name(test_case_names[test_case_index]) + _stage_after.set_test_name(test_case_names[test_case_index]) + + var test_context := GdUnitExecutionContext.of(context) + await _stage_before.execute(test_context) + await test_case.execute_paramaterized(test_case_parameters[test_case_index]) + 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 = test_case_index - 1 + test_context.reports().clear() + 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 set_debug_mode(debug_mode :bool = false): + super.set_debug_mode(debug_mode) + _stage_before.set_debug_mode(debug_mode) + _stage_after.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 new file mode 100644 index 00000000..fde7eb29 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -0,0 +1,22 @@ +## The test case execution stage.[br] +class_name GdUnitTestCaseSingleExecutionStage +extends IGdUnitExecutionStage + + +var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() +var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() +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) + + +func set_debug_mode(debug_mode :bool = false): + 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/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd new file mode 100644 index 00000000..6882fe29 --- /dev/null +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -0,0 +1,11 @@ +## The single test case execution stage.[br] +class_name GdUnitTestCaseSingleTestStage +extends IGdUnitExecutionStage + + +## Executes a single test case 'test_()'.[br] +## It executes synchronized following stages[br] +## -> test_case() [br] +func _execute(context :GdUnitExecutionContext) -> void: + await context.test_case.execute() + await context.gc() diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd index d724a39a..846d414c 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -1,36 +1,46 @@ -# holds all decodings for default values -class_name GdDefaultValueDecoder +# holds all decodings for default values +class_name GdDefaultValueDecoder extends GdUnitSingleton @warning_ignore("unused_parameter") var _decoders = { - TYPE_NIL: func(value): return "", + TYPE_NIL: func(value): return "null", TYPE_STRING: func(value): return '"%s"' % value, - TYPE_STRING_NAME: func(value): return '"%s"' % value, + TYPE_STRING_NAME: _on_type_StringName, TYPE_BOOL: func(value): return str(value).to_lower(), TYPE_FLOAT: func(value): return '%f' % value, - TYPE_COLOR: func(value): return "Color%s" % value, - TYPE_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_BYTE_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_STRING_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_FLOAT32_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_FLOAT64_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_INT32_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_INT64_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_COLOR_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_VECTOR2_ARRAY: func(value): return GdArrayTools.as_string(value), - TYPE_PACKED_VECTOR3_ARRAY: func(value): return GdArrayTools.as_string(value), + TYPE_COLOR: _on_type_Color, + TYPE_ARRAY: _on_type_Array.bind(TYPE_ARRAY), + TYPE_PACKED_BYTE_ARRAY: _on_type_Array.bind(TYPE_PACKED_BYTE_ARRAY), + TYPE_PACKED_STRING_ARRAY: _on_type_Array.bind(TYPE_PACKED_STRING_ARRAY), + TYPE_PACKED_FLOAT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT32_ARRAY), + TYPE_PACKED_FLOAT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_FLOAT64_ARRAY), + TYPE_PACKED_INT32_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT32_ARRAY), + TYPE_PACKED_INT64_ARRAY: _on_type_Array.bind(TYPE_PACKED_INT64_ARRAY), + TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), + TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), + TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), + TYPE_DICTIONARY: _on_type_Dictionary, TYPE_RID: _on_type_RID, - TYPE_VECTOR2: func(value): return "Vector2%s" % value, - TYPE_VECTOR2I: func(value): return "Vector2i%s" % value, - TYPE_VECTOR3: func(value): return "Vector3%s" % value, - TYPE_VECTOR3I: func(value): return "Vector3i%s" % value, - TYPE_VECTOR4: func(value): return "Vector4%s" % value, - TYPE_VECTOR4I: func(value): return "Vector4i%s" % value, - TYPE_RECT2: _on_decode_Rect2.bind(GdDefaultValueDecoder._regex("P: ?(\\(.+\\)), S: ?(\\(.+\\))")), - TYPE_RECT2I: _on_decode_Rect2i.bind(GdDefaultValueDecoder._regex("P: ?(\\(.+\\)), S: ?(\\(.+\\))")), + TYPE_NODE_PATH: _on_type_NodePath, + TYPE_VECTOR2: _on_type_Vector.bind(TYPE_VECTOR2), + TYPE_VECTOR2I: _on_type_Vector.bind(TYPE_VECTOR2I), + TYPE_VECTOR3: _on_type_Vector.bind(TYPE_VECTOR3), + TYPE_VECTOR3I: _on_type_Vector.bind(TYPE_VECTOR3I), + TYPE_VECTOR4: _on_type_Vector.bind(TYPE_VECTOR4), + TYPE_VECTOR4I: _on_type_Vector.bind(TYPE_VECTOR4I), + TYPE_RECT2: _on_type_Rect2, + TYPE_RECT2I: _on_type_Rect2i, + TYPE_PLANE: _on_type_Plane, + TYPE_QUATERNION: _on_type_Quaternion, + TYPE_AABB: _on_type_AABB, + TYPE_BASIS: _on_type_Basis, + TYPE_CALLABLE: _on_type_Callable, + TYPE_SIGNAL: _on_type_Signal, TYPE_TRANSFORM2D: _on_type_Transform2D, TYPE_TRANSFORM3D: _on_type_Transform3D, + TYPE_PROJECTION: _on_type_Projection, + TYPE_OBJECT: _on_type_Object } static func _regex(pattern :String) -> RegEx: @@ -46,49 +56,199 @@ func get_decoder(type :int) -> Callable: return _decoders.get(type, func(value): return '%s' % value) -func _on_type_Transform2D(value :Variant) -> String: - var transform := value as Transform2D +func _on_type_StringName(value :StringName) -> String: + if value.is_empty(): + return 'StringName()' + return 'StringName("%s")' % value + + +func _on_type_Object(value :Object, type :int) -> String: + return str(value) + + +func _on_type_Color(color :Color) -> String: + if color == Color.BLACK: + return "Color()" + return "Color%s" % color + + +func _on_type_NodePath(path :NodePath) -> String: + if path.is_empty(): + return 'NodePath()' + return 'NodePath("%s")' % path + + +func _on_type_Callable(cb :Callable) -> String: + return 'Callable()' + + +func _on_type_Signal(s :Signal) -> String: + return 'Signal()' + + +func _on_type_Dictionary(dict :Dictionary) -> String: + if dict.is_empty(): + return '{}' + return str(dict) + + +func _on_type_Array(value, type :int) -> String: + match type: + TYPE_ARRAY: + return str(value) + + TYPE_PACKED_COLOR_ARRAY: + var colors := PackedStringArray() + for color in value as PackedColorArray: + colors.append(_on_type_Color(color)) + if colors.is_empty(): + return "PackedColorArray()" + return "PackedColorArray([%s])" % ", ".join(colors) + + TYPE_PACKED_VECTOR2_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector2Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR2)) + if vectors.is_empty(): + return "PackedVector2Array()" + return "PackedVector2Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_VECTOR3_ARRAY: + var vectors := PackedStringArray() + for vector in value as PackedVector3Array: + vectors.append(_on_type_Vector(vector, TYPE_VECTOR3)) + if vectors.is_empty(): + return "PackedVector3Array()" + return "PackedVector3Array([%s])" % ", ".join(vectors) + + TYPE_PACKED_STRING_ARRAY: + var values := PackedStringArray() + for v in value as PackedStringArray: + values.append('"%s"' % v) + if values.is_empty(): + return "PackedStringArray()" + return "PackedStringArray([%s])" % ", ".join(values) + + TYPE_PACKED_BYTE_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY,\ + TYPE_PACKED_FLOAT64_ARRAY,\ + TYPE_PACKED_INT32_ARRAY,\ + TYPE_PACKED_INT64_ARRAY: + var vectors := PackedStringArray() + for vector in value as Array: + vectors.append(str(vector)) + if vectors.is_empty(): + return GdObjects.type_as_string(type) + "()" + return "%s([%s])" % [GdObjects.type_as_string(type), ", ".join(vectors)] + return "unknown array type %d" % type + + +func _on_type_Vector(value :Variant, type :int) -> String: + match type: + TYPE_VECTOR2: + if value == Vector2(): + return "Vector2()" + return "Vector2%s" % value + TYPE_VECTOR2I: + if value == Vector2i(): + return "Vector2i()" + return "Vector2i%s" % value + TYPE_VECTOR3: + if value == Vector3(): + return "Vector3()" + return "Vector3%s" % value + TYPE_VECTOR3I: + if value == Vector3i(): + return "Vector3i()" + return "Vector3i%s" % value + TYPE_VECTOR4: + if value == Vector4(): + return "Vector4()" + return "Vector4%s" % value + TYPE_VECTOR4I: + if value == Vector4i(): + return "Vector4i()" + return "Vector4i%s" % value + return "unknown vector type %d" % type + + +func _on_type_Transform2D(transform :Transform2D) -> String: + if transform == Transform2D(): + return "Transform2D()" return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] -func _on_type_Transform3D(value :Variant) -> String: - var transform :Transform3D = value +func _on_type_Transform3D(transform :Transform3D) -> String: + if transform == Transform3D(): + return "Transform3D()" return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] +func _on_type_Projection(projection :Projection) -> String: + return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] + + @warning_ignore("unused_parameter") -func _on_type_RID(value :Variant) -> String: +func _on_type_RID(value :RID) -> String: return "RID()" -func _on_decode_Rect2(value :Variant, regEx :RegEx) -> String: - for reg_match in regEx.search_all(str(value)): - var decodeP = reg_match.get_string(1) - var decodeS = reg_match.get_string(2) - return "Rect2(Vector2%s, Vector2%s)" % [decodeP, decodeS] - return "Rect2()" +func _on_type_Rect2(rect :Rect2) -> String: + if rect == Rect2(): + return "Rect2()" + return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] + + +func _on_type_Rect2i(rect :Variant) -> String: + if rect == Rect2i(): + return "Rect2i()" + return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] + + +func _on_type_Plane(plane :Plane) -> String: + if plane == Plane(): + return "Plane()" + return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] -func _on_decode_Rect2i(value :Variant, regEx :RegEx) -> String: - for reg_match in regEx.search_all(str(value)): - var decodeP = reg_match.get_string(1) - var decodeS = reg_match.get_string(2) - return "Rect2i(Vector2i%s, Vector2i%s)" % [decodeP, decodeS] - return "Rect2i()" +func _on_type_Quaternion(quaternion :Quaternion) -> String: + if quaternion == Quaternion(): + return "Quaternion()" + return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] + + +func _on_type_AABB(aabb :AABB) -> String: + if aabb == AABB(): + return "AABB()" + return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] + + +func _on_type_Basis(basis :Basis) -> String: + if basis == Basis(): + return "Basis()" + return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] static func decode(value :Variant) -> String: - var type := typeof(value) + var type := typeof(value) + if GdArrayTools.is_type_array(type) and value.is_empty(): + return "" var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).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" + if type == TYPE_OBJECT: + return decoder.call(value, type) return decoder.call(value) static func decode_typed(type :int, value :Variant) -> String: + if value == null: + return "null" var decoder :Callable = instance("GdUnitDefaultValueDecoders", func(): return GdDefaultValueDecoder.new()).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" + if type == TYPE_OBJECT: + return decoder.call(value, type) return decoder.call(value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd index 6c444936..08f70079 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -5,7 +5,7 @@ var _name: String var _type: int var _default_value :Variant -const UNDEFINED = "<-NO_ARG->" +const UNDEFINED :Variant = "<-NO_ARG->" const ARG_PARAMETERIZED_TEST := "test_parameters" @@ -34,7 +34,7 @@ func type() -> int: func has_default() -> bool: - return _default_value != UNDEFINED + return not is_same(_default_value, UNDEFINED) func is_parameter_set() -> bool: diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd index e09582c2..51c18b0b 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -204,9 +204,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 := GdFunctionArgument.UNDEFINED + var arg_default :Variant = GdFunctionArgument.UNDEFINED if not defaults.is_empty(): - arg_default = _argument_default_value(arg, defaults.pop_back()) + var default_value = defaults.pop_back() + arg_default = GdDefaultValueDecoder.decode_typed(arg_type, default_value) args_.push_front(GdFunctionArgument.new(arg_name, arg_type, arg_default)) return args_ @@ -247,32 +248,3 @@ static func _argument_type_as_string(arg :Dictionary) -> String: return "" _: return GdObjects.type_as_string(type) - - -static func _argument_default_value(arg :Dictionary, default_value) -> String: - if default_value == null: - return "null" - var type := _argument_type(arg) - match type: - TYPE_NIL: - return "null" - TYPE_RID: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_STRING, TYPE_STRING_NAME: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_BOOL: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_RECT2, TYPE_RECT2I: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_TRANSFORM2D, TYPE_TRANSFORM3D: - return GdDefaultValueDecoder.decode_typed(type, default_value) - TYPE_OBJECT: - if default_value == null: - return "null" - if GdObjects.is_primitive_type(default_value): - return str(default_value) - if GdArrayTools.is_type_array(type): - if default_value == null or default_value.is_empty(): - return "[]" - return GdDefaultValueDecoder.decode_typed(type, default_value) - return "%s(%s)" % [GdObjects.type_as_string(type), str(default_value).trim_prefix("(").trim_suffix(")")] diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd index fa0a1ea7..0b2e55b6 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -778,9 +778,9 @@ func extract_functions(script :GDScript, clazz_name :String, clazz_path :PackedS return parse_functions(source_code, clazz_name, clazz_path) -func parse(clazz_name :String, clazz_path :PackedStringArray) -> Result: +func parse(clazz_name :String, clazz_path :PackedStringArray) -> GdUnitResult: if clazz_path.is_empty(): - return Result.error("Invalid script path '%s'" % clazz_path) + 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) @@ -792,4 +792,4 @@ func parse(clazz_name :String, clazz_path :PackedStringArray) -> Result: 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 Result.success(gd_class) + return GdUnitResult.success(gd_class) diff --git a/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd b/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd deleted file mode 100644 index 9ff4a9e7..00000000 --- a/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd +++ /dev/null @@ -1,118 +0,0 @@ -# collects all reports seperated as warnings and failures/errors -class_name GdUnitReportCollector -extends RefCounted - -const STAGE_TEST_SUITE_BEFORE = 1 -const STAGE_TEST_SUITE_AFTER = 2 -const STAGE_TEST_CASE_BEFORE = 4 -const STAGE_TEST_CASE_EXECUTE = 8 -const STAGE_TEST_CASE_AFTER = 16 - -var ALL_REPORT_STATES := [STAGE_TEST_SUITE_BEFORE, STAGE_TEST_SUITE_AFTER, STAGE_TEST_CASE_BEFORE, STAGE_TEST_CASE_EXECUTE, STAGE_TEST_CASE_AFTER] -var _current_stage :int -var _consume_reports := true - - -var _reports_by_state :Dictionary = { - STAGE_TEST_SUITE_BEFORE : [] as Array[GdUnitReport], - STAGE_TEST_SUITE_AFTER : [] as Array[GdUnitReport], - STAGE_TEST_CASE_BEFORE : [] as Array[GdUnitReport], - STAGE_TEST_CASE_AFTER : [] as Array[GdUnitReport], - STAGE_TEST_CASE_EXECUTE : [] as Array[GdUnitReport], -} - - -func _init(): - GdUnitSignals.instance().gdunit_report.connect(consume) - - -func get_reports_by_state(execution_state :int) -> Array[GdUnitReport]: - return _reports_by_state.get(execution_state) - - -func add_report(execution_state :int, report :GdUnitReport) -> void: - get_reports_by_state(execution_state).append(report) - - -func push_front(execution_state :int, report :GdUnitReport) -> void: - get_reports_by_state(execution_state).push_front(report) - - -func pop_front(execution_state :int) -> GdUnitReport: - return get_reports_by_state(execution_state).pop_front() - - -func clear_reports(execution_states :int) -> void: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - get_reports_by_state(state).clear() - - -func get_reports(execution_states :int) -> Array[GdUnitReport]: - var reports :Array[GdUnitReport] = [] - for state in ALL_REPORT_STATES: - if execution_states&state == state: - reports.append_array(get_reports_by_state(state)) - return reports - - -func has_errors(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.is_error(): - return true - return false - - -func count_errors(execution_states :int) -> int: - var count := 0 - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.is_error(): - count += 1 - return count - - -func has_failures(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.FAILURE: - return true - return false - - -func count_failures(execution_states :int) -> int: - var count := 0 - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.FAILURE: - count += 1 - return count - - -func has_warnings(execution_states :int) -> bool: - for state in ALL_REPORT_STATES: - if execution_states&state == state: - for report in get_reports_by_state(state): - if report.type() == GdUnitReport.WARN: - return true - return false - - -func set_stage(stage :int) -> void: - _current_stage = stage - - - -# we need to disable report collection for testing purposes -func set_consume_reports(enabled :bool) -> void: - _consume_reports = enabled - - -func consume(report :GdUnitReport) -> void: - if _consume_reports: - add_report(_current_stage, report) diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd index 8fd313ec..d0eaa16d 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -2,23 +2,31 @@ class_name GdUnitThreadContext extends RefCounted var _thread :Thread +var _thread_name :String +var _thread_id :int var _assert :GdUnitAssert var _signal_collector :GdUnitSignalCollector +var _execution_context :GdUnitExecutionContext func _init(thread :Thread = null): - _thread = thread + if thread != null: + _thread = thread + _thread_name = thread.get_meta("name") + _thread_id = thread.get_id() as int + else: + _thread_name = "main" + _thread_id = OS.get_main_thread_id() _signal_collector = GdUnitSignalCollector.new() -func init() -> void: - clear() - - -func clear() -> void: +func dispose() -> void: _assert = null if is_instance_valid(_signal_collector): _signal_collector.clear() + _signal_collector = null + _execution_context = null + _thread = null func set_assert(value :GdUnitAssert) -> GdUnitThreadContext: @@ -30,12 +38,25 @@ func get_assert() -> GdUnitAssert: return _assert +func set_execution_context(context :GdUnitExecutionContext) -> void: + _execution_context = context + + +func get_execution_context() -> GdUnitExecutionContext: + return _execution_context + + +func get_execution_context_id() -> int: + return _execution_context.get_instance_id() + + func get_signal_collector() -> GdUnitSignalCollector: return _signal_collector +func thread_id() -> int: + return _thread_id + + func _to_string() -> String: - var id := OS.get_main_thread_id() if _thread == null else int(_thread.get_id()) - var name := "main" if _thread == null else _thread.get_meta("name") as String - #var assert_ = _assert if is_instance_valid(_assert) else null - return "Thread <%s>: %s " % [name, id] + return "ThreadContext <%s>: %s " % [_thread_name, _thread_id] diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd index 24421b73..532946de 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd @@ -1,40 +1,62 @@ +## A manager to run new thread and crate a ThreadContext shared over the actual test run class_name GdUnitThreadManager extends RefCounted -## { id: = GdUnitThreadContext } -var _threads_by_id := {} +## { = } +var _thread_context_by_id := {} +## holds the current thread id +var _current_thread_id :int = -1 +func _init(): + # add initail the main thread + _current_thread_id = OS.get_thread_caller_id() + _thread_context_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() -func _init(): - _threads_by_id[OS.get_main_thread_id()] = GdUnitThreadContext.new() +static func instance() -> GdUnitThreadManager: + return GdUnitSingleton.instance("GdUnitThreadManager", func(): return GdUnitThreadManager.new()) -func _notification(_what): - # prints("_notification", what) - pass +## Runs a new thread by given name and Callable.[br] +## A new GdUnitThreadContext is created, which is used for the actual test execution.[br] +## We need this custom implementation while this bug is not solved +## Godot issue https://github.com/godotengine/godot/issues/79637 +static func run(name :String, cb :Callable) -> Variant: + return await instance()._run(name, cb) -static func instance() -> GdUnitThreadManager: - return GdUnitSingleton.instance("GdUnitThreadManager", func(): return GdUnitThreadManager.new()) +## Returns the current valid thread context +static func get_current_context() -> GdUnitThreadContext: + return instance()._get_current_context() -static func create_thread(name :String, cb :Callable) -> Thread: - var t := Thread.new() - t.set_meta("name", name) - t.start(cb) - instance().register_thread_context(t) - return t +func _run(name :String, cb :Callable): + # we do this hack because of `OS.get_thread_caller_id()` not returns the current id + # when await process_frame is called inside the fread + var save_current_thread_id = _current_thread_id + var thread := Thread.new() + thread.set_meta("name", name) + thread.start(cb) + _current_thread_id = thread.get_id() as int + _register_thread(thread, _current_thread_id) + var result :Variant = await thread.wait_to_finish() + _unregister_thread(_current_thread_id) + # restore original thread id + _current_thread_id = save_current_thread_id + return result -func register_thread_context(thread :Thread): - _threads_by_id[thread.get_id() as int] = GdUnitThreadContext.new(thread) +func _register_thread(thread :Thread, thread_id :int) -> void: + var context := GdUnitThreadContext.new(thread) + _thread_context_by_id[thread_id] = context -func get_context(thread_id :int) -> GdUnitThreadContext: - return _threads_by_id.get(thread_id) +func _unregister_thread(thread_id :int) -> void: + var context := _thread_context_by_id.get(thread_id) as GdUnitThreadContext + if context: + _thread_context_by_id.erase(thread_id) + context.dispose() -static func get_current_context() -> GdUnitThreadContext: - var current_thread_id := OS.get_thread_caller_id() - return instance().get_context(current_thread_id) +func _get_current_context() -> GdUnitThreadContext: + return _thread_context_by_id.get(_current_thread_id) diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd index 49ff160d..74cded24 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -52,14 +52,18 @@ func _call_func(value, func_name :String): if GdArrayTools.is_array_type(value) and func_name == "empty": return value.is_empty() - if not (value is Object): - if GdUnitSettings.is_verbose_assert_warnings(): - push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) - return "n.a." - var extract := Callable(value, func_name) - if extract.is_valid(): - return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) - else: - if GdUnitSettings.is_verbose_assert_warnings(): - push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) - return "n.a." + if is_instance_valid(value): + # extract from function + if value.has_method(func_name): + var extract := Callable(value, func_name) + if extract.is_valid(): + return value.call(func_name) if args().is_empty() else value.callv(func_name, args()) + else: + # if no function exists than try to extract form parmeters + var parameter = value.get(func_name) + if parameter != null: + return parameter + # nothing found than return 'n.a.' + if GdUnitSettings.is_verbose_assert_warnings(): + push_warning("Extracting value from element '%s' by func '%s' failed! Converting to \"n.a.\"" % [value, func_name]) + return "n.a." diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Fuzzer.gd index 1d240266..b17b6865 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -24,7 +24,7 @@ var _iteration_limit :int = ITERATION_DEFAULT_COUNT # generates the next fuzz value # needs to be implement -func next_value(): +func next_value() -> Variant: push_error("Invalid vall. Fuzzer not implemented 'next_value()'") return null diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd index 40015629..0235ee20 100644 --- a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd @@ -19,7 +19,7 @@ func _init(from: int, to: int, mode :int = NORMAL): _mode = mode -func next_value() -> int: +func next_value() -> Variant: var value := randi_range(_from, _to) match _mode: NORMAL: diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd index b75e0423..b05ee138 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -52,7 +52,7 @@ static func build_chars(from :int, to :int) -> Array: characters.append(character) return characters -func next_value(): +func next_value() -> Variant: var value := PackedByteArray() var max_char := len(_charset) var length :int = max(_min_length, randi() % _max_length) diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd index 6bc43c3a..87c88900 100644 --- a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd @@ -10,7 +10,7 @@ func _init(from: Vector2,to: Vector2): _from = from _to = to -func next_value() -> Vector2: +func next_value() -> Variant: var x = randf_range(_from.x, _to.x) var y = randf_range(_from.y, _to.y) return Vector2(x, y) diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd index 7ff4d8c4..16b6e876 100644 --- a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd @@ -10,7 +10,7 @@ func _init(from: Vector3,to: Vector3): _from = from _to = to -func next_value() -> Vector3: +func next_value() -> Variant: var x = randf_range(_from.x, _to.x) var y = randf_range(_from.y, _to.y) var z = randf_range(_from.z, _to.z) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd index 1a38e241..df7f02c3 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -2,36 +2,22 @@ class_name GdUnitMockBuilder extends GdUnitClassDoubler const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const GdUnitMemoryPool = preload("res://addons/gdUnit4/src/core/GdUnitMemoryPool.gd") - - -# holds mocker runtime configuration -const KEY_REPORT_PUSH_ERRORS = "report_push_errors" - - -# only for testing -static func do_push_errors(enabled :bool) -> void: - GdUnitStaticDictionary.add_value(KEY_REPORT_PUSH_ERRORS, enabled) - - -static func is_push_errors_enabled() -> bool: - return GdUnitStaticDictionary.get_value(KEY_REPORT_PUSH_ERRORS, false) +const MOCK_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/mocking/GdUnitMockImpl.gd") static func is_push_errors() -> bool: - return is_push_errors_enabled() or GdUnitSettings.is_report_push_errors() + return GdUnitSettings.is_report_push_errors() -static func build(caller :Object, clazz, mock_mode :String, debug_write := false) -> Object: - var memory_pool :GdUnitMemoryPool.POOL = caller.get_meta(GdUnitMemoryPool.META_PARAM) +static func build(clazz, mock_mode :String, debug_write := false) -> Object: var push_errors := is_push_errors() if not is_mockable(clazz, push_errors): return null # mocking a scene? if GdObjects.is_scene(clazz): - return mock_on_scene(clazz as PackedScene, memory_pool, debug_write) + 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), memory_pool, debug_write) + return mock_on_scene(load(clazz), debug_write) # mocking a script var instance := create_instance(clazz) var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) @@ -43,7 +29,7 @@ static func build(caller :Object, clazz, mock_mode :String, debug_write := false mock_instance.__set_script(mock) mock_instance.__set_singleton() mock_instance.__set_mode(mock_mode) - return GdUnitMemoryPool.register_auto_free(mock_instance, memory_pool) + return register_auto_free(mock_instance) static func create_instance(clazz) -> Object: @@ -64,7 +50,7 @@ static func create_instance(clazz) -> Object: return null -static func mock_on_scene(scene :PackedScene, memory_pool :int, debug_write :bool) -> Object: +static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Object: var push_errors := is_push_errors() if not scene.can_instantiate(): if push_errors: @@ -85,7 +71,7 @@ static func mock_on_scene(scene :PackedScene, memory_pool :int, debug_write :boo scene_instance.set_script(mock) scene_instance.__set_singleton() scene_instance.__set_mode(GdUnitMock.CALL_REAL_FUNC) - return GdUnitMemoryPool.register_auto_free(scene_instance, memory_pool) + return register_auto_free(scene_instance) static func get_class_info(clazz :Variant) -> Dictionary: @@ -101,7 +87,7 @@ static func mock_on_script(instance :Object, clazz :Variant, function_excludes : var push_errors := is_push_errors() var function_doubler := GdUnitMockFunctionDoubler.new(push_errors) var class_info := get_class_info(clazz) - var lines := load_template(GdUnitMockImpl, class_info, instance) + var lines := load_template(MOCK_TEMPLATE.source_code, class_info, instance) var clazz_name :String = class_info.get("class_name") var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) @@ -175,3 +161,7 @@ static func is_mockable(clazz :Variant, push_errors :bool=false) -> bool: return false # finally check is extending from script return GdObjects.is_script(resource) or GdObjects.is_scene(resource) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd index eea9726a..e544bcef 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd @@ -6,19 +6,17 @@ const TEMPLATE_FUNC_WITH_RETURN_VALUE = """ var args :Array = ["$(func_name)", $(arguments)] if $(instance)__is_prepare_return_value(): - return $(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) return ${default_return_value} else: $(instance)__save_function_interaction(args) - if $(instance)__saved_return_values.has(args): - return $(instance)__saved_return_values.get(args) - - if $(instance)__do_call_real_func("$(func_name)"): + if $(instance)__do_call_real_func("$(func_name)", args): return $(await)super($(arguments)) - return ${default_return_value} + return $(instance)__get_mocked_return_value_or_default(args, ${default_return_value}) """ @@ -49,6 +47,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ 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) return ${default_return_value} if $(instance)__is_verify_interactions(): $(instance)__verify_interactions(args) @@ -56,7 +55,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ else: $(instance)__save_function_interaction(args) - if $(instance)__do_call_real_func("$(func_name)"): + if $(instance)__do_call_real_func("$(func_name)", args): match varargs.size(): 0: return $(await)super($(arguments)) 1: return $(await)super($(arguments), varargs[0]) @@ -69,7 +68,7 @@ const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ 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 ${default_return_value} + return __get_mocked_return_value_or_default(args, ${default_return_value}) """ diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd index 3a71435b..94110c28 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -1,20 +1,30 @@ -# warnings-disable -# warning-ignore:unused_argument -class_name GdUnitMockImpl ################################################################################ # internal mocking stuff ################################################################################ const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" -var __working_mode :String +var __working_mode := GdUnitMock.RETURN_DEFAULTS var __excluded_methods :PackedStringArray = [] var __do_return_value = null -var __saved_return_values := Dictionary() +var __prepare_return_value := false + +#{ = { +# = +# } +#} +var __mocked_return_values := Dictionary() static func __instance(): - return GdUnitStaticDictionary.get_value(__INSTANCE_ID) + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) func __instance_id() -> String: @@ -23,22 +33,78 @@ func __instance_id() -> String: func __set_singleton(): # store self need to mock static functions - GdUnitStaticDictionary.add_value(__INSTANCE_ID, self) + Engine.set_meta(__INSTANCE_ID, self) func __release_double(): # we need to release the self reference manually to prevent orphan nodes - GdUnitStaticDictionary.erase(__INSTANCE_ID) + Engine.remove_meta(__INSTANCE_ID) func __is_prepare_return_value() -> bool: - return __do_return_value != null + return __prepare_return_value + + +func __sort_by_argument_matcher(left_args :Array, _right_args :Array) -> bool: + for larg in left_args: + if larg is GdUnitArgumentMatcher: + return false + return true + + +# we need to sort by matcher arguments so that they are all at the end of the list +func __sort_dictionary(unsorted_args :Dictionary) -> Dictionary: + # only need to sort if contains more than one entry + if unsorted_args.size() <= 1: + return unsorted_args + var sorted_args := unsorted_args.keys() + sorted_args.sort_custom(__sort_by_argument_matcher) + var sorted_result := {} + for key in sorted_args: + sorted_result[key] = unsorted_args[key] + return sorted_result func __save_function_return_value(args :Array): - __saved_return_values[args] = __do_return_value + var func_name :String = args[0] + var func_args :Array = args.slice(1) + var mocked_return_value_by_args :Dictionary = __mocked_return_values.get(func_name, {}) + mocked_return_value_by_args[func_args] = __do_return_value + __mocked_return_values[func_name] = __sort_dictionary(mocked_return_value_by_args) __do_return_value = null - return __saved_return_values[args] + __prepare_return_value = false + + +func __is_mocked_args_match(func_args :Array, mocked_args :Array) -> bool: + var is_matching := false + for args in mocked_args: + if func_args.size() != args.size(): + continue + is_matching = true + for arg_index in func_args.size(): + var func_arg = func_args[arg_index] + var mock_arg = args[arg_index] + if mock_arg is GdUnitArgumentMatcher: + is_matching = is_matching and mock_arg.is_match(func_arg) + else: + is_matching = is_matching and typeof(func_arg) == typeof(mock_arg) and func_arg == mock_arg + if not is_matching: + break + if is_matching: + break + return is_matching + + +func __get_mocked_return_value_or_default(args :Array, default_return_value :Variant) -> Variant: + var func_name :String = args[0] + if not __mocked_return_values.has(func_name): + return default_return_value + var func_args :Array = args.slice(1) + var mocked_args :Array = __mocked_return_values.get(func_name).keys() + for margs in mocked_args: + if __is_mocked_args_match(func_args, [margs]): + return __mocked_return_values[func_name][margs] + return default_return_value func __set_script(script :GDScript) -> void: @@ -50,8 +116,14 @@ func __set_mode(working_mode :String): return self -func __do_call_real_func(func_name :String) -> bool: - return __working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(func_name) +func __do_call_real_func(func_name :String, func_args := []) -> bool: + var is_call_real_func := __working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(func_name) + # do not call real funcions for mocked functions + if is_call_real_func and __mocked_return_values.has(func_name): + var args :Array = func_args.slice(1) + var mocked_args :Array = __mocked_return_values.get(func_name).keys() + return not __is_mocked_args_match(args, mocked_args) + return is_call_real_func func __exclude_method_call(exluded_methods :PackedStringArray) -> void: @@ -60,4 +132,5 @@ func __exclude_method_call(exluded_methods :PackedStringArray) -> void: func __do_return(return_value): __do_return_value = return_value + __prepare_return_value = true return self diff --git a/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd deleted file mode 100644 index f6e3dfa1..00000000 --- a/addons/gdUnit4/src/monitor/GdUnitMemMonitor.gd +++ /dev/null @@ -1,26 +0,0 @@ -class_name GdUnitMemMonitor -extends GdUnitMonitor - -var _orphan_nodes_start :float -var _orphan_nodes_end :float -var _orphan_total :float - -func _init(name :String = ""): - super("MemMonitor:" + name) - _orphan_nodes_start = 0 - _orphan_nodes_end = 0 - _orphan_total = 0 - -func reset(): - _orphan_total = 0 - -func start(): - _orphan_nodes_start = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - -func stop(): - _orphan_nodes_end = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) - _orphan_total += _orphan_nodes_end - _orphan_nodes_start - -func orphan_nodes() -> int: - return _orphan_total as int - diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd new file mode 100644 index 00000000..9bf76e46 --- /dev/null +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd @@ -0,0 +1,27 @@ +class_name GdUnitOrphanNodesMonitor +extends GdUnitMonitor + +var _initial_count := 0 +var _orphan_count := 0 +var _orphan_detection_enabled :bool + + +func _init(name :String = ""): + super("OrphanNodesMonitor:" + name) + _orphan_detection_enabled = GdUnitSettings.is_verbose_orphans() + + +func start(): + _initial_count = _orphans() + + +func stop(): + _orphan_count = max(0, _orphans() - _initial_count) + + +func _orphans() -> int: + return Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) as int + + +func orphan_nodes() -> int: + return _orphan_count if _orphan_detection_enabled else 0 diff --git a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs b/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs deleted file mode 100644 index 9ef6caa4..00000000 --- a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs +++ /dev/null @@ -1,5 +0,0 @@ - -// GdUnit3 c# API wrapper -public partial class GdUnit3MonoAPI : GdUnit3.GdUnit3MonoAPI -{ -} diff --git a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd b/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd deleted file mode 100644 index 429e4843..00000000 --- a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd +++ /dev/null @@ -1,41 +0,0 @@ -extends RefCounted -class_name GdUnit3MonoAPI - -const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") - - -static func instance() : - return null#GdUnitSingleton.get_or_create_singleton("GdUnit3MonoAPI", "res://addons/gdUnit4/src/mono/GdUnit3MonoAPI.cs") - -static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> Result: - if not GdUnitTools.is_mono_supported(): - return Result.error("Can't create test suite. No c# support found.") - var result := instance().CreateTestSuite(source_path, line_number, test_suite_path) as Dictionary - if result.has("error"): - return Result.error(result.get("error")) - return Result.success(result) - -static func is_test_suite(resource_path :String) -> bool: - if not is_csharp_file(resource_path) or not GdUnitTools.is_mono_supported(): - return false - if resource_path.is_empty(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. Missing resource path.") - return false - return instance().IsTestSuite(resource_path) - -static func parse_test_suite(source_path :String) -> Node: - if not GdUnitTools.is_mono_supported(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. No c# support found.") - return null - return instance().ParseTestSuite(source_path) - -static func create_executor(listener :Node) -> Node: - if not GdUnitTools.is_mono_supported(): - return null - return instance().Executor(listener) - -static func is_csharp_file(resource_path :String) -> bool: - var ext := resource_path.get_extension() - return ext == "cs" and GdUnitTools.is_mono_supported() diff --git a/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs b/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs new file mode 100644 index 00000000..0563ef92 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4MonoApi.cs @@ -0,0 +1,17 @@ +using Godot; +using Godot.Collections; + +// GdUnit4 C# API wrapper +public partial class GdUnit4MonoApi : GdUnit4.GdUnit4MonoAPI +{ + public new string Version() => GdUnit4.GdUnit4MonoAPI.Version(); + + public new bool IsTestSuite(string classPath) => GdUnit4.GdUnit4MonoAPI.IsTestSuite(classPath); + + public new RefCounted Executor(Node listener) => (RefCounted)GdUnit4.GdUnit4MonoAPI.Executor(listener); + + public new GdUnit4.CsNode? ParseTestSuite(string classPath) => GdUnit4.GdUnit4MonoAPI.ParseTestSuite(classPath); + + public new Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => + GdUnit4.GdUnit4MonoAPI.CreateTestSuite(sourcePath, lineNumber, testSuitePath); +} diff --git a/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd b/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd new file mode 100644 index 00000000..4ad97a48 --- /dev/null +++ b/addons/gdUnit4/src/mono/GdUnit4MonoApiLoader.gd @@ -0,0 +1,64 @@ +extends RefCounted +class_name GdUnit4MonoApiLoader + + +static func instance() -> Object: + return GdUnitSingleton.instance("GdUnit4MonoAPI", func(): + if not GdUnit4MonoApiLoader.is_mono_supported(): + return null + var GdUnit4MonoApi = load("res://addons/gdUnit4/src/mono/GdUnit4MonoApi.cs") + return GdUnit4MonoApi.new() + ) + + +static func is_engine_version_supported(engine_version :int = Engine.get_version_info().hex) -> bool: + return engine_version >= 0x40100 + + +# test is Godot mono running +static func is_mono_supported() -> bool: + return ClassDB.class_exists("CSharpScript") and is_engine_version_supported() + + +static func version() -> String: + if not GdUnit4MonoApiLoader.is_mono_supported(): + return "unknown" + return instance().Version() + + +static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> GdUnitResult: + if not GdUnit4MonoApiLoader.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 + if result.has("error"): + return GdUnitResult.error(result.get("error")) + return GdUnitResult.success(result) + + +static func is_test_suite(resource_path :String) -> bool: + if not is_csharp_file(resource_path) or not GdUnit4MonoApiLoader.is_mono_supported(): + return false + if resource_path.is_empty(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. Missing resource path.") + return false + return instance().IsTestSuite(resource_path) + + +static func parse_test_suite(source_path :String) -> Node: + if not GdUnit4MonoApiLoader.is_mono_supported(): + if GdUnitSettings.is_report_push_errors(): + push_error("Can't create test suite. No c# support found.") + return null + return instance().ParseTestSuite(source_path) + + +static func create_executor(listener :Node) -> RefCounted: + if not GdUnit4MonoApiLoader.is_mono_supported(): + return null + return instance().Executor(listener) + + +static func is_csharp_file(resource_path :String) -> bool: + var ext := resource_path.get_extension() + return ext == "cs" and GdUnit4MonoApiLoader.is_mono_supported() diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd b/addons/gdUnit4/src/network/GdUnitTask.gd index 662900a4..c27fd982 100644 --- a/addons/gdUnit4/src/network/GdUnitTask.gd +++ b/addons/gdUnit4/src/network/GdUnitTask.gd @@ -16,7 +16,7 @@ func _init(task_name :String,instance :Object,func_name :String): func name() -> String: return _task_name -func execute(args :Array) -> Result: +func execute(args :Array) -> GdUnitResult: if args.is_empty(): return _fref.call() return _fref.callv(args) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd index 83c7c316..0b5ee646 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -32,19 +32,19 @@ func stop() -> void: _connected = false -func start(host :String, port :int) -> Result: +func start(host :String, port :int) -> GdUnitResult: _host = host _port = port if _connected: - return Result.warn("Client already connected ... %s:%d" % [_host, _port]) + return GdUnitResult.warn("Client already connected ... %s:%d" % [_host, _port]) # Connect client to server if _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: var err := _stream.connect_to_host(host, port) #prints("connect_to_host", host, port, err) if err != OK: - return Result.error("GdUnit3: Can't establish client, error code: %s" % err) - return Result.success("GdUnit3: Client connected checked port %d" % port) + return GdUnitResult.error("GdUnit3: Can't establish client, error code: %s" % err) + return GdUnitResult.success("GdUnit3: Client connected checked port %d" % port) func _process(_delta): diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index 29207e68..b3ecf59f 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -103,23 +103,23 @@ func _notification(what): stop() -func start() -> Result: +func start() -> GdUnitResult: var server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT var err := OK for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: err = _server.listen(server_port, "127.0.0.1") if err != OK: - prints("GdUnit3: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) + prints("GdUnit4: Can't establish server checked port: %d, Error: %s" % [server_port, error_string(err)]) server_port += 1 - prints("GdUnit3: Retry (%d) ..." % retry) + prints("GdUnit4: Retry (%d) ..." % retry) else: break if err != OK: if err == ERR_ALREADY_IN_USE: - return Result.error("GdUnit3: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) - return Result.error("GdUnit3: Can't establish server. Error: %s." % error_string(err)) + return GdUnitResult.error("GdUnit3: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) + return GdUnitResult.error("GdUnit3: Can't establish server. Error: %s." % error_string(err)) prints("GdUnit4: Test server successfully started checked port: %d" % server_port) - return Result.success(server_port) + return GdUnitResult.success(server_port) func stop() -> void: diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd index 11884620..d30a0696 100644 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd +++ b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd @@ -4,7 +4,8 @@ extends GdUnitResourceDto var _line_number :int = -1 var _test_case_names :PackedStringArray = [] -func serialize(test_case) -> Dictionary: + +func serialize(test_case :Object) -> Dictionary: var serialized := super.serialize(test_case) if test_case.has_method("line_number"): serialized["line_number"] = test_case.line_number() @@ -12,16 +13,21 @@ func serialize(test_case) -> Dictionary: serialized["line_number"] = test_case.get("LineNumber") if test_case.has_method("test_case_names"): serialized["test_case_names"] = test_case.test_case_names() + elif test_case.has_method("TestCaseNames"): + serialized["test_case_names"] = test_case.TestCaseNames() return serialized + func deserialize(data :Dictionary) -> GdUnitResourceDto: super.deserialize(data) _line_number = data.get("line_number", -1) _test_case_names = data.get("test_case_names", []) return self + func line_number() -> int: return _line_number + func test_case_names() -> PackedStringArray: return _test_case_names diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd b/addons/gdUnit4/src/report/GdUnitByPathReport.gd index ab0d233c..a0c47fa3 100644 --- a/addons/gdUnit4/src/report/GdUnitByPathReport.gd +++ b/addons/gdUnit4/src/report/GdUnitByPathReport.gd @@ -2,14 +2,14 @@ class_name GdUnitByPathReport extends GdUnitReportSummary -func _init(path :String, reports :Array[GdUnitReportSummary]): - _resource_path = path - _reports = reports +func _init(path_ :String, reports_ :Array[GdUnitReportSummary]): + _resource_path = path_ + _reports = reports_ -static func sort_reports_by_path(reports :Array[GdUnitReportSummary]) -> Dictionary: +static func sort_reports_by_path(reports_ :Array[GdUnitReportSummary]) -> Dictionary: var by_path := Dictionary() - for report in reports: + for report in reports_: var suite_path :String = report.path() var suite_report :Array[GdUnitReportSummary] = by_path.get(suite_path, [] as Array[GdUnitReportSummary]) suite_report.append(report) @@ -38,10 +38,10 @@ func write(report_dir :String) -> String: return output_path -static func apply_testsuite_reports(report_dir :String, template :String, reports :Array[GdUnitReportSummary]) -> String: +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array[GdUnitReportSummary]) -> String: var table_records := PackedStringArray() - for report in reports: + for report in reports_: var report_link = report.output_path(report_dir).replace(report_dir, "..") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd index 78d55b9a..41ea52a7 100644 --- a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd +++ b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd @@ -7,9 +7,9 @@ var _report_path :String var _iteration :int -func _init(path :String): - _iteration = GdUnitTools.find_last_path_index(path, REPORT_DIR_PREFIX) + 1 - _report_path = "%s/%s%d" % [path, REPORT_DIR_PREFIX, _iteration] +func _init(path_ :String): + _iteration = GdUnitTools.find_last_path_index(path_, REPORT_DIR_PREFIX) + 1 + _report_path = "%s/%s%d" % [path_, REPORT_DIR_PREFIX, _iteration] DirAccess.make_dir_recursive_absolute(_report_path) @@ -17,37 +17,37 @@ func add_testsuite_report(suite_report :GdUnitTestSuiteReport): _reports.append(suite_report) -func add_testcase_report(resource_path :String, suite_report :GdUnitTestCaseReport) -> void: +func add_testcase_report(resource_path_ :String, suite_report :GdUnitTestCaseReport) -> void: for report in _reports: - if report.resource_path() == resource_path: + if report.resource_path() == resource_path_: report.add_report(suite_report) 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: + 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_orphans(orphan_count) - report.set_reports(reports) - if is_skipped: - _skipped_count = skipped_count + if report.resource_path() == resource_path_: + report.set_duration(duration_) + report.set_failed(is_failed_, failed_count_) + report.set_orphans(orphan_count_) + report.set_reports(reports_) + if is_skipped_: + _skipped_count = skipped_count_ -func update_testcase_report(resource_path :String, test_report :GdUnitTestCaseReport): +func update_testcase_report(resource_path_ :String, test_report :GdUnitTestCaseReport): for report in _reports: - if report.resource_path() == resource_path: + if report.resource_path() == resource_path_: report.update(test_report) @@ -67,21 +67,21 @@ func delete_history(max_reports :int) -> int: return GdUnitTools.delete_path_index_lower_equals_than(_report_path.get_base_dir(), REPORT_DIR_PREFIX, _iteration-max_reports) -static func apply_path_reports(report_dir :String, template :String, reports :Array) -> String: - var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(reports) +func apply_path_reports(report_dir :String, template :String, reports_ :Array) -> String: + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(reports_) var table_records := PackedStringArray() var paths := path_report_mapping.keys() paths.sort() - for path in paths: - var report := GdUnitByPathReport.new(path, path_report_mapping.get(path)) + for path_ in paths: + var report := GdUnitByPathReport.new(path_, path_report_mapping.get(path_)) var report_link :String = report.write(report_dir).replace(report_dir, ".") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) -static func apply_testsuite_reports(report_dir :String, template :String, reports :Array) -> String: +func apply_testsuite_reports(report_dir :String, template :String, reports_ :Array) -> String: var table_records := PackedStringArray() - for report in reports: + for report in reports_: var report_link :String = report.write(report_dir).replace(report_dir, ".") table_records.append(report.create_record(report_link)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd index c4d04afa..68ac30fd 100644 --- a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd @@ -9,9 +9,9 @@ func _init( p_suite_name :String, test_name :String, is_error := false, - is_failed := false, + _is_failed := false, failed_count :int = 0, - orphan_count :int = 0, + orphan_count_ :int = 0, is_skipped := false, failure_reports :Array = [], p_duration :int = 0): @@ -21,7 +21,7 @@ func _init( _test_count = 1 _error_count = is_error _failure_count = failed_count - _orphan_count = orphan_count + _orphan_count = orphan_count_ _skipped_count = is_skipped _failure_reports = failure_reports _duration = p_duration diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd index 633936cd..ca4dc3fc 100644 --- a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd +++ b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd @@ -85,8 +85,8 @@ func set_failed(failed :bool, count :int) -> void: _failure_count += count -func set_reports(reports :Array) -> void: - _failure_reports = reports +func set_reports(reports_ :Array) -> void: + _failure_reports = reports_ func update(test_report :GdUnitTestCaseReport) -> void: diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd b/addons/gdUnit4/src/report/JUnitXmlReport.gd index 0fbe8603..65a708bb 100644 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd +++ b/addons/gdUnit4/src/report/JUnitXmlReport.gd @@ -78,7 +78,7 @@ func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: for index in suite_report.reports().size(): var report :GdUnitTestCaseReport = suite_report.reports()[index] test_cases.append( XmlElement.new("testcase")\ - .attribute(ATTR_NAME, report.name())\ + .attribute(ATTR_NAME, encode_xml(report.name()))\ .attribute(ATTR_CLASSNAME, report.suite_name())\ .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ .add_childs(build_reports(report))) @@ -135,5 +135,9 @@ static func to_time(duration :int) -> String: return "%4.03f" % (duration / 1000.0) +static func encode_xml(value :String) -> String: + return value.xml_escape(true) + + #static func to_ISO8601_datetime() -> String: #return "%04d-%02d-%02dT%02d:%02d:%02d" % [date["year"], date["month"], date["day"], date["hour"], date["minute"], date["second"]] diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd index 7539d9b5..b01350ae 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -2,11 +2,10 @@ class_name GdUnitSpyBuilder extends GdUnitClassDoubler const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const GdUnitMemoryPool = preload("res://addons/gdUnit4/src/core/GdUnitMemoryPool.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") -static func build(caller :Object, to_spy, debug_write = false) -> Object: - var memory_pool :GdUnitMemoryPool.POOL = caller.get_meta(GdUnitMemoryPool.META_PARAM) +static func build(to_spy, debug_write = false) -> Object: if GdObjects.is_singleton(to_spy): push_error("Spy on a Singleton is not allowed! '%s'" % to_spy.get_class()) return null @@ -18,10 +17,10 @@ static func build(caller :Object, to_spy, debug_write = false) -> Object: to_spy = load(to_spy) # spy checked PackedScene if GdObjects.is_scene(to_spy): - return spy_on_scene(to_spy.instantiate(), memory_pool, debug_write) + return spy_on_scene(to_spy.instantiate(), debug_write) # spy checked a scene instance if GdObjects.is_instance_scene(to_spy): - return spy_on_scene(to_spy, memory_pool, debug_write) + return spy_on_scene(to_spy, debug_write) var spy := spy_on_script(to_spy, [], debug_write) if spy == null: @@ -32,7 +31,7 @@ static func build(caller :Object, to_spy, debug_write = false) -> Object: 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 spy_instance.__exclude_method_call([ "_input", "_gui_input", "_input_event", "_unhandled_input"]) - return GdUnitMemoryPool.register_auto_free(spy_instance, memory_pool) + return register_auto_free(spy_instance) static func get_class_info(clazz :Variant) -> Dictionary: @@ -56,7 +55,7 @@ static func spy_on_script(instance, function_excludes :PackedStringArray, debug_ 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(GdUnitSpyImpl, class_info, instance) + var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance) lines += double_functions(instance, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) var spy := GDScript.new() @@ -74,7 +73,7 @@ static func spy_on_script(instance, function_excludes :PackedStringArray, debug_ return spy -static func spy_on_scene(scene :Node, memory_pool :GdUnitMemoryPool.POOL, debug_write) -> Object: +static func spy_on_scene(scene :Node, debug_write) -> Object: if scene.get_script() == null: if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't create a spy checked a scene without script '%s'" % scene.get_scene_file_path()) @@ -87,7 +86,7 @@ static func spy_on_scene(scene :Node, memory_pool :GdUnitMemoryPool.POOL, debug_ return null # replace original script whit spy scene.set_script(spy) - return GdUnitMemoryPool.register_auto_free(scene, memory_pool) + return register_auto_free(scene) const EXCLUDE_PROPERTIES_TO_COPY = ["script", "type"] @@ -107,3 +106,7 @@ static func copy_properties(source :Object, dest :Object) -> void: dest.set(property_name, ""); continue dest.set(property_name, property_value) + + +static func register_auto_free(obj :Variant) -> Variant: + return GdUnitThreadManager.get_current_context().get_execution_context().register_auto_free(obj) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd index e4b4b8d0..8b75a3e0 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -1,15 +1,19 @@ -# warnings-disable -# warning-ignore:unused_argument -class_name GdUnitSpyImpl const __INSTANCE_ID = "${instance_id}" +const __SOURCE_CLASS = "${source_class}" var __instance_delegator var __excluded_methods :PackedStringArray = [] -static func __instance(): - return GdUnitStaticDictionary.get_value(__INSTANCE_ID) +static func __instance() -> Variant: + return Engine.get_meta(__INSTANCE_ID) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + if Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) func __instance_id() -> String: @@ -18,14 +22,13 @@ func __instance_id() -> String: func __set_singleton(delegator): # store self need to mock static functions - GdUnitStaticDictionary.add_value(__INSTANCE_ID, self) + Engine.set_meta(__INSTANCE_ID, self) __instance_delegator = delegator - #assert(__self[0] != null, "Invalid mock") func __release_double(): # we need to release the self reference manually to prevent orphan nodes - GdUnitStaticDictionary.erase(__INSTANCE_ID) + Engine.remove_meta(__INSTANCE_ID) __instance_delegator = null diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd index 283666e0..f0a3814b 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -58,6 +58,13 @@ func init_statistics(event :GdUnitEvent) : _summary["total_count"] += event.total_count() +func reset_statistics() -> void: + for k in _statistics.keys(): + _statistics[k] = 0 + for k in _summary.keys(): + _summary[k] = 0 + + func update_statistics(event :GdUnitEvent) : _statistics["error_count"] += event.error_count() _statistics["failed_count"] += event.failed_count() @@ -87,7 +94,8 @@ func println_message(message :String, color :Color = _text_color, indent :int = func _on_gdunit_event(event :GdUnitEvent): match event.type(): GdUnitEvent.INIT: - _summary["total_count"] = 0 + reset_statistics() + 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) @@ -96,9 +104,14 @@ func _on_gdunit_event(event :GdUnitEvent): GdUnitEvent.TESTSUITE_BEFORE: init_statistics(event) print_message("Execute: ", Color.DODGER_BLUE) - println_message( event._suite_name, _engine_type_color) + println_message(event._suite_name, _engine_type_color) GdUnitEvent.TESTSUITE_AFTER: + update_statistics(event) + if not event.reports().is_empty(): + var report :GdUnitReport = event.reports().front() + println_message("\t" +event._suite_name, _engine_type_color) + println_message("line %d %s" % [report._line_number, report._message], _text_color, 2) if event.is_success(): print_message("[wave]PASSED[/wave]", Color.LIGHT_GREEN) else: diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index adb57f71..1eb0eba6 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -55,7 +55,7 @@ func _getEditorThemes(interface :EditorInterface) -> void: # Context menu registrations ---------------------------------------------------------------------- func add_file_system_dock_context_menu() -> void: - var is_test_suite := func is_visible(script :GDScript, is_test_suite :bool): + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): if script == null: return true return GdObjects.is_test_suite(script) == is_test_suite @@ -67,7 +67,7 @@ func add_file_system_dock_context_menu() -> void: func add_script_editor_context_menu(): - var is_test_suite := func is_visible(script :GDScript, is_test_suite :bool): + var is_test_suite := func is_visible(script :Script, is_test_suite :bool): return GdObjects.is_test_suite(script) == is_test_suite var menu :Array[GdUnitContextMenuItem] = [ GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd index 3cca2a46..efb403f0 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -59,7 +59,7 @@ func collect_testsuites(_menu_item :GdUnitContextMenuItem, file_tree :Tree) -> P var is_dir := DirAccess.dir_exists_absolute(resource_path) if is_dir: selected_test_suites.append(resource_path) - elif is_dir or file_type == "GDScript": + 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) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd index 24a9580d..edb5d80f 100644 --- a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd @@ -50,11 +50,11 @@ func shortcut() -> Shortcut: return GdUnitCommandHandler.instance().get_shortcut(command.shortcut) -func is_enabled(script :GDScript) -> bool: +func is_enabled(script :Script) -> bool: return command.is_enabled.call(script) -func is_visible(script :GDScript) -> bool: +func is_visible(script :Script) -> bool: return visible.call(script) diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd index 318c9441..cda0ca56 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -39,8 +39,8 @@ func has_editor_focus() -> bool: return Engine.get_main_loop().root.gui_get_focus_owner() == active_base_editor() -func on_script_changed(script): - if script is GDScript: +func on_script_changed(script :Script): + if script is Script: var popups :Array[Node] = GdObjects.find_nodes_by_class(active_editor(), "PopupMenu", true) for popup in popups: if not popup.about_to_popup.is_connected(on_context_menu_show): @@ -49,7 +49,7 @@ func on_script_changed(script): popup.id_pressed.connect(on_context_menu_pressed) -func on_context_menu_show(script :GDScript, context_menu :PopupMenu): +func on_context_menu_show(script :Script, context_menu :PopupMenu): #prints("on_context_menu_show", _context_menus.keys(), context_menu, self) context_menu.add_separator() var current_index := context_menu.get_item_count() diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd index 716f2b42..08c6e549 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -17,9 +17,9 @@ func _ready(): _failures.text = "0" _errors.text = "0" var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") - var editiorTheme := editor.get_editor_interface().get_base_control().theme - _button_failure_up.icon = editiorTheme.get_icon("ArrowUp", "EditorIcons") - _button_failure_down.icon = editiorTheme.get_icon("ArrowDown", "EditorIcons") + var editior_control := editor.get_editor_interface().get_base_control() + _button_failure_up.icon = GodotVersionFixures.get_icon(editior_control, "ArrowUp") + _button_failure_down.icon = GodotVersionFixures.get_icon(editior_control, "ArrowDown") func status_changed(errors :int, failed :int): diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd index 40b484ea..0072249f 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -38,15 +38,15 @@ func _ready(): func init_buttons() -> void: - var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") - var editiorTheme := editor.get_editor_interface().get_base_control().theme + var editor :EditorPlugin = EditorPlugin.new() + var editior_control := editor.get_editor_interface().get_base_control() _button_run_overall.icon = overall_icon_image _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() - _button_run.icon = editiorTheme.get_icon("Play", "EditorIcons") + _button_run.icon = GodotVersionFixures.get_icon(editior_control, "Play") _button_run_debug.icon = debug_icon_image - _button_stop.icon = editiorTheme.get_icon("Stop", "EditorIcons") - _tool_button.icon = editiorTheme.get_icon("Tools", "EditorIcons") - _button_wiki.icon = editiorTheme.get_icon("HelpSearch", "EditorIcons") + _button_stop.icon = GodotVersionFixures.get_icon(editior_control, "Stop") + _tool_button.icon = GodotVersionFixures.get_icon(editior_control, "Tools") + _button_wiki.icon = GodotVersionFixures.get_icon(editior_control, "HelpSearch") func init_shortcuts(command_handler :GdUnitCommandHandler) -> void: diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd index 890baac2..8dc7923e 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -151,7 +151,7 @@ func init_tree() -> void: _tree.allow_rmb_select = true _tree_root = _tree.create_item() # fix tree icon scaling - var scale_factor := _editor.get_editor_interface().get_editor_scale() if Engine.is_editor_hint() else 1 + var scale_factor := _editor.get_editor_interface().get_editor_scale() if Engine.is_editor_hint() else 1.0 _tree.set("theme_override_constants/icon_max_width", 16*scale_factor) @@ -208,6 +208,9 @@ func set_state_skipped(item :TreeItem) -> void: func set_state_warnings(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return item.set_meta(META_GDUNIT_STATE, STATE.WARNING) item.set_custom_color(0, Color.YELLOW) item.set_icon(0, ICON_TEST_SUCCESS) @@ -215,6 +218,9 @@ func set_state_warnings(item :TreeItem) -> void: func set_state_failed(item :TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item): + return item.set_meta(META_GDUNIT_STATE, STATE.FAILED) item.set_custom_color(0, Color.LIGHT_BLUE) item.set_icon(0, ICON_TEST_FAILED) @@ -251,12 +257,12 @@ func set_state_orphan(item :TreeItem, event: GdUnitEvent) -> void: item.set_meta(META_GDUNIT_ORPHAN, orphan_count) item.set_custom_color(0, Color.YELLOW) item.set_tooltip_text(0, "Total <%d> orphan nodes detected." % orphan_count) - if is_state_warning(item): - item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) + if is_state_error(item): + item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) elif is_state_failed(item): item.set_icon(0, ICON_TEST_FAILED_ORPHAN) - elif is_state_error(item): - item.set_icon(0, ICON_TEST_ERRORS_ORPHAN) + elif is_state_warning(item): + item.set_icon(0, ICON_TEST_SUCCESS_ORPHAN) func update_state(item: TreeItem, event :GdUnitEvent) -> void: @@ -265,12 +271,12 @@ func update_state(item: TreeItem, event :GdUnitEvent) -> void: else: if event.is_skipped(): set_state_skipped(item) - elif event.is_warning(): - set_state_warnings(item) - elif event.is_failed(): - set_state_failed(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) for report in event.reports(): add_report(item, report) set_state_orphan(item, event) diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd index 77d125ab..8d2f1fad 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -17,26 +17,34 @@ const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdate @onready var _properties_shortcuts :Node = %"shortcut-content" @onready var _properties_report :Node = %"report-content" @onready var _input_capture :GdUnitInputCapture = %GdUnitInputCapture +@onready var _property_error :Window = %"propertyError" var _font_size :float func _ready(): + # initialize for testing + if not Engine.is_editor_hint(): + GdUnitSettings.setup() GdUnit4Version.init_version_label(_version_label) _font_size = GdUnitFonts.init_fonts(_version_label) - self.title = "GdUnitSettings" - setup_common_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) - setup_common_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) - setup_common_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) - setup_common_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) + setup_properties(_properties_common, GdUnitSettings.COMMON_SETTINGS) + setup_properties(_properties_ui, GdUnitSettings.UI_SETTINGS) + setup_properties(_properties_report, GdUnitSettings.REPORT_SETTINGS) + setup_properties(_properties_shortcuts, GdUnitSettings.SHORTCUT_SETTINGS) await get_tree().process_frame - popup_centered_ratio(.75) + if not Engine.is_editor_hint(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_size(Vector2i(1600, 800)) + popup_centered_ratio(1) + else: + popup_centered_ratio(.75) func _sort_by_key(left :GdUnitProperty, right :GdUnitProperty) -> bool: return left.name() < right.name() -func setup_common_properties(properties_parent :Node, property_category) -> void: +func setup_properties(properties_parent :Node, property_category) -> void: var category_properties := GdUnitSettings.list_settings(property_category) # sort by key category_properties.sort_custom(_sort_by_key) @@ -151,10 +159,14 @@ func _to_human_readable(value :String) -> String: func _get_btn_icon(p_name :String) -> Texture2D: + if not Engine.is_editor_hint(): + var placeholder := PlaceholderTexture2D.new() + placeholder.size = Vector2(8,8) + return placeholder var editor :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") if editor: - var editiorTheme := editor.get_editor_interface().get_base_control().theme - return editiorTheme.get_icon(p_name, "EditorIcons") + var editior_control := editor.get_editor_interface().get_base_control() + return GodotVersionFixures.get_icon(editior_control, p_name) return null @@ -233,7 +245,14 @@ func _on_btn_property_reset_pressed(property: GdUnitProperty, input :Node, reset func _on_property_text_changed(new_value :Variant, property: GdUnitProperty, reset_btn :Button): property.set_value(new_value) reset_btn.disabled = property.value() == property.default() - GdUnitSettings.update_property(property) + var error :Variant = GdUnitSettings.update_property(property) + if error: + var label :Label = _property_error.get_child(0) as Label + label.set_text(error) + var control := gui_get_focus_owner() + _property_error.show() + if control != null: + _property_error.position = control.global_position + Vector2(self.position) + Vector2(40, 40) func _on_option_selected(index :int, property: GdUnitProperty, reset_btn :Button): @@ -271,4 +290,3 @@ func stop_progress() -> void: func update_progress(message :String) -> void: _progress_text.text = message _progress_bar.value += 1 - prints(message) diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn index f569f84b..0650fbac 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=6 format=3 uid="uid://dwgat6j2u77g4"] +[gd_scene load_steps=7 format=3 uid="uid://dwgat6j2u77g4"] [ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] [ext_resource type="Texture2D" uid="uid://c7sk0yhd52lg3" path="res://addons/gdUnit4/src/ui/assets/icon.png" id="2_w63lb"] @@ -6,11 +6,28 @@ [ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] [ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.172549, 0.113725, 0.141176, 1) +border_width_left = 4 +border_width_top = 4 +border_width_right = 4 +border_width_bottom = 4 +border_color = Color(0.87451, 0.0705882, 0.160784, 1) +border_blend = true +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +shadow_color = Color(0, 0, 0, 0.756863) +shadow_size = 10 +shadow_offset = Vector2(10, 10) + [node name="Control" type="Window"] disable_3d = true -gui_embed_subwindows = true +title = "GdUnitSettings" initial_position = 1 -size = Vector2i(384, 384) visible = false wrap_controls = true transient = true @@ -186,7 +203,7 @@ layout_mode = 2 [node name="common-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Common"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(1431, 0) +custom_minimum_size = Vector2(1445, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -210,7 +227,7 @@ layout_mode = 2 [node name="shortcut-content" type="VBoxContainer" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts"] unique_name_in_owner = true clip_contents = true -custom_minimum_size = Vector2(941, 0) +custom_minimum_size = Vector2(983, 0) layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 @@ -218,7 +235,7 @@ size_flags_vertical = 3 [node name="GdUnitInputCapture" parent="Panel/v/MarginContainer/GridContainer/Properties/Shortcuts/shortcut-content" instance=ExtResource("5_xu3j8")] unique_name_in_owner = true visible = false -modulate = Color(0.543351, 0.543351, 0.543351, 0.589016) +modulate = Color(0.000201742, 0.000201742, 0.000201742, 0.100182) z_index = 1 z_as_relative = false layout_mode = 2 @@ -241,6 +258,21 @@ size_flags_vertical = 3 visible = false layout_mode = 2 +[node name="propertyError" type="PopupPanel" parent="Panel/v/MarginContainer/GridContainer/Properties"] +unique_name_in_owner = true +initial_position = 1 +size = Vector2i(400, 100) +theme_override_styles/panel = SubResource("StyleBoxFlat_hbbq5") + +[node name="Label" type="Label" parent="Panel/v/MarginContainer/GridContainer/Properties/propertyError"] +offset_left = 10.0 +offset_top = 4.0 +offset_right = 390.0 +offset_bottom = 96.0 +theme_override_colors/font_color = Color(0.858824, 0, 0.109804, 1) +horizontal_alignment = 1 +vertical_alignment = 1 + [node name="MarginContainer2" type="MarginContainer" parent="Panel/v"] layout_mode = 2 size_flags_horizontal = 3 diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd index a98e2732..fb3af570 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd @@ -24,7 +24,8 @@ var _download_zip_url :String func _ready(): - _editor_interface = Engine.get_meta("GdUnitEditorPlugin").get_editor_interface() + var plugin :EditorPlugin = Engine.get_meta("GdUnitEditorPlugin") + _editor_interface = plugin.get_editor_interface() _update_button.disabled = true _md_reader.set_http_client(_update_client) GdUnitFonts.init_fonts(_content) diff --git a/data.pandora b/data.pandora index c5f8419a..a5d99f7c 100644 --- a/data.pandora +++ b/data.pandora @@ -140,7 +140,7 @@ "_id": "54", "_index": 0, "_name": "Mock (Required for Testing!)", - "_script_path": "res://test/mock/custom_mock_entity.gd" + "_script_path": "res://mock/custom_mock_entity.gd" }, { "_category_id": "", diff --git a/examples/inventory/world/world_item.gd b/examples/inventory/world/world_item.gd index b331c22a..c92d40e2 100644 --- a/examples/inventory/world/world_item.gd +++ b/examples/inventory/world/world_item.gd @@ -12,7 +12,7 @@ extends Node2D @onready var sprite = $Sprite2D -var item_instance:ItemInstance: +var item_instance:Item: set(ii): item_instance = ii if sprite != null: diff --git a/test/mock/custom_mock_entity.gd b/mock/custom_mock_entity.gd similarity index 100% rename from test/mock/custom_mock_entity.gd rename to mock/custom_mock_entity.gd diff --git a/test/mock/custom_mock_entity_alternative.gd b/mock/custom_mock_entity_alternative.gd similarity index 100% rename from test/mock/custom_mock_entity_alternative.gd rename to mock/custom_mock_entity_alternative.gd diff --git a/test/mock/entity-compliation-error.gd b/mock/entity-compliation-error.gd similarity index 100% rename from test/mock/entity-compliation-error.gd rename to mock/entity-compliation-error.gd diff --git a/test/mock/entity-wrong-init.gd b/mock/entity-wrong-init.gd similarity index 100% rename from test/mock/entity-wrong-init.gd rename to mock/entity-wrong-init.gd diff --git a/test/mock/mock_scene.gd b/mock/mock_scene.gd similarity index 100% rename from test/mock/mock_scene.gd rename to mock/mock_scene.gd diff --git a/test/mock/mock_scene.tscn b/mock/mock_scene.tscn similarity index 61% rename from test/mock/mock_scene.tscn rename to mock/mock_scene.tscn index 76f41ba0..54537300 100644 --- a/test/mock/mock_scene.tscn +++ b/mock/mock_scene.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://c3j2xs0rnqdst"] -[ext_resource type="Script" path="res://test/mock/mock_scene.gd" id="1_m4lrk"] -[ext_resource type="Script" path="res://test/mock/custom_mock_entity.gd" id="2_mijc7"] +[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"] [sub_resource type="Resource" id="Resource_6ucbm"] script = ExtResource("2_mijc7") diff --git a/test/mock/non-entity.gd b/mock/non-entity.gd similarity index 100% rename from test/mock/non-entity.gd rename to mock/non-entity.gd diff --git a/project.godot b/project.godot index 7c6a419e..2a2ee5d2 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.1", "Forward Plus") +config/features=PackedStringArray("4.2", "Forward Plus") boot_splash/image="res://splash.png" config/icon="res://addons/pandora/icons/icon.png" diff --git a/test/backend/entity_backend_test.gd b/test/backend/entity_backend_test.gd index 120fbdfa..c5fd2c91 100644 --- a/test/backend/entity_backend_test.gd +++ b/test/backend/entity_backend_test.gd @@ -7,8 +7,8 @@ extends GdUnitTestSuite # TestSuite generated from const __source = "res://addons/pandora/backend/entity_backend.gd" -const MOCK_ENTITY_PATH = "res://test/mock/custom_mock_entity.gd" -const MOCK_ENTITY_ALT_PATH = "res://test/mock/custom_mock_entity_alternative.gd" +const MOCK_ENTITY_PATH = "res://mock/custom_mock_entity.gd" +const MOCK_ENTITY_ALT_PATH = "res://mock/custom_mock_entity_alternative.gd" var _pandora_backend:PandoraEntityBackend @@ -582,7 +582,7 @@ func test_saveload_invalid_entity() -> void: func test_saveload_non_entity() -> void: var backend = create_object_backend() as PandoraEntityBackend var category = backend.create_category("root") - category.set_script_path("res://test/mock/non-entity.gd") + category.set_script_path("res://mock/non-entity.gd") var entity_id = backend.create_entity("root", category).get_entity_id() var data = backend.save_data() backend.load_data(data) @@ -594,7 +594,7 @@ func test_saveload_non_entity() -> void: func test_saveload_wrong_init() -> void: var backend = create_object_backend() as PandoraEntityBackend var category = backend.create_category("root") - category.set_script_path("res://test/mock/entity-wrong-init.gd") + category.set_script_path("res://mock/entity-wrong-init.gd") var entity_id = backend.create_entity("root", category).get_entity_id() var data = backend.save_data() backend.load_data(data) @@ -606,7 +606,7 @@ func test_saveload_wrong_init() -> void: func test_saveload_compilation_error_on_script() -> void: var backend = create_object_backend() as PandoraEntityBackend var category = backend.create_category("root") - category.set_script_path("res://test/mock/entity-compilation-error.gd") + category.set_script_path("res://mock/entity-compilation-error.gd") var entity_id = backend.create_entity("root", category).get_entity_id() var data = backend.save_data() backend.load_data(data) diff --git a/test/scene/mock_scene_test.gd b/test/scene/mock_scene_test.gd index 001bb4e0..1d0582b8 100644 --- a/test/scene/mock_scene_test.gd +++ b/test/scene/mock_scene_test.gd @@ -5,7 +5,7 @@ extends GdUnitTestSuite # TestSuite generated from -const __source = "res://test/mock/mock_scene.tscn" +const __source = "res://mock/mock_scene.tscn" func test_instantiate_mock_data_via_scene() -> void: diff --git a/test/scene_test.gd b/test/scene_test.gd index 713bc10c..65e7e4fd 100644 --- a/test/scene_test.gd +++ b/test/scene_test.gd @@ -6,9 +6,9 @@ extends GdUnitTestSuite # TestSuite generated from -const __source = "res://test/mock/mock_scene.tscn" +const __source = "res://mock/mock_scene.tscn" const TEST_DIR = "testdata" -const CUSTOM_ENTITY_PATH = "res://test/mock/custom_mock_entity.gd" +const CUSTOM_ENTITY_PATH = "res://mock/custom_mock_entity.gd" func before() -> void: Pandora.set_context_id(TEST_DIR)