diff --git a/addons/beehave/plugin.gd b/addons/beehave/plugin.gd index 55b1248c..71130fce 100644 --- a/addons/beehave/plugin.gd +++ b/addons/beehave/plugin.gd @@ -7,8 +7,8 @@ var frames: RefCounted func _init(): name = "BeehavePlugin" - add_autoload_singleton("BeehaveGlobalMetrics", "./metrics/beehave_global_metrics.gd") - add_autoload_singleton("BeehaveGlobalDebugger", "./debug/global_debugger.gd") + add_autoload_singleton("BeehaveGlobalMetrics", "metrics/beehave_global_metrics.gd") + add_autoload_singleton("BeehaveGlobalDebugger", "debug/global_debugger.gd") print("Beehave initialized!") @@ -20,5 +20,3 @@ func _enter_tree() -> void: func _exit_tree() -> void: remove_debugger_plugin(editor_debugger) - editor_debugger.free() - frames.free() diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index a32cc032..9871fba7 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -1,6 +1,8 @@ #!/usr/bin/env -S godot -s extends SceneTree +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + #warning-ignore-all:return_value_discarded class CLIRunner extends Node: @@ -21,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), @@ -48,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): @@ -76,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 @@ -88,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) @@ -397,9 +398,8 @@ func _initialize(): func _finalize(): prints("Finallize ..") - _cli_runner.free() + if is_instance_valid(_cli_runner): + _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 9b6f6696..01495eef 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -1,6 +1,8 @@ #!/usr/bin/env -S godot -s extends MainLoop +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const NO_LOG_TEMPLATE = """ diff --git a/addons/gdUnit4/bin/ProjectScanner.gd b/addons/gdUnit4/bin/ProjectScanner.gd index 75070b29..dcddf67e 100644 --- a/addons/gdUnit4/bin/ProjectScanner.gd +++ b/addons/gdUnit4/bin/ProjectScanner.gd @@ -4,28 +4,33 @@ 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 _process(_delta): + if not is_instance_valid(scanner): + quit(0) + + 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,36 +51,43 @@ 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() await get_tree().process_frame - get_tree().quit(0) + queue_free() func scan_project() -> void: 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 + plugin.queue_free() diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg index 133bd07d..56c4c0f6 100644 --- a/addons/gdUnit4/plugin.cfg +++ b/addons/gdUnit4/plugin.cfg @@ -3,5 +3,5 @@ name="gdUnit4" description="Unit Testing Framework for Godot Scripts" author="Mike Schulze" -version="4.1.4" +version="4.2.0" script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index e533d079..065e0318 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -1,24 +1,13 @@ @tool extends EditorPlugin +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + var _gd_inspector :Node var _server_node var _gd_console :Node -# removes GdUnit classes inherits from Godot.Node from the node inspecor, ohterwise it takes very long to popup the dialog -func _fixup_node_inspector() -> void: - var classes := PackedStringArray([ - "GdUnitTestSuite", - "_TestCase", - "GdUnitInspecor", - "GdUnitExecutor", - "GdUnitTcpClient", - "GdUnitTcpServer"]) - for clazz in classes: - remove_custom_type(clazz) - - func _enter_tree(): Engine.set_meta("GdUnitEditorPlugin", self) GdUnitSettings.setup() @@ -30,11 +19,12 @@ func _enter_tree(): add_control_to_bottom_panel(_gd_console, "gdUnitConsole") _server_node = load("res://addons/gdUnit4/src/network/GdUnitServer.tscn").instantiate() add_child(_server_node) - _fixup_node_inspector() prints("Loading GdUnit4 Plugin success") 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/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd index 1674d26c..04ba5263 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -3,6 +3,32 @@ class_name GdUnitAssert extends RefCounted +# Scans the current stack trace for the root cause to extract the line number +static func _get_line_number() -> int: + var stack_trace := get_stack() + if stack_trace == null or stack_trace.is_empty(): + return -1 + for stack_info in stack_trace: + var function :String = stack_info.get("function") + # we catch helper asserts to skip over to return the correct line number + if function.begins_with("assert_"): + continue + if function.begins_with("test_"): + return stack_info.get("line") + var source :String = stack_info.get("source") + if source.is_empty() \ + or source.begins_with("user://") \ + or source.ends_with("GdUnitAssert.gd") \ + or source.ends_with("AssertImpl.gd") \ + or source.ends_with("GdUnitTestSuite.gd") \ + or source.ends_with("GdUnitSceneRunnerImpl.gd") \ + or source.ends_with("GdUnitObjectInteractions.gd") \ + or source.ends_with("GdUnitAwaiter.gd"): + continue + return stack_info.get("line") + return -1 + + ## Verifies that the current value is null. func is_null(): return self diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd index 99bf7732..c1fc2ccf 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd @@ -1,14 +1,21 @@ class_name GdUnitAwaiter extends RefCounted +const GdUnitAssertImpl = preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + # Waits for a specified signal in an interval of 50ms sent from the , and terminates with an error after the specified timeout has elapsed. # source: the object from which the signal is emitted # 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: - var line_number := GdUnitAssertImpl._get_line_number() +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): + GdUnitAssertImpl.new(signal_name)\ + .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number) + return await Engine.get_main_loop().process_frame # fail fast if the given source instance invalid if not is_instance_valid(source): GdUnitAssertImpl.new(signal_name)\ @@ -27,8 +34,8 @@ 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: - var line_number := GdUnitAssertImpl._get_line_number() +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): GdUnitAssertImpl.new(signal_name)\ @@ -47,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 c7a60a59..30734918 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -3,23 +3,56 @@ ## You have to extend and implement your test cases as described[br] ## e.g MyTests.gd [br] ## [codeblock] -## extends GdUnitTestSuite -## # testcase -## func test_testCaseA(): -## assert_that("value").is_equal("value") -## [/codeblock][br] +## extends GdUnitTestSuite +## # testcase +## func test_case_a(): +## assert_that("value").is_equal("value") +## [/codeblock] ## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/ @icon("res://addons/gdUnit4/src/ui/assets/TestSuite.svg") class_name GdUnitTestSuite extends Node - -const NO_ARG = GdUnitConstants.NO_ARG +const NO_ARG :Variant = GdUnitConstants.NO_ARG ### internal runtime variables that must not be overwritten!!! +@warning_ignore("unused_private_class_variable") var __is_skipped := false +@warning_ignore("unused_private_class_variable") var __skip_reason :String = "Unknow." +var __active_test_case :String +var __awaiter := __gdunit_awaiter() +# holds the actual execution context +var __execution_context + + +### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" +### in order to noticeably reduce the loading time of the test suite. +# We go this hard way to increase the loading performance to avoid reparsing all the used scripts +# for more detailed info -> https://github.com/godotengine/godot/issues/67400 +func __lazy_load(script_path :String) -> GDScript: + return GdUnitAssertions.__lazy_load(script_path) + + +func __gdunit_assert() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") + + +func __gdunit_tools() -> GDScript: + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func __gdunit_awaiter() -> Object: + return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new() + + +func __gdunit_argument_matchers(): + return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd") + + +func __gdunit_object_interactions(): + return __lazy_load("res://addons/gdUnit4/src/core/GdUnitObjectInteractions.gd") ## This function is called before a test suite starts[br] @@ -50,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 @@ -59,60 +91,67 @@ func set_active_test_case(test_case :String) -> void: # Mapps Godot error number to a readable error message. See at ERROR # https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error func error_as_string(error_number :int) -> String: - return GdUnitTools.error_as_string(error_number) + return __gdunit_tools().error_as_string(error_number) ## A litle helper to auto freeing your created objects after test execution func auto_free(obj) -> Variant: - 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] ## By default, an interrupted test is reported as an error.[br] ## This function allows you to change the message to Success when an interrupted error is reported. func discard_error_interupted_by_timeout() -> void: - GdUnitTools.register_expect_interupted_by_timeout(self, __active_test_case) + __gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case) ## Creates a new directory under the temporary directory *user://tmp*[br] ## Useful for storing data during test execution. [br] ## The directory is automatically deleted after test suite execution func create_temp_dir(relative_path :String) -> String: - return GdUnitTools.create_temp_dir(relative_path) + return __gdunit_tools().create_temp_dir(relative_path) ## Deletes the temporary base directory[br] ## Is called automatically after each execution of the test suite func clean_temp_dir(): - GdUnitTools.clear_tmp() + __gdunit_tools().clear_tmp() ## Creates a new file under the temporary directory *user://tmp* + [br] ## with given name and given file (default = File.WRITE)[br] ## If success the returned File is automatically closed after the execution of the test suite func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: - return GdUnitTools.create_temp_file(relative_path, file_name, mode) + return __gdunit_tools().create_temp_file(relative_path, file_name, mode) ## Reads a resource by given path into a PackedStringArray. func resource_as_array(resource_path :String) -> PackedStringArray: - return GdUnitTools.resource_as_array(resource_path) + return __gdunit_tools().resource_as_array(resource_path) ## Reads a resource by given path and returned the content as String. func resource_as_string(resource_path :String) -> String: - return GdUnitTools.resource_as_string(resource_path) + return __gdunit_tools().resource_as_string(resource_path) ## Reads a resource by given path and return Variand translated by str_to_var func resource_as_var(resource_path :String): - return str_to_var(GdUnitTools.resource_as_string(resource_path)) + return str_to_var(__gdunit_tools().resource_as_string(resource_path)) ## clears the debuger error list[br] ## PROTOTYPE!!!! Don't use it for now func clear_push_errors() -> void: - GdUnitTools.clear_push_errors() + __gdunit_tools().clear_push_errors() ## Waits for given signal is emited by the until a specified timeout to fail[br] @@ -121,17 +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: - # fail fast if the given source instance invalid - if not is_instance_valid(source): - GdUnitAssertImpl.new(signal_name)\ - .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), GdUnitAssertImpl._get_line_number()) - return await GdUnitAwaiter.await_idle_frame() - return await GdUnitAwaiter.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 GdUnitAwaiter.await_idle_frame() + await __awaiter.await_idle_frame() ## Waits for for a given amount of milliseconds[br] @@ -142,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 GdUnitAwaiter.await_millis(timeout) + await __awaiter.await_millis(timeout) ## Creates a new scene runner to allow simulate interactions checked a scene.[br] @@ -157,7 +191,7 @@ func await_millis(timeout :int): ## var runner := scene_runner("res://foo/my_scne.tscn") ## [/codeblock] func scene_runner(scene, verbose := false) -> GdUnitSceneRunner: - return auto_free(GdUnitSceneRunnerImpl.new(scene, verbose)) + return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose)) # === Mocking & Spy =========================================================== @@ -173,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 GdUnitMockBuilder.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): - return GdUnitSpyBuilder.build(self, instance) +func spy(instance) -> Object: + return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance) ## Configures a return value for the specified function and used arguments.[br] @@ -193,22 +227,22 @@ func do_return(value) -> GdUnitMock: ## Verifies certain behavior happened at least once or exact number of times func verify(obj, times := 1): - return GdUnitObjectInteractions.verify(obj, times) + return __gdunit_object_interactions().verify(obj, times) ## Verifies no interactions is happen checked this mock or spy func verify_no_interactions(obj) -> GdUnitAssert: - return GdUnitObjectInteractions.verify_no_interactions(obj) + return __gdunit_object_interactions().verify_no_interactions(obj) ## Verifies the given mock or spy has any unverified interaction. func verify_no_more_interactions(obj) -> GdUnitAssert: - return GdUnitObjectInteractions.verify_no_more_interactions(obj) + return __gdunit_object_interactions().verify_no_more_interactions(obj) ## Resets the saved function call counters checked a mock or spy func reset(obj) -> void: - GdUnitObjectInteractions.reset(obj) + __gdunit_object_interactions().reset(obj) ## Starts monitoring the specified source to collect all transmitted signals.[br] @@ -224,45 +258,47 @@ func reset(obj) -> void: ## await assert_signal(emitter).is_emitted('my_signal') ## [/codeblock] func monitor_signals(source :Object, _auto_free := true) -> Object: - var signal_collector := GdUnitThreadManager.get_current_context().get_signal_collector() - signal_collector.register_emitter(source) + __lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\ + .get_current_context()\ + .get_signal_collector()\ + .register_emitter(source) return auto_free(source) if _auto_free else source # === Argument matchers ======================================================== ## Argument matcher to match any argument func any() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.any() + return __gdunit_argument_matchers().any() ## Argument matcher to match any boolean value func any_bool() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_BOOL) + return __gdunit_argument_matchers().by_type(TYPE_BOOL) ## Argument matcher to match any integer value func any_int() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_INT) + return __gdunit_argument_matchers().by_type(TYPE_INT) ## Argument matcher to match any float value func any_float() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_FLOAT) + return __gdunit_argument_matchers().by_type(TYPE_FLOAT) ## Argument matcher to match any string value func any_string() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_STRING) + return __gdunit_argument_matchers().by_type(TYPE_STRING) ## Argument matcher to match any Color value func any_color() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_COLOR) + return __gdunit_argument_matchers().by_type(TYPE_COLOR) ## Argument matcher to match any Vector typed value func any_vector() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_types([ + return __gdunit_argument_matchers().by_types([ TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, @@ -274,142 +310,151 @@ func any_vector() -> GdUnitArgumentMatcher: ## Argument matcher to match any Vector2 value func any_vector2() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR2) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2) ## Argument matcher to match any Vector2i value func any_vector2i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR2I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I) ## Argument matcher to match any Vector3 value func any_vector3() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR3) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3) ## Argument matcher to match any Vector3i value func any_vector3i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR3I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I) ## Argument matcher to match any Vector4 value func any_vector4() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR4) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4) ## Argument matcher to match any Vector3i value func any_vector4i() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_VECTOR4I) + return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I) ## Argument matcher to match any Rect2 value func any_rect2() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_RECT2) + return __gdunit_argument_matchers().by_type(TYPE_RECT2) ## Argument matcher to match any Plane value func any_plane() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PLANE) + return __gdunit_argument_matchers().by_type(TYPE_PLANE) ## Argument matcher to match any Quaternion value func any_quat() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_QUATERNION) + return __gdunit_argument_matchers().by_type(TYPE_QUATERNION) ## Argument matcher to match any AABB value func any_aabb() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_AABB) + return __gdunit_argument_matchers().by_type(TYPE_AABB) ## Argument matcher to match any Basis value func any_basis() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_BASIS) + return __gdunit_argument_matchers().by_type(TYPE_BASIS) ## Argument matcher to match any Transform3D value func any_transform() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_TRANSFORM3D) + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D) ## Argument matcher to match any Transform2D value func any_transform_2d() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_TRANSFORM2D) + return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D) ## Argument matcher to match any NodePath value func any_node_path() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_NODE_PATH) + return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH) ## Argument matcher to match any RID value func any_rid() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_RID) + return __gdunit_argument_matchers().by_type(TYPE_RID) ## Argument matcher to match any Object value func any_object() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_OBJECT) + return __gdunit_argument_matchers().by_type(TYPE_OBJECT) ## Argument matcher to match any Dictionary value func any_dictionary() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_DICTIONARY) + return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY) ## Argument matcher to match any Array value func any_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_ARRAY) ## Argument matcher to match any PackedByteArray value func any_pool_byte_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_BYTE_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY) ## Argument matcher to match any PackedInt32Array value func any_pool_int_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_INT32_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY) ## Argument matcher to match any PackedFloat32Array value func any_pool_float_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_FLOAT32_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY) ## Argument matcher to match any PackedStringArray value func any_pool_string_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_STRING_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY) ## Argument matcher to match any PackedVector2Array value func any_pool_vector2_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_VECTOR2_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY) ## Argument matcher to match any PackedVector3Array value func any_pool_vector3_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_VECTOR3_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY) ## Argument matcher to match any PackedColorArray value func any_pool_color_array() -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.by_type(TYPE_PACKED_COLOR_ARRAY) + return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY) ## Argument matcher to match any instance of given class func any_class(clazz :Object) -> GdUnitArgumentMatcher: - return GdUnitArgumentMatchers.any_class(clazz) + return __gdunit_argument_matchers().any_class(clazz) # === value extract utils ====================================================== ## Builds an extractor by given function name and optional arguments func extr(func_name :String, args := Array()) -> GdUnitValueExtractor: - return GdUnitFuncValueExtractor.new(func_name, args) + return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args) ## Constructs a tuple by given arguments -func tuple(arg0, arg1=GdUnitTuple.NO_ARG, arg2=GdUnitTuple.NO_ARG, arg3=GdUnitTuple.NO_ARG, arg4=GdUnitTuple.NO_ARG, arg5=GdUnitTuple.NO_ARG, arg6=GdUnitTuple.NO_ARG, arg7=GdUnitTuple.NO_ARG, arg8=GdUnitTuple.NO_ARG, arg9=GdUnitTuple.NO_ARG) -> GdUnitTuple: +func tuple(arg0 :Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG) -> GdUnitTuple: return GdUnitTuple.new(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) @@ -418,10 +463,6 @@ func tuple(arg0, arg1=GdUnitTuple.NO_ARG, arg2=GdUnitTuple.NO_ARG, arg3=GdUnitTu ## The common assertion tool to verify values. ## It checks the given value by type to fit to the best assert func assert_that(current) -> GdUnitAssert: - - if GdArrayTools.is_array_type(current): - return assert_array(current) - match typeof(current): TYPE_BOOL: return assert_bool(current) @@ -435,32 +476,34 @@ func assert_that(current) -> GdUnitAssert: return assert_vector(current) TYPE_DICTIONARY: return assert_dict(current) - TYPE_ARRAY: + TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\ + TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\ + TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY: return assert_array(current) TYPE_OBJECT, TYPE_NIL: return assert_object(current) _: - return GdUnitAssertImpl.new(current) + return __gdunit_assert().new(current) ## An assertion tool to verify boolean values. func assert_bool(current) -> GdUnitBoolAssert: - return GdUnitBoolAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current) ## An assertion tool to verify String values. func assert_str(current) -> GdUnitStringAssert: - return GdUnitStringAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current) ## An assertion tool to verify integer values. func assert_int(current) -> GdUnitIntAssert: - return GdUnitIntAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current) ## An assertion tool to verify float values. func assert_float(current) -> GdUnitFloatAssert: - return GdUnitFloatAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current) ## An assertion tool to verify Vector values.[br] @@ -470,55 +513,41 @@ func assert_float(current) -> GdUnitFloatAssert: ## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001)) ## [/codeblock] func assert_vector(current) -> GdUnitVectorAssert: - return GdUnitVectorAssertImpl.new(current) - - -## An assertion tool to verify Vector2 values.[br] -## This function is [b]deprecated[/b] you have to use [method assert_vector] instead -func assert_vector2(current) -> GdUnitVectorAssert: - push_warning("assert_vector2 is deprecated, Use 'assert_vector' instead.") - return assert_vector(current) - - -## An assertion tool to verify Vector3 values.[br] -## This function is [b]deprecated[/b] you have to use [method assert_vector] instead -func assert_vector3(current) -> GdUnitVectorAssert: - push_warning("assert_vector3 is deprecated, Use 'assert_vector' instead.") - return assert_vector(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current) ## An assertion tool to verify arrays. func assert_array(current) -> GdUnitArrayAssert: - return GdUnitArrayAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current) ## An assertion tool to verify dictionaries. func assert_dict(current) -> GdUnitDictionaryAssert: - return GdUnitDictionaryAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current) ## An assertion tool to verify FileAccess. func assert_file(current) -> GdUnitFileAssert: - return GdUnitFileAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current) ## An assertion tool to verify Objects. func assert_object(current) -> GdUnitObjectAssert: - return GdUnitObjectAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current) func assert_result(current) -> GdUnitResultAssert: - return GdUnitResultAssertImpl.new(current) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current) ## An assertion tool that waits until a certain time for an expected function return value func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnitFuncAssert: - return GdUnitFuncAssertImpl.new(instance, func_name, args) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args) ## An Assertion Tool to verify for emitted signals until a certain time. func assert_signal(instance :Object) -> GdUnitSignalAssert: - return GdUnitSignalAssertImpl.new(instance) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance) ## An assertion tool to test for failing assertions.[br] @@ -529,7 +558,7 @@ func assert_signal(instance :Object) -> GdUnitSignalAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure(assertion :Callable) -> GdUnitFailureAssert: - return GdUnitFailureAssertImpl.new().execute(assertion) + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion) ## An assertion tool to test for failing assertions.[br] @@ -540,7 +569,7 @@ func assert_failure(assertion :Callable) -> GdUnitFailureAssert: ## .has_message("Expecting:\n 'true'\n not equal to\n 'true'") ## [/codeblock] func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: - return await GdUnitFailureAssertImpl.new().execute_and_await(assertion) + return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion) ## An assertion tool to verify for Godot errors.[br] @@ -556,26 +585,15 @@ func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: ## .is_push_error('test error') ## [/codeblock] func assert_error(current :Callable) -> GdUnitGodotErrorAssert: - return GdUnitGodotErrorAssertImpl.new(current) - - -## Utility to check if a test has failed in a particular line and if there is an error message -func assert_failed_at(line_number :int, expected_failure :String) -> bool: - var is_failed = is_failure() - var last_failure = GdAssertReports.current_failure() - var last_failure_line = GdAssertReports.get_last_error_line_number() - assert_str(last_failure).is_equal(expected_failure) - assert_int(last_failure_line).is_equal(line_number) - GdAssertReports.expect_fail(true) - return is_failed + return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current) func assert_not_yet_implemented(): - GdUnitAssertImpl.new(null).test_fail() + __gdunit_assert().new(null).test_fail() func fail(message :String): - GdUnitAssertImpl.new(null).report_error(message) + __gdunit_assert().new(null).report_error(message) # --- internal stuff do not override!!! diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd index d0ec5524..a6e3c41b 100644 --- a/addons/gdUnit4/src/GdUnitTuple.gd +++ b/addons/gdUnit4/src/GdUnitTuple.gd @@ -7,7 +7,16 @@ const NO_ARG :Variant = GdUnitConstants.NO_ARG var __values :Array = Array() -func _init(arg0,arg1,arg2=NO_ARG,arg3=NO_ARG,arg4=NO_ARG,arg5=NO_ARG,arg6=NO_ARG,arg7=NO_ARG,arg8=NO_ARG,arg9=NO_ARG): +func _init(arg0:Variant, + arg1 :Variant=NO_ARG, + arg2 :Variant=NO_ARG, + arg3 :Variant=NO_ARG, + arg4 :Variant=NO_ARG, + arg5 :Variant=NO_ARG, + arg6 :Variant=NO_ARG, + arg7 :Variant=NO_ARG, + arg8 :Variant=NO_ARG, + arg9 :Variant=NO_ARG): __values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) 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/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd index 29aeec05..419f0172 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -1,13 +1,13 @@ -class_name GdUnitArrayAssertImpl extends GdUnitArrayAssert + var _base :GdUnitAssert var _current_value_provider :ValueProvider func _init(current): _current_value_provider = DefaultValueProvider.new(current) - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not __validate_value_type(current): @@ -50,6 +50,12 @@ func __current() -> Variant: return _current_value_provider.get_value() +func max_length(left, right) -> int: + var ls = str(left).length() + var rs = str(right).length() + return rs if ls < rs else ls + + func _array_equals_div(current, expected, case_sensitive :bool = false) -> Array: var current_ := PackedStringArray(Array(current)) var expected_ := PackedStringArray(Array(expected)) @@ -59,7 +65,7 @@ func _array_equals_div(current, expected, case_sensitive :bool = false) -> Array if index < expected_.size(): var e := expected_[index] if not GdObjects.equals(c, e, case_sensitive): - var length := GdUnitTools.max_length(c, e) + var length := max_length(c, e) current_[index] = GdAssertMessages.format_invalid(c.lpad(length)) expected_[index] = e.lpad(length) index_report_.push_back({"index" : index, "current" :c, "expected": e}) @@ -293,7 +299,7 @@ func is_instanceof(expected) -> GdUnitAssert: func extract(func_name :String, args := Array()) -> GdUnitArrayAssert: var extracted_elements := Array() - var extractor := GdUnitFuncValueExtractor.new(func_name, args) + var extractor :GdUnitValueExtractor = ResourceLoader.load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(func_name, args) var current = __current() if current == null: _current_value_provider = DefaultValueProvider.new(null) diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd index a5852626..6fc87b18 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -1,4 +1,3 @@ -class_name GdUnitAssertImpl extends GdUnitAssert @@ -7,29 +6,6 @@ var _current_error_message :String = "" var _custom_failure_message :String = "" -# Scans the current stack trace for the root cause to extract the line number -static func _get_line_number() -> int: - var stack_trace := get_stack() - if stack_trace == null or stack_trace.is_empty(): - return -1 - for stack_info in stack_trace: - var function :String = stack_info.get("function") - # we catch helper asserts to skip over to return the correct line number - if function.begins_with("assert_"): - continue - var source :String = stack_info.get("source") - if source.is_empty() \ - or source.begins_with("user://") \ - or source.ends_with("AssertImpl.gd") \ - or source.ends_with("GdUnitTestSuite.gd") \ - or source.ends_with("GdUnitSceneRunnerImpl.gd") \ - or source.ends_with("GdUnitObjectInteractions.gd") \ - or source.ends_with("GdUnitAwaiter.gd"): - continue - return stack_info.get("line") - return -1 - - func _init(current :Variant): _current = current # save the actual assert instance on the current thread context @@ -55,7 +31,7 @@ func report_success() -> GdUnitAssert: func report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() GdAssertReports.set_last_error_line_number(line_number) _current_error_message = error_message if _custom_failure_message.is_empty() else _custom_failure_message GdAssertReports.report_error(_current_error_message, line_number) @@ -66,10 +42,6 @@ func test_fail(): return report_error(GdAssertMessages.error_not_implemented()) -static func _normalize_bbcode(message :String) -> String: - return GdUnitTools.richtext_normalize(message).replace("\r", "") - - func override_failure_message(message :String): _custom_failure_message = message return self 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/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd index dc7c31c2..d838c86f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitBoolAssertImpl extends GdUnitBoolAssert -var _base: GdUnitAssertImpl +var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_BOOL): diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd index eea65385..514894ba 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitDictionaryAssertImpl extends GdUnitDictionaryAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_DICTIONARY): diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd index ec19413e..8be056e6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -1,6 +1,7 @@ -class_name GdUnitFailureAssertImpl extends GdUnitFailureAssert +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + var _is_failed := false var _failure_message :String @@ -75,7 +76,7 @@ func has_line(expected :int) -> GdUnitFailureAssert: func has_message(expected :String) -> GdUnitFailureAssert: var expected_error := GdUnitTools.normalize_text(expected) - var current_error := GdUnitAssertImpl._normalize_bbcode(_failure_message) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) if current_error != expected_error: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages._colored_array_div(diffs[1]) @@ -85,7 +86,7 @@ func has_message(expected :String) -> GdUnitFailureAssert: func starts_with_message(expected :String) -> GdUnitFailureAssert: var expected_error := GdUnitTools.normalize_text(expected) - var current_error := GdUnitAssertImpl._normalize_bbcode(_failure_message) + var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message)) if current_error.find(expected_error) != 0: var diffs := GdDiffTool.string_diff(current_error, expected_error) var current := GdAssertMessages._colored_array_div(diffs[1]) @@ -94,7 +95,7 @@ func starts_with_message(expected :String) -> GdUnitFailureAssert: func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() GdAssertReports.report_error(error_message, line_number) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd index 6ccc6645..b4e07008 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -1,12 +1,12 @@ -class_name GdUnitFileAssertImpl extends GdUnitFileAssert +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") var _base: GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_STRING): @@ -89,5 +89,6 @@ func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: var source_code = GdScriptParser.to_unix_format(instance.get_script().source_code) GdUnitTools.free_instance(instance) var rows := Array(source_code.split("\n")) - GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows) + ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(rows)\ + .contains_exactly(expected_rows) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd index fbc291b8..d457b00a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitFloatAssertImpl extends GdUnitFloatAssert var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_FLOAT): diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd index 64a5792b..0c9e4b9f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -1,43 +1,49 @@ -class_name GdUnitFuncAssertImpl 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 func _init(instance :Object, func_name :String, args := Array()): - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() GdAssertReports.reset_last_error_line_number() # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) # verify at first the function name exists if not instance.has_method(func_name): report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance]) + _interrupted = true else: _current_value_provider = CallBackValueProvider.new(instance, func_name, args) func _notification(_what): - 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 @@ -66,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()) @@ -97,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() @@ -106,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) @@ -119,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/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd index 9cfbcb7b..169994be 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -1,7 +1,5 @@ -class_name GdUnitGodotErrorAssertImpl extends GdUnitGodotErrorAssert - var _current_error_message :String var _callable :Callable @@ -37,7 +35,7 @@ func _report_success() -> GdUnitAssert: func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: - var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertImpl._get_line_number() + var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssert._get_line_number() _current_error_message = error_message GdAssertReports.report_error(error_message, line_number) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd index aee77125..7f39c060 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitIntAssertImpl extends GdUnitIntAssert var _base: GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_INT): diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd index 29b3a1dc..18c67bf7 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitObjectAssertImpl extends GdUnitObjectAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if (current != null diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index 8136dc39..a6cebba1 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -1,10 +1,10 @@ -class_name GdUnitResultAssertImpl extends GdUnitResultAssert var _base :GdUnitAssert + func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not __validate_value_type(current): diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd index b3ebba26..b82ebe46 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -1,10 +1,8 @@ -class_name GdUnitSignalAssertImpl extends GdUnitSignalAssert const DEFAULT_TIMEOUT := 2000 -const NO_ARG = GdUnitConstants.NO_ARG -var _signal_collector :GdUnitSignalAssertImpl.SignalCollector +var _signal_collector :GdUnitSignalCollector var _emitter :Object var _current_error_message :String = "" var _custom_failure_message :String = "" @@ -13,113 +11,12 @@ var _timeout := DEFAULT_TIMEOUT var _interrupted := false -# It connects to all signals of given emitter and collects received signals and arguments -# The collected signals are cleand finally when the emitter is freed. -class SignalCollector extends RefCounted: - const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] - - # { - # emitter : { - # signal_name : [signal_args], - # ... - # } - # } - var _collected_signals :Dictionary = {} - - - func clear() -> void: - for emitter in _collected_signals.keys(): - if is_instance_valid(emitter): - unregister_emitter(emitter) - - - # connect to all possible signals defined by the emitter - # prepares the signal collection to store received signals and arguments - func register_emitter(emitter :Object): - if is_instance_valid(emitter): - # check emitter is already registerd - if _collected_signals.has(emitter): - return - _collected_signals[emitter] = Dictionary() - # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. - if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): - emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) - # connect to all signals of the emitter we want to collect - for signal_def in emitter.get_signal_list(): - var signal_name = signal_def["name"] - # set inital collected to empty - if not is_signal_collecting(emitter, signal_name): - _collected_signals[emitter][signal_name] = Array() - if SIGNAL_BLACK_LIST.find(signal_name) != -1: - continue - if !emitter.is_connected(signal_name, _on_signal_emmited): - var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) - if err != OK: - push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) - - - # unregister all acquired resources/connections, otherwise it ends up in orphans - # is called when the emitter is removed from the parent - func unregister_emitter(emitter :Object): - if is_instance_valid(emitter): - for signal_def in emitter.get_signal_list(): - var signal_name = signal_def["name"] - if emitter.is_connected(signal_name, _on_signal_emmited): - emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) - _collected_signals.erase(emitter) - - - # receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements - func _on_signal_emmited( arg0=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, arg10=NO_ARG, arg11=NO_ARG): - var signal_args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) - # extract the emitter and signal_name from the last two arguments (see line 61 where is added) - var signal_name :String = signal_args.pop_back() - var emitter :Object = signal_args.pop_back() - # prints("_on_signal_emmited:", emitter, signal_name, signal_args) - if is_signal_collecting(emitter, signal_name): - _collected_signals[emitter][signal_name].append(signal_args) - - - func reset_received_signals(emitter :Object): - # _debug_signal_list("before claer"); - if _collected_signals.has(emitter): - for signal_name in _collected_signals[emitter]: - _collected_signals[emitter][signal_name].clear() - # _debug_signal_list("after claer"); - - - func is_signal_collecting(emitter :Object, signal_name :String) -> bool: - return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) - - - func match(emitter :Object, signal_name :String, args :Array) -> bool: - #prints("match", signal_name, _collected_signals[emitter][signal_name]); - if _collected_signals.is_empty() or not _collected_signals.has(emitter): - return false - for received_args in _collected_signals[emitter][signal_name]: - # prints("testing", signal_name, received_args, "vs", args) - if GdObjects.equals(received_args, args): - return true - return false - - - func _debug_signal_list(message :String): - prints("-----", message, "-------") - prints("senders {") - for emitter in _collected_signals: - prints("\t", emitter) - for signal_name in _collected_signals[emitter]: - var args = _collected_signals[emitter][signal_name] - prints("\t\t", signal_name, args) - prints("}") - - func _init(emitter :Object): # save the actual assert instance on the current thread context var context := GdUnitThreadManager.get_current_context() context.set_assert(self) _signal_collector = context.get_signal_collector() - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() _emitter = emitter GdAssertReports.reset_last_error_line_number() @@ -130,7 +27,7 @@ func report_success() -> GdUnitAssert: func report_warning(message :String) -> GdUnitAssert: - GdAssertReports.report_warning(message, GdUnitAssertImpl._get_line_number()) + GdAssertReports.report_warning(message, GdUnitAssert._get_line_number()) return self @@ -171,13 +68,13 @@ func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: # Verifies that given signal is emitted until waiting time func is_emitted(name :String, args := []) -> GdUnitSignalAssert: - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() return await _wail_until_signal(name, args, false) # Verifies that given signal is NOT emitted until waiting time func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: - _line_number = GdUnitAssertImpl._get_line_number() + _line_number = GdUnitAssert._get_line_number() return await _wail_until_signal(name, args, true) diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd index a47ed744..154b9e5c 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -1,11 +1,10 @@ -class_name GdUnitStringAssertImpl extends GdUnitStringAssert var _base :GdUnitAssert func _init(current): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _base.__validate_value_type(current, TYPE_STRING): diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd index 4f39a231..43a63f1d 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -1,4 +1,3 @@ -class_name GdUnitVectorAssertImpl extends GdUnitVectorAssert var _base: GdUnitAssert @@ -6,7 +5,7 @@ var _current_type :int func _init(current :Variant): - _base = GdUnitAssertImpl.new(current) + _base = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) if not _validate_value_type(current): diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index 58a6c98a..103efe17 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -2,6 +2,8 @@ class_name GdObjects extends Resource +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const TYPE_VOID = TYPE_MAX + 1000 const TYPE_VARARG = TYPE_MAX + 1001 const TYPE_VARIANT = TYPE_MAX + 1002 @@ -373,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: @@ -397,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] 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 a9f717ac..00000000 --- a/addons/gdUnit4/src/core/GdUnitExecutor.gd +++ /dev/null @@ -1,400 +0,0 @@ -extends Node - -signal ExecutionCompleted() - - -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 = GdUnitMemoryPool.new() -var _report_errors_enabled :bool -var _report_collector : = GdUnitReportCollector.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() - GdUnitTools.append_array(reports, _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 := FuzzerTool.create_fuzzer(test_suite.get_script(), fuzzer_arg) - 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 38ff31a1..00000000 --- a/addons/gdUnit4/src/core/GdUnitMemoryPool.gd +++ /dev/null @@ -1,135 +0,0 @@ -class_name GdUnitMemoryPool -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: - GdUnitMemoryPool.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/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd index 2c847c26..b317d392 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd @@ -9,7 +9,7 @@ static func verify(obj :Object, times): static func verify_no_interactions(obj :Object) -> GdUnitAssert: - var gd_assert := GdUnitAssertImpl.new("") + var gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") if not _is_mock_or_spy(obj, "__verify"): return gd_assert.report_success() var summary :Dictionary = obj.__verify_no_interactions() @@ -19,7 +19,7 @@ static func verify_no_interactions(obj :Object) -> GdUnitAssert: static func verify_no_more_interactions(obj :Object) -> GdUnitAssert: - var gd_assert := GdUnitAssertImpl.new("") + var gd_assert = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE).new("") if not _is_mock_or_spy(obj, "__verify_no_more_interactions"): return gd_assert var summary :Dictionary = obj.__verify_no_more_interactions() diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd index d4f10700..5d455abb 100644 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd +++ b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd @@ -1,4 +1,4 @@ -class_name GdUnitObjectInteractionsTemplate +const GdUnitAssertImpl := preload("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") var __expected_interactions :int = -1 var __saved_interactions := Dictionary() diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd index 0e61f7ea..ab64df44 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)) +# Event bridge from C# GdUnit4.ITestEventListener.cs func PublishEvent(data) -> void: var event := GdUnitEvent.new().deserialize(data.AsDictionary()) _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 15d3ad0e..70fc5bf1 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -1,6 +1,8 @@ class_name GdUnitRunnerConfig extends Resource +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CONFIG_VERSION = "1.0" const VERSION = "version" const INCLUDED = "included" diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index e6222389..ed6dffaa 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -2,6 +2,10 @@ class_name GdUnitSceneRunnerImpl extends GdUnitSceneRunner + +var GdUnitFuncAssertImpl := ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE) + + # mapping of mouse buttons and his masks const MAP_MOUSE_BUTTON_MASKS := { MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT, @@ -16,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 @@ -73,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 @@ -208,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 @@ -227,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 52ea7a24..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" % GdUnitTools.error_as_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/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd new file mode 100644 index 00000000..cf55adcf --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -0,0 +1,102 @@ +# It connects to all signals of given emitter and collects received signals and arguments +# The collected signals are cleand finally when the emitter is freed. +class_name GdUnitSignalCollector +extends RefCounted + +const NO_ARG :Variant = GdUnitConstants.NO_ARG +const SIGNAL_BLACK_LIST = []#["tree_exiting", "tree_exited", "child_exiting_tree"] + +# { +# emitter : { +# signal_name : [signal_args], +# ... +# } +# } +var _collected_signals :Dictionary = {} + + +func clear() -> void: + for emitter in _collected_signals.keys(): + if is_instance_valid(emitter): + unregister_emitter(emitter) + + +# connect to all possible signals defined by the emitter +# prepares the signal collection to store received signals and arguments +func register_emitter(emitter :Object): + if is_instance_valid(emitter): + # check emitter is already registerd + if _collected_signals.has(emitter): + return + _collected_signals[emitter] = Dictionary() + # connect to 'tree_exiting' of the emitter to finally release all acquired resources/connections. + if emitter is Node and !emitter.tree_exiting.is_connected(unregister_emitter): + emitter.tree_exiting.connect(unregister_emitter.bind(emitter)) + # connect to all signals of the emitter we want to collect + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + # set inital collected to empty + if not is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name] = Array() + if SIGNAL_BLACK_LIST.find(signal_name) != -1: + continue + if !emitter.is_connected(signal_name, _on_signal_emmited): + var err := emitter.connect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + if err != OK: + push_error("Can't connect to signal %s on %s. Error: %s" % [signal_name, emitter, error_string(err)]) + + +# unregister all acquired resources/connections, otherwise it ends up in orphans +# is called when the emitter is removed from the parent +func unregister_emitter(emitter :Object): + if is_instance_valid(emitter): + for signal_def in emitter.get_signal_list(): + var signal_name = signal_def["name"] + if emitter.is_connected(signal_name, _on_signal_emmited): + emitter.disconnect(signal_name, _on_signal_emmited.bind(emitter, signal_name)) + _collected_signals.erase(emitter) + + +# receives the signal from the emitter with all emitted signal arguments and additional the emitter and signal_name as last two arguements +func _on_signal_emmited( arg0=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, arg10=NO_ARG, arg11=NO_ARG): + var signal_args = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11], NO_ARG) + # extract the emitter and signal_name from the last two arguments (see line 61 where is added) + var signal_name :String = signal_args.pop_back() + var emitter :Object = signal_args.pop_back() + # prints("_on_signal_emmited:", emitter, signal_name, signal_args) + if is_signal_collecting(emitter, signal_name): + _collected_signals[emitter][signal_name].append(signal_args) + + +func reset_received_signals(emitter :Object): + # _debug_signal_list("before claer"); + if _collected_signals.has(emitter): + for signal_name in _collected_signals[emitter]: + _collected_signals[emitter][signal_name].clear() + # _debug_signal_list("after claer"); + + +func is_signal_collecting(emitter :Object, signal_name :String) -> bool: + return _collected_signals.has(emitter) and _collected_signals[emitter].has(signal_name) + + +func match(emitter :Object, signal_name :String, args :Array) -> bool: + #prints("match", signal_name, _collected_signals[emitter][signal_name]); + if _collected_signals.is_empty() or not _collected_signals.has(emitter): + return false + for received_args in _collected_signals[emitter][signal_name]: + # prints("testing", signal_name, received_args, "vs", args) + if GdObjects.equals(received_args, args): + return true + return false + + +func _debug_signal_list(message :String): + prints("-----", message, "-------") + prints("senders {") + for emitter in _collected_signals: + prints("\t", emitter) + for signal_name in _collected_signals[emitter]: + var args = _collected_signals[emitter][signal_name] + prints("\t\t", signal_name, args) + prints("}") 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 df0ab91e..4fb05619 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -8,6 +8,7 @@ class_name GdUnitSingleton extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const MEATA_KEY := "GdUnitSingletons" @@ -30,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..05afe11c 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -6,11 +6,9 @@ static func create(source :Script, line_number :int) -> Result: 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] diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd index f751253e..3da01047 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -8,9 +8,17 @@ 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 regex_replace_class_name := GdUnitTools.to_regex("(?m)^class_name .*$") +var _expression_runner := GdUnitExpressionRunner.new() func scan_testsuite_classes() -> void: @@ -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() @@ -49,9 +59,12 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N if sub_dir != null: _scan_test_suites(sub_dir, collected_suites) else: + var time = LocalTime.now() var test_suite := _parse_is_test_suite(resource_path) if test_suite: collected_suites.append(test_suite) + if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: + push_warning("Scanning of test-suite '%s' took more than 300ms: " % resource_path, time.elapsed_since()) file_name = dir.get_next() return collected_suites @@ -66,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 @@ -80,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: @@ -119,7 +132,7 @@ func _handle_test_suite_arguments(test_suite, script :GDScript, fd :GdFunctionDe for arg in fd.args(): match arg.name(): _TestCase.ARGUMENT_SKIP: - var result = _run_expression(script, arg.value_as_string()) + var result = _expression_runner.execute(script, arg.value_as_string()) if result is bool: test_suite.__is_skipped = result else: @@ -149,7 +162,7 @@ func _handle_test_case_arguments(test_suite, script :GDScript, fd :GdFunctionDes _TestCase.ARGUMENT_TIMEOUT: timeout = arg.default() _TestCase.ARGUMENT_SKIP: - var result = _run_expression(script, arg.value_as_string()) + var result = _expression_runner.execute(script, arg.value_as_string()) if result is bool: is_skipped = result else: @@ -187,23 +200,6 @@ func _parse_and_add_test_cases(test_suite, script :GDScript, test_case_names :Pa _handle_test_case_arguments(test_suite, script, fd) -func _run_expression(src_script :GDScript, expression :String) -> Variant: - var script := GDScript.new() - script.source_code = _remove_class_name(src_script.source_code) - script.source_code += """ - func __run_expression() -> Variant: - return $expression - """.dedent().replace("$expression", expression) - script.reload(false) - var runner := script.new() - runner.queue_free() - return runner.__run_expression() - - -func _remove_class_name(source_code :String) -> String: - return regex_replace_class_name.sub(source_code, "") - - const TEST_CASE_ARGUMENTS = [_TestCase.ARGUMENT_TIMEOUT, _TestCase.ARGUMENT_SKIP, _TestCase.ARGUMENT_SKIP_REASON, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ARGUMENT_SEED] func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void: @@ -234,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 @@ -258,7 +254,11 @@ 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: diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index 771945e7..30256e11 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -1,4 +1,3 @@ -class_name GdUnitTools extends RefCounted const GDUNIT_TEMP := "user://tmp" @@ -9,15 +8,18 @@ static func temp_dir() -> String: DirAccess.make_dir_recursive_absolute(GDUNIT_TEMP) return GDUNIT_TEMP + static func create_temp_dir(folder_name :String) -> String: var new_folder = temp_dir() + "/" + folder_name if not DirAccess.dir_exists_absolute(new_folder): DirAccess.make_dir_recursive_absolute(new_folder) return new_folder + static func clear_tmp(): delete_directory(GDUNIT_TEMP) - + + # Creates a new file under static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess: var file_path := create_temp_dir(relative_path) + "/" + file_name @@ -26,9 +28,11 @@ static func create_temp_file(relative_path :String, file_name :String, mode := F push_error("Error creating temporary file at: %s, %s" % [file_path, error_as_string(FileAccess.get_open_error())]) return file + static func current_dir() -> String: return ProjectSettings.globalize_path("res://") + static func delete_directory(path :String, only_content := false) -> void: var dir := DirAccess.open(path) if dir != null: @@ -102,6 +106,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f push_error("Directory not found: " + from_dir) return false + # scans given path for sub directories by given prefix and returns the highest index numer # e.g. static func find_last_path_index(path :String, prefix :String) -> int: @@ -121,6 +126,7 @@ static func find_last_path_index(path :String, prefix :String) -> int: last_iteration = iteration return last_iteration + static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int: var dir := DirAccess.open(path) if dir == null: @@ -154,6 +160,7 @@ static func scan_dir(path :String) -> PackedStringArray: content.append(next) return content + static func resource_as_array(resource_path :String) -> PackedStringArray: var file := FileAccess.open(resource_path, FileAccess.READ) if file == null: @@ -164,6 +171,7 @@ static func resource_as_array(resource_path :String) -> PackedStringArray: file_content.append(file.get_line()) return file_content + static func resource_as_string(resource_path :String) -> String: var file := FileAccess.open(resource_path, FileAccess.READ) if file == null: @@ -178,20 +186,15 @@ static func normalize_text(text :String) -> String: static func richtext_normalize(input :String) -> String: return GdUnitSingleton.instance("regex_richtext", func _regex_richtext() -> RegEx: - return GdUnitTools.to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") ).sub(input, "", true) - - -static func max_length(left, right) -> int: - var ls = str(left).length() - var rs = str(right).length() - return rs if ls < rs else ls + return to_regex("\\[/?(b|color|bgcolor|right|table|cell).*?\\]") )\ + .sub(input, "", true).replace("\r", "") static func to_regex(pattern :String) -> RegEx: var regex := RegEx.new() var err := regex.compile(pattern) if err != OK: - push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, GdUnitTools.error_as_string(err)]) + push_error("Can't compiling regx '%s'.\n ERROR: %s" % [pattern, error_string(err)]) return regex @@ -200,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) @@ -212,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) @@ -248,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() @@ -268,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("//"): @@ -281,27 +287,21 @@ static func make_qualified_path(path :String) -> String: return "res:/" + path return path + static func error_as_string(error_number :int) -> String: return error_string(error_number) - + + static func clear_push_errors() -> void: var runner = Engine.get_meta("GdUnitRunner") if runner != null: runner.clear_push_errors() + static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: var test_case = test_suite.find_child(test_case_name, false, false) test_case.expect_to_interupt() -static func append_array(array, append :Array) -> void: - var major :int = Engine.get_version_info()["major"] - var minor :int = Engine.get_version_info()["minor"] - if major >= 3 and minor >= 3: - array.append_array(append) - else: - for element in append: - array.append(element) - static func extract_zip(zip_package :String, dest_path :String) -> Result: var zip: ZIPReader = ZIPReader.new() 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 c1893e58..ffe3c125 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -5,6 +5,8 @@ signal gdunit_runner_start() signal gdunit_runner_stop(client_id :int) +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CMD_RUN_OVERALL = "Debug Overall TestSuites" const CMD_RUN_TESTCASE = "Run TestCases" const CMD_RUN_TESTCASE_DEBUG = "Run TestCases (Debug)" @@ -54,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) @@ -63,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)) @@ -186,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() @@ -259,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 3f78d7f6..fa0a1ea7 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -1,6 +1,7 @@ class_name GdScriptParser extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd new file mode 100644 index 00000000..f3130a07 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd @@ -0,0 +1,26 @@ +class_name GdUnitExpressionRunner +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ExpressionRunner extends '${clazz_path}' + +func __run_expression() -> Variant: + return $expression + +""" + +func execute(src_script :GDScript, expression :String) -> Variant: + var script := GDScript.new() + var resource_path := "res://addons/gdUnit4/src/Fuzzers.gd" if src_script.resource_path.is_empty() else src_script.resource_path + script.source_code = CLASS_TEMPLATE.dedent()\ + .replace("${clazz_path}", resource_path)\ + .replace("$expression", expression) + script.reload(false) + var runner :Variant = script.new() + if runner.has_method("queue_free"): + runner.queue_free() + return runner.__run_expression() + + +func to_fuzzer(src_script :GDScript, expression :String) -> Fuzzer: + return execute(src_script, expression) as Fuzzer diff --git a/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd b/addons/gdUnit4/src/core/report/GdUnitReportCollector.gd deleted file mode 100644 index cdad2d9b..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: - GdUnitTools.append_array(reports, 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 91fa72cc..d0eaa16d 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd @@ -1,25 +1,32 @@ class_name GdUnitThreadContext extends RefCounted - var _thread :Thread +var _thread_name :String +var _thread_id :int var _assert :GdUnitAssert -var _signal_collector :GdUnitSignalAssertImpl.SignalCollector +var _signal_collector :GdUnitSignalCollector +var _execution_context :GdUnitExecutionContext func _init(thread :Thread = null): - _thread = thread - _signal_collector = GdUnitSignalAssertImpl.SignalCollector.new() - - -func init() -> void: - clear() + 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 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: @@ -31,12 +38,25 @@ func get_assert() -> GdUnitAssert: return _assert -func get_signal_collector() -> GdUnitSignalAssertImpl.SignalCollector: +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 acad41cf..74cded24 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd @@ -1,5 +1,4 @@ # This class defines a value extractor by given function name and args -class_name GdUnitFuncValueExtractor extends GdUnitValueExtractor var _func_names :Array @@ -53,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 b11368e5..b17b6865 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd @@ -1,7 +1,7 @@ # Base interface for fuzz testing # https://en.wikipedia.org/wiki/Fuzzing class_name Fuzzer -extends Resource +extends RefCounted # To run a test with a specific fuzzer you have to add defailt argument checked your test case # all arguments are optional [] # syntax: @@ -21,16 +21,19 @@ const ARGUMENT_SEED := "fuzzer_seed" var _iteration_index :int = 0 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 + # returns the current iteration index func iteration_index() -> int: return _iteration_index + # returns the amount of iterations where the fuzzer will be run func iteration_limit() -> int: return _iteration_limit diff --git a/addons/gdUnit4/src/fuzzers/FuzzerTool.gd b/addons/gdUnit4/src/fuzzers/FuzzerTool.gd deleted file mode 100644 index 64a81186..00000000 --- a/addons/gdUnit4/src/fuzzers/FuzzerTool.gd +++ /dev/null @@ -1,35 +0,0 @@ -class_name FuzzerTool -extends Resource - - -const fuzzer_template := """ -${source_code} - -func __fuzzer(): - return ${fuzzer_func} -""" - -static func create_fuzzer(source :GDScript, function: GdFunctionArgument) -> Fuzzer: - var className := source.resource_path.get_file().replace(".gd", "") - var fuzzer_func := function.value_as_string() - var source_code := fuzzer_template\ - .replace("${source_code}", source.source_code)\ - .replace("${fuzzer_func}", fuzzer_func)\ - .replace(className, className + "extented") - var script := GDScript.new() - script.source_code = source_code - var temp_dir := "res://addons/gdUnit4/.tmp" - DirAccess.make_dir_recursive_absolute(temp_dir) - var resource_path_ := "%s/%s" % [temp_dir, "_fuzzer_bulder%d.gd" % Time.get_ticks_msec()] - var err := ResourceSaver.save(script, resource_path_, ResourceSaver.FLAG_BUNDLE_RESOURCES|ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS) - if err != OK: - prints("Script loading error", error_string(err)) - return null - script = ResourceLoader.load(resource_path_, "GDScript", ResourceLoader.CACHE_MODE_IGNORE); - var instance :Object = script.new() - instance.queue_free() - DirAccess.remove_absolute(script.resource_path) - if not instance.has_method("__fuzzer"): - prints("Error", script, "Missing function '__fuzzer'") - return null - return instance.call("__fuzzer") as Fuzzer 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 2ceeef99..df7f02c3 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -1,33 +1,23 @@ class_name GdUnitMockBuilder extends GdUnitClassDoubler - -# 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 GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +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) @@ -39,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: @@ -60,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: @@ -81,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: @@ -97,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]) @@ -171,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/ErrorLogEntry.gd b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd index 33136134..f574147e 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd @@ -1,12 +1,16 @@ extends RefCounted class_name ErrorLogEntry + enum TYPE { SCRIPT_ERROR, PUSH_ERROR, PUSH_WARNING } + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const PATTERN_SCRIPT_ERROR := "USER SCRIPT ERROR:" const PATTERN_PUSH_ERROR := "USER ERROR:" const PATTERN_PUSH_WARNING := "USER WARNING:" 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 44bbedab..00000000 --- a/addons/gdUnit4/src/mono/GdUnit3MonoAPI.gd +++ /dev/null @@ -1,38 +0,0 @@ -extends RefCounted -class_name GdUnit3MonoAPI - -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..3df1c981 --- /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) -> Result: + if not GdUnit4MonoApiLoader.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 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/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index 29207e68..6552cfc3 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -109,9 +109,9 @@ func start() -> Result: 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: 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/GdUnitReportSummary.gd b/addons/gdUnit4/src/report/GdUnitReportSummary.gd index 24b39080..1aefa1fd 100644 --- a/addons/gdUnit4/src/report/GdUnitReportSummary.gd +++ b/addons/gdUnit4/src/report/GdUnitReportSummary.gd @@ -1,6 +1,8 @@ class_name GdUnitReportSummary extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const CHARACTERS_TO_ENCODE := { '<' : '<', '>' : '>' 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 551c6bf9..65a708bb 100644 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd +++ b/addons/gdUnit4/src/report/JUnitXmlReport.gd @@ -3,6 +3,8 @@ class_name JUnitXmlReport extends RefCounted +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + const ATTR_CLASSNAME := "classname" const ATTR_ERRORS := "errors" const ATTR_FAILURES := "failures" @@ -76,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))) @@ -133,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 eb1492d8..b01350ae 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -1,9 +1,11 @@ class_name GdUnitSpyBuilder extends GdUnitClassDoubler +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const SPY_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/spy/GdUnitSpyImpl.gd") -static func build(caller :Object, to_spy, debug_write = false): - 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 @@ -15,10 +17,10 @@ static func build(caller :Object, to_spy, debug_write = false): 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: @@ -29,7 +31,7 @@ static func build(caller :Object, to_spy, debug_write = false): 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: @@ -53,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() @@ -71,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()) @@ -84,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"] @@ -104,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 a60ca253..0ce658aa 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -15,10 +15,10 @@ signal run_testsuite # tree icons @onready var ICON_SPINNER = load("res://addons/gdUnit4/src/ui/assets/spinner.tres") -@onready var ICON_TEST_DEFAULT = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCase.svg") -@onready var ICON_TEST_SUCCESS = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg") -@onready var ICON_TEST_FAILED = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg") -@onready var ICON_TEST_ERROR = load_resized_texture("res://addons/gdUnit4/src/ui/assets/TestCaseError.svg") +@onready var ICON_TEST_DEFAULT = load("res://addons/gdUnit4/src/ui/assets/TestCase.svg") +@onready var ICON_TEST_SUCCESS = load("res://addons/gdUnit4/src/ui/assets/TestCaseSuccess.svg") +@onready var ICON_TEST_FAILED = load("res://addons/gdUnit4/src/ui/assets/TestCaseFailed.svg") +@onready var ICON_TEST_ERROR = load("res://addons/gdUnit4/src/ui/assets/TestCaseError.svg") @onready var ICON_TEST_SUCCESS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_success_orphan.tres") @onready var ICON_TEST_FAILED_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_failed_orphan.tres") @onready var ICON_TEST_ERRORS_ORPHAN = load("res://addons/gdUnit4/src/ui/assets/TestCase_error_orphan.tres") @@ -126,9 +126,9 @@ func is_test_suite(item :TreeItem) -> bool: func _ready(): - init_tree() if Engine.is_editor_hint(): _editor = Engine.get_meta("GdUnitEditorPlugin") + init_tree() GdUnitSignals.instance().gdunit_add_test_suite.connect(_on_gdunit_add_test_suite) GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) var command_handler := GdUnitCommandHandler.instance() @@ -144,20 +144,15 @@ func _process(_delta): queue_redraw() -func load_resized_texture(path :String, width :int = 16, height :int = 16) -> Texture2D: - var texture :Texture2D = load(path) - var image := texture.get_image() - if width > 0 && height > 0: - image.resize(width, height) - return ImageTexture.create_from_image(image) - - func init_tree() -> void: cleanup_tree() _tree.set_hide_root(true) _tree.ensure_cursor_is_visible() _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.0 + _tree.set("theme_override_constants/icon_max_width", 16*scale_factor) func cleanup_tree() -> void: diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd index 692ba3c3..8d2f1fad 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -2,6 +2,8 @@ extends Window const EAXAMPLE_URL := "https://github.com/MikeSchulze/gdUnit4-examples/archive/refs/heads/master.zip" + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") @onready var _update_client :GdUnitUpdateClient = $GdUnitUpdateClient @@ -15,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) @@ -149,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 @@ -231,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): @@ -269,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/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd index 8d517787..dc2c0bec 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -1,7 +1,9 @@ @tool extends ConfirmationDialog -const GdUnitUpdateClient = preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") +const GdUnitUpdateClient := preload("res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd") + const spinner_icon := "res://addons/gdUnit4/src/ui/assets/spinner.tres" @@ -219,7 +221,7 @@ func download_release() -> void: _update_client.queue_free() if response.code() != 200: push_warning("Update information cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.code(), response.response()]) - await message_h4("Update failed! Try it later again.", Color.RED) + message_h4("Update failed! Try it later again.", Color.RED) await get_tree().create_timer(3).timeout return 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/project.godot b/project.godot index 5ba5887f..a8002da3 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Beehave" run/main_scene="res://examples/BeehaveTestScene.tscn" -config/features=PackedStringArray("4.1") +config/features=PackedStringArray("4.2") boot_splash/image="res://splash.png" boot_splash/fullsize=false config/icon="res://icon.png" @@ -36,7 +36,7 @@ import/blender/enabled=false [gdunit4] -settings/test/test_root_folder="test/" +settings/test/test_lookup_folder="test/" [input] diff --git a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd index 13a64219..e78db050 100644 --- a/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd +++ b/test/randomized_composites/weighted_sampling/selector_random/selector_random_weights_test.gd @@ -3,7 +3,7 @@ extends GdUnitTestSuite const __source = "res://test/randomized_composites/weighted_sampling/selector_random/SelectorRandomWeights.tscn" -const ACCEPTABLE_RANGE = 3.0 +const ACCEPTABLE_RANGE = 4.0 func create_scene() -> Node2D: