diff --git a/.gitea/workflows/dev-branch.yaml b/.gitea/workflows/dev-branch.yaml
index bc7bb5c2..91a4ee5e 100644
--- a/.gitea/workflows/dev-branch.yaml
+++ b/.gitea/workflows/dev-branch.yaml
@@ -23,33 +23,38 @@ jobs:
run: |
apt update && apt -y install curl zip nodejs
- - name: Checkout with LFS
- uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
- with:
- checkout-version: 3
+# - name: Checkout with LFS
+# uses: https://git.game-dev.space/minimata/checkout-with-lfs.git@main
+# with:
+# checkout-version: 3
+ - uses: actions/checkout@v6
+ with:
+ lfs: true
# Cache
-# - uses: actions/cache@v3
-# with:
-# path: assets
-# key: Assets-${{ hashFiles('**') }}
-# restore-keys: |
-# Assets-
+ - name: cache
+ uses: actions/cache@v5
+ with:
+ path: assets
+ key: Assets-${{ hashFiles('assets/**') }}
+ restore-keys: |
+ Assets-
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '9.0.x'
- - uses: godot-gdunit-labs/gdUnit4-action@v1
+ - name: Launch Godot
+ uses: godot-gdunit-labs/gdUnit4-action@v1
with:
godot-version: '4.5.1'
godot-net: true
version: 'v6.0.3'
paths: |
res://tests/
- timeout: 5
+ timeout: 1
report-name: test_report.xml
- name: Build Windows
diff --git a/.runsettings b/.runsettings
new file mode 100644
index 00000000..d51fe20a
--- /dev/null
+++ b/.runsettings
@@ -0,0 +1,55 @@
+
+
+
+ 1
+ ./TestResults
+ net8.0;net9.0
+ 180000
+ true
+
+ d:\development\Godot_v4.5-stable_mono_win64\Godot_v4.5-stable_mono_win64.exe
+
+
+
+
+
+
+
+ detailed
+
+
+
+
+ test-result.html
+
+
+
+
+ test-result.trx
+
+
+
+
+
+
+
+ "--verbose"
+
+
+ FullyQualifiedName
+
+
+ true
+
+
+ 20000
+
+
diff --git a/Movement tests.csproj b/Movement tests.csproj
index b8e9488d..680b232c 100644
--- a/Movement tests.csproj
+++ b/Movement tests.csproj
@@ -1,10 +1,11 @@
- net8.0
+ net9.0
true
Movementtests
+
@@ -124,9 +125,18 @@
-
+
+
+
+
+
+
+ none
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
\ No newline at end of file
diff --git a/Movement tests.sln.DotSettings.user b/Movement tests.sln.DotSettings.user
index da18330c..ec0e0613 100644
--- a/Movement tests.sln.DotSettings.user
+++ b/Movement tests.sln.DotSettings.user
@@ -5,4 +5,8 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
+ <Solution />
+</SessionState>
+ D:\Godot\Projects\movement-tests\.runsettings
True
\ No newline at end of file
diff --git a/addons/gdUnit4/LICENSE b/addons/gdUnit4/LICENSE
new file mode 100644
index 00000000..8c60d132
--- /dev/null
+++ b/addons/gdUnit4/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Mike Schulze
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd
new file mode 100644
index 00000000..cae9138b
--- /dev/null
+++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd
@@ -0,0 +1,21 @@
+#!/usr/bin/env -S godot -s
+extends SceneTree
+
+
+var _cli_runner: GdUnitTestCIRunner
+
+
+func _initialize() -> void:
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
+ _cli_runner = GdUnitTestCIRunner.new()
+ root.add_child(_cli_runner)
+
+
+# do not use print statements on _finalize it results in random crashes
+func _finalize() -> void:
+ queue_delete(_cli_runner)
+ if OS.is_stdout_verbose():
+ prints("Finallize ..")
+ prints("-Orphan nodes report-----------------------")
+ Window.print_orphan_nodes()
+ prints("Finallize .. done")
diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid
new file mode 100644
index 00000000..9e751a61
--- /dev/null
+++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid
@@ -0,0 +1 @@
+uid://do2c2faoehm61
diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd
new file mode 100644
index 00000000..8b228051
--- /dev/null
+++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd
@@ -0,0 +1,167 @@
+#!/usr/bin/env -S godot -s
+extends MainLoop
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+# gdlint: disable=max-line-length
+const LOG_FRAME_TEMPLATE = """
+
+
+
+
+
+ Godot Logging
+
+
+
+
+
+${content}
+
+
+
+"""
+
+const NO_LOG_MESSAGE = """
+No logging available!
+
+In order for logging to take place, you must activate the Activate file logging option in the project settings.
+You can enable the logging under:
+Project Settings > Debug > File Logging > Enable File Logging in the project settings.
+"""
+
+#warning-ignore-all:return_value_discarded
+var _cmd_options := CmdOptions.new([
+ CmdOption.new(
+ "-rd, --report-directory",
+ "-rd ",
+ "Specifies the output directory in which the reports are to be written. The default is res://reports/.",
+ TYPE_STRING,
+ true
+ )
+ ])
+
+
+var _report_root_path: String
+var _current_report_path: String
+var _debug_cmd_args := PackedStringArray()
+
+
+func _init() -> void:
+ set_report_directory(GdUnitFileAccess.current_dir() + "reports")
+ set_current_report_path()
+
+
+func _process(_delta: float) -> bool:
+ # check if reports exists
+ if not reports_available():
+ prints("no reports found")
+ return true
+
+ # only process if godot logging is enabled
+ if not GdUnitSettings.is_log_enabled():
+ write_report(NO_LOG_MESSAGE, "")
+ return true
+
+ # parse possible custom report path,
+ var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
+ # ignore erros and exit quitly
+ if cmd_parser.parse(get_cmdline_args(), true).is_error():
+ return true
+ CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory)
+
+ var godot_log_file := scan_latest_godot_log()
+ var result := read_log_file_content(godot_log_file)
+ if result.is_error():
+ write_report(result.error_message(), godot_log_file)
+ return true
+ write_report(result.value_as_string(), godot_log_file)
+ return true
+
+
+func set_current_report_path() -> void:
+ # scan for latest report directory
+ var iteration := GdUnitFileAccess.find_last_path_index(
+ _report_root_path, GdUnitConstants.REPORT_DIR_PREFIX
+ )
+ _current_report_path = "%s/%s%d" % [_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX, iteration]
+
+
+func set_report_directory(path: String) -> void:
+ _report_root_path = path
+
+
+func get_log_report_html() -> String:
+ return _current_report_path + "/godot_report_log.html"
+
+
+func reports_available() -> bool:
+ return DirAccess.dir_exists_absolute(_report_root_path)
+
+
+func scan_latest_godot_log() -> String:
+ var path := GdUnitSettings.get_log_path().get_base_dir()
+ var files_sorted := Array()
+ for file in GdUnitFileAccess.scan_dir(path):
+ var file_name := "%s/%s" % [path, file]
+ files_sorted.append(file_name)
+ # sort by name, the name contains the timestamp so we sort at the end by timestamp
+ files_sorted.sort()
+ return files_sorted.back()
+
+
+func read_log_file_content(log_file: String) -> GdUnitResult:
+ var file := FileAccess.open(log_file, FileAccess.READ)
+ if file == null:
+ return GdUnitResult.error(
+ "Can't find log file '%s'. Error: %s"
+ % [log_file, error_string(FileAccess.get_open_error())]
+ )
+ var content := "" + file.get_as_text()
+ # patch out console format codes
+ for color_index in range(0, 256):
+ var to_replace := "[38;5;%dm" % color_index
+ content = content.replace(to_replace, "")
+ content += "
"
+ content = content\
+ .replace("[0m", "")\
+ .replace(GdUnitCSIMessageWriter.CSI_BOLD, "")\
+ .replace(GdUnitCSIMessageWriter.CSI_ITALIC, "")\
+ .replace(GdUnitCSIMessageWriter.CSI_UNDERLINE, "")
+ return GdUnitResult.success(content)
+
+
+func write_report(content: String, godot_log_file: String) -> GdUnitResult:
+ var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE)
+ if file == null:
+ return GdUnitResult.error(
+ "Can't open to write '%s'. Error: %s"
+ % [get_log_report_html(), error_string(FileAccess.get_open_error())]
+ )
+ var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content)
+ file.store_string(report_html)
+ _update_index_html(godot_log_file)
+ return GdUnitResult.success(file)
+
+
+func _update_index_html(godot_log_file: String) -> void:
+ var index_path := "%s/index.html" % _current_report_path
+ var index_file := FileAccess.open(index_path, FileAccess.READ_WRITE)
+ if index_file == null:
+ push_error(
+ "Can't add log path '%s' to `%s`. Error: %s"
+ % [godot_log_file, index_path, error_string(FileAccess.get_open_error())]
+ )
+ return
+ var content := index_file.get_as_text()\
+ .replace("${log_report}", get_log_report_html())\
+ .replace("${godot_log_file}", godot_log_file)
+ # overide it
+ index_file.seek(0)
+ index_file.store_string(content)
+
+
+func get_cmdline_args() -> PackedStringArray:
+ if _debug_cmd_args.is_empty():
+ return OS.get_cmdline_args()
+ return _debug_cmd_args
diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid
new file mode 100644
index 00000000..d5b381ef
--- /dev/null
+++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid
@@ -0,0 +1 @@
+uid://bretpek2ehht4
diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg
new file mode 100644
index 00000000..ca818277
--- /dev/null
+++ b/addons/gdUnit4/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="gdUnit4"
+description="Unit Testing Framework for Godot Scripts"
+author="Mike Schulze"
+version="6.0.3"
+script="plugin.gd"
diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd
new file mode 100644
index 00000000..822c4d56
--- /dev/null
+++ b/addons/gdUnit4/plugin.gd
@@ -0,0 +1,110 @@
+@tool
+extends EditorPlugin
+
+# We need to define manually the slot id's, to be downwards compatible
+const CONTEXT_SLOT_FILESYSTEM: int = 1 # EditorContextMenuPlugin.CONTEXT_SLOT_FILESYSTEM
+const CONTEXT_SLOT_SCRIPT_EDITOR: int = 2 # EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR
+
+var _gd_inspector: Control
+var _gd_console: Control
+var _gd_filesystem_context_menu: Variant
+var _gd_scripteditor_context_menu: Variant
+
+
+func _enter_tree() -> void:
+
+ var inferred_declaration: int = ProjectSettings.get_setting("debug/gdscript/warnings/inferred_declaration")
+ var exclude_addons: bool = ProjectSettings.get_setting("debug/gdscript/warnings/exclude_addons")
+ if !exclude_addons and inferred_declaration != 0:
+ printerr("GdUnit4: 'inferred_declaration' is set to Warning/Error!")
+ printerr("GdUnit4 is not 'inferred_declaration' save, you have to excluded addons (debug/gdscript/warnings/exclude_addons)")
+ printerr("Loading GdUnit4 Plugin failed.")
+ return
+
+ if check_running_in_test_env():
+ @warning_ignore("return_value_discarded")
+ GdUnitCSIMessageWriter.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!")
+ return
+
+ if Engine.get_version_info().hex < 0x40500:
+ prints("This GdUnit4 plugin version '%s' requires Godot version '4.5' or higher to run." % GdUnit4Version.current())
+ return
+ GdUnitSettings.setup()
+ # Install the GdUnit Inspector
+ _gd_inspector = (load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn") as PackedScene).instantiate()
+ _add_context_menus()
+ add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector)
+ # Install the GdUnit Console
+ _gd_console = (load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn") as PackedScene).instantiate()
+ var control: Control = add_control_to_bottom_panel(_gd_console, "gdUnitConsole")
+ @warning_ignore("unsafe_method_access")
+ await _gd_console.setup_update_notification(control)
+ if GdUnit4CSharpApiLoader.is_api_loaded():
+ prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
+ else:
+ prints("No GdUnit4Net found.")
+ # Connect to be notified for script changes to be able to discover new tests
+ GdUnitTestDiscoverGuard.instance()
+ @warning_ignore("return_value_discarded")
+ resource_saved.connect(_on_resource_saved)
+ prints("Loading GdUnit4 Plugin success")
+
+
+func _exit_tree() -> void:
+ if check_running_in_test_env():
+ return
+ if is_instance_valid(_gd_inspector):
+ remove_control_from_docks(_gd_inspector)
+ _gd_inspector.free()
+ _remove_context_menus()
+ if is_instance_valid(_gd_console):
+ remove_control_from_bottom_panel(_gd_console)
+ _gd_console.free()
+ var gdUnitTools: GDScript = load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+ @warning_ignore("unsafe_method_access")
+ gdUnitTools.dispose_all(true)
+ prints("Unload GdUnit4 Plugin success")
+
+
+func check_running_in_test_env() -> bool:
+ var args: PackedStringArray = OS.get_cmdline_args()
+ args.append_array(OS.get_cmdline_user_args())
+ return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import")
+
+
+func _add_context_menus() -> void:
+ if Engine.get_version_info().hex >= 0x40400:
+ # With Godot 4.4 we have to use the 'add_context_menu_plugin' to register editor context menus
+ _gd_filesystem_context_menu = _preload_gdx_script("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx")
+ call_deferred("add_context_menu_plugin", CONTEXT_SLOT_FILESYSTEM, _gd_filesystem_context_menu)
+ # the CONTEXT_SLOT_SCRIPT_EDITOR is adding to the script panel instead of script editor see https://github.com/godotengine/godot/pull/100556
+ #_gd_scripteditor_context_menu = _preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx")
+ #call_deferred("add_context_menu_plugin", CONTEXT_SLOT_SCRIPT_EDITOR, _gd_scripteditor_context_menu)
+ # so we use the old hacky way to add the context menu
+ _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new())
+ else:
+ # TODO Delete it if the minimum requirement for the plugin is set to Godot 4.4.
+ _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd").new())
+ _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new())
+
+
+func _remove_context_menus() -> void:
+ if is_instance_valid(_gd_filesystem_context_menu):
+ call_deferred("remove_context_menu_plugin", _gd_filesystem_context_menu)
+ if is_instance_valid(_gd_scripteditor_context_menu):
+ call_deferred("remove_context_menu_plugin", _gd_scripteditor_context_menu)
+
+
+func _preload_gdx_script(script_path: String) -> Variant:
+ var script: GDScript = GDScript.new()
+ script.source_code = GdUnitFileAccess.resource_as_string(script_path)
+ script.take_over_path(script_path)
+ var err :Error = script.reload()
+ if err != OK:
+ push_error("Can't create context menu %s, error: %s" % [script_path, error_string(err)])
+ return script.new()
+
+
+func _on_resource_saved(resource: Resource) -> void:
+ if resource is Script:
+ await GdUnitTestDiscoverGuard.instance().discover(resource as Script)
diff --git a/addons/gdUnit4/plugin.gd.uid b/addons/gdUnit4/plugin.gd.uid
new file mode 100644
index 00000000..5e8143a9
--- /dev/null
+++ b/addons/gdUnit4/plugin.gd.uid
@@ -0,0 +1 @@
+uid://bc4fimf6ynr5d
diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd
new file mode 100644
index 00000000..ad5da4d9
--- /dev/null
+++ b/addons/gdUnit4/runtest.cmd
@@ -0,0 +1,62 @@
+@echo off
+setlocal enabledelayedexpansion
+
+:: Initialize variables
+set "godot_binary="
+set "filtered_args="
+
+:: Process all arguments
+set "i=0"
+:parse_args
+if "%~1"=="" goto end_parse_args
+
+if "%~1"=="--godot_binary" (
+ set "godot_binary=%~2"
+ shift
+ shift
+) else (
+ set "filtered_args=!filtered_args! %~1"
+ shift
+)
+goto parse_args
+:end_parse_args
+
+:: If --godot_binary wasn't provided, fallback to environment variable
+if "!godot_binary!"=="" (
+ set "godot_binary=%GODOT_BIN%"
+)
+
+:: Check if we have a godot_binary value from any source
+if "!godot_binary!"=="" (
+ echo Godot binary path is not specified.
+ echo Please either:
+ echo - Set the environment variable: set GODOT_BIN=C:\path\to\godot.exe
+ echo - Or use the --godot_binary argument: --godot_binary C:\path\to\godot.exe
+ exit /b 1
+)
+
+:: Check if the Godot binary exists
+if not exist "!godot_binary!" (
+ echo Error: The specified Godot binary '!godot_binary!' does not exist.
+ exit /b 1
+)
+
+:: Get Godot version and check if it's a mono build
+for /f "tokens=*" %%i in ('"!godot_binary!" --version') do set GODOT_VERSION=%%i
+echo !GODOT_VERSION! | findstr /I "mono" >nul
+if !errorlevel! equ 0 (
+ echo Godot .NET detected
+ echo Compiling c# classes ... Please Wait
+ dotnet build --debug
+ echo done !errorlevel!
+)
+
+:: Run the tests with the filtered arguments
+"!godot_binary!" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd !filtered_args!
+set exit_code=%ERRORLEVEL%
+echo Run tests ends with %exit_code%
+
+:: Run the copy log command
+"!godot_binary!" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd !filtered_args! > nul
+set exit_code2=%ERRORLEVEL%
+exit /b %exit_code%
diff --git a/addons/gdUnit4/runtest.sh b/addons/gdUnit4/runtest.sh
new file mode 100644
index 00000000..f0269efb
--- /dev/null
+++ b/addons/gdUnit4/runtest.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# Check for command-line argument
+godot_binary=""
+filtered_args=""
+
+# Process all arguments with a more compatible approach
+while [ $# -gt 0 ]; do
+ if [ "$1" = "--godot_binary" ] && [ $# -gt 1 ]; then
+ # Get the next argument as the value
+ godot_binary="$2"
+ shift 2
+ else
+ # Keep non-godot_binary arguments for passing to Godot
+ filtered_args="$filtered_args $1"
+ shift
+ fi
+done
+
+# If --godot_binary wasn't provided, fallback to environment variable
+if [ -z "$godot_binary" ]; then
+ godot_binary="$GODOT_BIN"
+fi
+
+# Check if we have a godot_binary value from any source
+if [ -z "$godot_binary" ]; then
+ echo "Godot binary path is not specified."
+ echo "Please either:"
+ echo " - Set the environment variable: export GODOT_BIN=/path/to/godot"
+ echo " - Or use the --godot_binary argument: --godot_binary /path/to/godot"
+ exit 1
+fi
+
+# Check if the Godot binary exists and is executable
+if [ ! -f "$godot_binary" ]; then
+ echo "Error: The specified Godot binary '$godot_binary' does not exist."
+ exit 1
+fi
+
+if [ ! -x "$godot_binary" ]; then
+ echo "Error: The specified Godot binary '$godot_binary' is not executable."
+ exit 1
+fi
+
+# Get Godot version and check if it's a .NET build
+GODOT_VERSION=$("$godot_binary" --version)
+if echo "$GODOT_VERSION" | grep -i "mono" > /dev/null; then
+ echo "Godot .NET detected"
+ echo "Compiling c# classes ... Please Wait"
+ dotnet build --debug
+ echo "done $?"
+fi
+
+# Run the tests with the filtered arguments
+"$godot_binary" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $filtered_args
+exit_code=$?
+echo "Run tests ends with $exit_code"
+
+# Run the copy log command
+"$godot_binary" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd $filtered_args > /dev/null
+exit_code2=$?
+exit $exit_code
diff --git a/addons/gdUnit4/src/Comparator.gd b/addons/gdUnit4/src/Comparator.gd
new file mode 100644
index 00000000..096088a6
--- /dev/null
+++ b/addons/gdUnit4/src/Comparator.gd
@@ -0,0 +1,12 @@
+class_name Comparator
+extends Resource
+
+enum {
+ EQUAL,
+ LESS_THAN,
+ LESS_EQUAL,
+ GREATER_THAN,
+ GREATER_EQUAL,
+ BETWEEN_EQUAL,
+ NOT_BETWEEN_EQUAL,
+}
diff --git a/addons/gdUnit4/src/Comparator.gd.uid b/addons/gdUnit4/src/Comparator.gd.uid
new file mode 100644
index 00000000..73ac82a9
--- /dev/null
+++ b/addons/gdUnit4/src/Comparator.gd.uid
@@ -0,0 +1 @@
+uid://buiskkw1yyuw3
diff --git a/addons/gdUnit4/src/Fuzzers.gd b/addons/gdUnit4/src/Fuzzers.gd
new file mode 100644
index 00000000..c8689df3
--- /dev/null
+++ b/addons/gdUnit4/src/Fuzzers.gd
@@ -0,0 +1,80 @@
+## Factory class providing convenient static methods to create various fuzzer instances.[br]
+##
+## Fuzzers is a utility class that simplifies the creation of different fuzzer types
+## for testing purposes. It provides static factory methods that create pre-configured
+## fuzzers with sensible defaults, making it easier to set up fuzz testing in your
+## test suites without manually instantiating each fuzzer type.[br]
+##
+## This class acts as a central access point for all fuzzer types, improving code
+## readability and reducing boilerplate in test cases.[br]
+##
+## @tutorial(Fuzzing Testing): https://en.wikipedia.org/wiki/Fuzzing
+class_name Fuzzers
+extends Resource
+
+
+## Generates random strings with length between [param min_length] and
+## [param max_length] (inclusive), using characters from [param charset].
+## See [StringFuzzer] for detailed documentation and examples.
+static func rand_str(min_length: int, max_length: int, charset := StringFuzzer.DEFAULT_CHARSET) -> StringFuzzer:
+ return StringFuzzer.new(min_length, max_length, charset)
+
+
+## Creates a [BoolFuzzer] for generating random boolean values.[br]
+##
+## See [BoolFuzzer] for detailed documentation and examples.
+static func boolean() -> BoolFuzzer:
+ return BoolFuzzer.new()
+
+
+## Creates an [IntFuzzer] for generating random integers within a range.[br]
+##
+## Generates random integers between [param from] and [param to] (inclusive)
+## using [constant IntFuzzer.NORMAL] mode.
+## See [IntFuzzer] for detailed documentation and examples.
+static func rangei(from: int, to: int) -> IntFuzzer:
+ return IntFuzzer.new(from, to)
+
+
+## Creates a [FloatFuzzer] for generating random floats within a range.[br]
+##
+## Generates random float values between [param from] and [param to] (inclusive).
+## See [FloatFuzzer] for detailed documentation and examples.
+static func rangef(from: float, to: float) -> FloatFuzzer:
+ return FloatFuzzer.new(from, to)
+
+
+## Creates a [Vector2Fuzzer] for generating random 2D vectors within a range.[br]
+##
+## Generates random Vector2 values where each component is bounded by
+## [param from] and [param to] (inclusive).
+## See [Vector2Fuzzer] for detailed documentation and examples.
+static func rangev2(from: Vector2, to: Vector2) -> Vector2Fuzzer:
+ return Vector2Fuzzer.new(from, to)
+
+
+## Creates a [Vector3Fuzzer] for generating random 3D vectors within a range.[br]
+##
+## Generates random Vector3 values where each component is bounded by
+## [param from] and [param to] (inclusive).
+## See [Vector3Fuzzer] for detailed documentation and examples.
+static func rangev3(from: Vector3, to: Vector3) -> Vector3Fuzzer:
+ return Vector3Fuzzer.new(from, to)
+
+
+## Creates an [IntFuzzer] that generates only even integers.[br]
+##
+## Generates random even integers between [param from] and [param to] (inclusive)
+## using [constant IntFuzzer.EVEN] mode.
+## See [IntFuzzer] for detailed documentation about even number generation.
+static func eveni(from: int, to: int) -> IntFuzzer:
+ return IntFuzzer.new(from, to, IntFuzzer.EVEN)
+
+
+## Creates an [IntFuzzer] that generates only odd integers.[br]
+##
+## Generates random odd integers between [param from] and [param to] (inclusive)
+## using [constant IntFuzzer.ODD] mode.
+## See [IntFuzzer] for detailed documentation about odd number generation.
+static func oddi(from: int, to: int) -> IntFuzzer:
+ return IntFuzzer.new(from, to, IntFuzzer.ODD)
diff --git a/addons/gdUnit4/src/Fuzzers.gd.uid b/addons/gdUnit4/src/Fuzzers.gd.uid
new file mode 100644
index 00000000..1d752c61
--- /dev/null
+++ b/addons/gdUnit4/src/Fuzzers.gd.uid
@@ -0,0 +1 @@
+uid://drfioswpw8u2u
diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd b/addons/gdUnit4/src/GdUnitArrayAssert.gd
new file mode 100644
index 00000000..eeb7ca6b
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd
@@ -0,0 +1,122 @@
+## An Assertion Tool to verify array values
+@abstract class_name GdUnitArrayAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitArrayAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is equal to the given one.
+@abstract func is_equal(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is equal to the given one, ignoring case considerations.
+@abstract func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is not equal to the given one.
+@abstract func is_not_equal(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is not equal to the given one, ignoring case considerations.
+@abstract func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitArrayAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is empty, it has a size of 0.
+@abstract func is_empty() -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is not empty, it has a size of minimum 1.
+@abstract func is_not_empty() -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is the same. [br]
+## Compares the current by object reference equals
+@abstract func is_same(expected: Variant) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array is NOT the same. [br]
+## Compares the current by object reference equals
+@abstract func is_not_same(expected: Variant) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array has a size of given value.
+@abstract func has_size(expectd: int) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains the given values, in any order.[br]
+## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same]
+@abstract func contains(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
+## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly]
+@abstract func contains_exactly(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
+## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order]
+@abstract func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains the given values, in any order.[br]
+## The values are compared by object reference, for deep parameter comparision use [method contains]
+@abstract func contains_same(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
+## The values are compared by object reference, for deep parameter comparision use [method contains_exactly]
+@abstract func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
+## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order]
+@abstract func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array do NOT contains the given values, in any order.[br]
+## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same]
+## [b]Example:[/b]
+## [codeblock]
+## # will succeed
+## assert_array([1, 2, 3, 4, 5]).not_contains(6)
+## # will fail
+## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
+## [/codeblock]
+@abstract func not_contains(...expected: Array) -> GdUnitArrayAssert
+
+
+## Verifies that the current Array do NOT contains the given values, in any order.[br]
+## The values are compared by object reference, for deep parameter comparision use [method not_contains]
+## [b]Example:[/b]
+## [codeblock]
+## # will succeed
+## assert_array([1, 2, 3, 4, 5]).not_contains(6)
+## # will fail
+## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
+## [/codeblock]
+@abstract func not_contains_same(...expected: Array) -> GdUnitArrayAssert
+
+
+## Extracts all values by given function name and optional arguments into a new ArrayAssert.
+## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values
+@abstract func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert
+
+
+## Extracts all values by given extractor's into a new ArrayAssert.
+## If the elements not extractable than the value is converted to `"n.a"`, expecting null values
+## -- The argument type is Array[GdUnitValueExtractor]
+@abstract func extractv(...extractors: Array) -> GdUnitArrayAssert
diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid
new file mode 100644
index 00000000..bf8d079e
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid
@@ -0,0 +1 @@
+uid://byeulsiqvaugq
diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd
new file mode 100644
index 00000000..41382d9f
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitAssert.gd
@@ -0,0 +1,47 @@
+## Base interface of all GdUnit asserts
+@abstract class_name GdUnitAssert
+extends RefCounted
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitAssert
+
+
+## Verifies that the current value is equal to expected one.
+@abstract func is_equal(expected: Variant) -> GdUnitAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitAssert
+
+
+## Overrides the default failure message by given custom message.[br]
+## This function allows you to replace the automatically generated failure message with a more specific
+## or user-friendly message that better describes the test failure context.[br]
+## Usage:
+## [codeblock]
+## # Override with custom context-specific message
+## func test_player_inventory():
+## assert_that(player.get_item_count("sword"))\
+## .override_failure_message("Player should have exactly one sword")\
+## .is_equal(1)
+## [/codeblock]
+@abstract func override_failure_message(message: String) -> GdUnitAssert
+
+
+## Appends a custom message to the failure message.[br]
+## This can be used to add additional information to the generated failure message
+## while keeping the original assertion details for better debugging context.[br]
+## Usage:
+## [codeblock]
+## # Add context to existing failure message
+## func test_player_health():
+## assert_that(player.health)\
+## .append_failure_message("Player was damaged by: %s" % last_damage_source)\
+## .is_greater(0)
+## [/codeblock]
+@abstract func append_failure_message(message: String) -> GdUnitAssert
diff --git a/addons/gdUnit4/src/GdUnitAssert.gd.uid b/addons/gdUnit4/src/GdUnitAssert.gd.uid
new file mode 100644
index 00000000..aa0a9cf2
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitAssert.gd.uid
@@ -0,0 +1 @@
+uid://bmy2nu4w22wia
diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd
new file mode 100644
index 00000000..51385e88
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitAwaiter.gd
@@ -0,0 +1,72 @@
+class_name GdUnitAwaiter
+extends RefCounted
+
+
+# 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
+func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
+ # fail fast if the given source instance invalid
+ var assert_that := GdUnitAssertImpl.new(signal_name)
+ var line_number := GdUnitAssertions.get_line_number()
+ if not is_instance_valid(source):
+ @warning_ignore("return_value_discarded")
+ assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
+ return await (Engine.get_main_loop() as SceneTree).process_frame
+ # fail fast if the given source instance invalid
+ if not is_instance_valid(source):
+ @warning_ignore("return_value_discarded")
+ assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
+ return await await_idle_frame()
+ var awaiter := GdUnitSignalAwaiter.new(timeout_millis)
+ var value :Variant = await awaiter.on_signal(source, signal_name, args)
+ if awaiter.is_interrupted():
+ var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
+ @warning_ignore("return_value_discarded")
+ assert_that.report_error(failure, line_number)
+ return value
+
+
+# Waits for a specified signal sent from the between idle frames and aborts with an error after the specified timeout has elapsed
+# source: the object from which the signal is emitted
+# signal_name: signal name
+# args: the expected signal arguments as an array
+# timeout: the timeout in ms, default is set to 2000ms
+func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
+ var line_number := GdUnitAssertions.get_line_number()
+ # fail fast if the given source instance invalid
+ if not is_instance_valid(source):
+ @warning_ignore("return_value_discarded")
+ GdUnitAssertImpl.new(signal_name)\
+ .report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
+ return await await_idle_frame()
+ var awaiter := GdUnitSignalAwaiter.new(timeout_millis, true)
+ var value :Variant = await awaiter.on_signal(source, signal_name, args)
+ if awaiter.is_interrupted():
+ var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
+ @warning_ignore("return_value_discarded")
+ GdUnitAssertImpl.new(signal_name).report_error(failure, line_number)
+ return value
+
+
+# Waits for for a given amount of milliseconds
+# example:
+# # waits for 100ms
+# await GdUnitAwaiter.await_millis(myNode, 100).completed
+# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
+func await_millis(milliSec :int) -> void:
+ var timer :Timer = Timer.new()
+ timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id())
+ (Engine.get_main_loop() as SceneTree).root.add_child(timer)
+ timer.add_to_group("GdUnitTimers")
+ timer.set_one_shot(true)
+ timer.start(milliSec / 1000.0)
+ await timer.timeout
+ timer.queue_free()
+
+
+# Waits until the next idle frame
+func await_idle_frame() -> void:
+ await (Engine.get_main_loop() as SceneTree).process_frame
diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd.uid b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid
new file mode 100644
index 00000000..5eda3099
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid
@@ -0,0 +1 @@
+uid://c1jp2le4lldby
diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd b/addons/gdUnit4/src/GdUnitBoolAssert.gd
new file mode 100644
index 00000000..714f8fc5
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd
@@ -0,0 +1,35 @@
+## An Assertion Tool to verify boolean values
+@abstract class_name GdUnitBoolAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitBoolAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitBoolAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitBoolAssert
+
+
+## Verifies that the current value is not equal to the given one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitBoolAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitBoolAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitBoolAssert
+
+
+## Verifies that the current value is true.
+@abstract func is_true() -> GdUnitBoolAssert
+
+
+## Verifies that the current value is false.
+@abstract func is_false() -> GdUnitBoolAssert
diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid
new file mode 100644
index 00000000..7e402923
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid
@@ -0,0 +1 @@
+uid://bftfpffmfb1il
diff --git a/addons/gdUnit4/src/GdUnitConstants.gd b/addons/gdUnit4/src/GdUnitConstants.gd
new file mode 100644
index 00000000..e43c75ab
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitConstants.gd
@@ -0,0 +1,10 @@
+class_name GdUnitConstants
+extends RefCounted
+
+const NO_ARG :Variant = "<--null-->"
+
+const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures"
+
+## The maximum number of report history files to store
+const DEFAULT_REPORT_HISTORY_COUNT = 20
+const REPORT_DIR_PREFIX = "report_"
diff --git a/addons/gdUnit4/src/GdUnitConstants.gd.uid b/addons/gdUnit4/src/GdUnitConstants.gd.uid
new file mode 100644
index 00000000..bee2c812
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitConstants.gd.uid
@@ -0,0 +1 @@
+uid://dkap7kpfh2bhg
diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd
new file mode 100644
index 00000000..45cc62a4
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd
@@ -0,0 +1,79 @@
+## An Assertion Tool to verify dictionary
+@abstract class_name GdUnitDictionaryAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitDictionaryAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is equal to the given one, ignoring order.
+@abstract func is_equal(expected: Variant) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is not equal to the given one, ignoring order.
+@abstract func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitDictionaryAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is empty, it has a size of 0.
+@abstract func is_empty() -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is not empty, it has a size of minimum 1.
+@abstract func is_not_empty() -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is the same. [br]
+## Compares the current by object reference equals
+@abstract func is_same(expected: Variant) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary is NOT the same. [br]
+## Compares the current by object reference equals
+@abstract func is_not_same(expected: Variant) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary has a size of given value.
+@abstract func has_size(expected: int) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary contains the given key(s).[br]
+## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys]
+@abstract func contains_keys(...expected: Array) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary contains the given key and value.[br]
+## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value]
+@abstract func contains_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary not contains the given key(s).[br]
+## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys]
+@abstract func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary contains the given key(s).[br]
+## The keys are compared by object reference, for deep parameter comparision use [method contains_keys]
+@abstract func contains_same_keys(expected: Array) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary contains the given key and value.[br]
+## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value]
+@abstract func contains_same_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
+
+
+## Verifies that the current dictionary not contains the given key(s).
+## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys]
+@abstract func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert
diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid
new file mode 100644
index 00000000..45ba6f27
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid
@@ -0,0 +1 @@
+uid://8s1lymhdvlpu
diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd b/addons/gdUnit4/src/GdUnitFailureAssert.gd
new file mode 100644
index 00000000..6fec1910
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd
@@ -0,0 +1,52 @@
+## An assertion tool to verify GDUnit asserts.
+## This assert is for internal use only, to verify that failed asserts work as expected.
+@abstract class_name GdUnitFailureAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitFailureAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitFailureAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitFailureAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitFailureAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitFailureAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitFailureAssert
+
+
+## Verifies if the executed assert was successful
+@abstract func is_success() -> GdUnitFailureAssert
+
+
+## Verifies if the executed assert has failed
+@abstract func is_failed() -> GdUnitFailureAssert
+
+
+## Verifies the failure line is equal to expected one.
+@abstract func has_line(expected: int) -> GdUnitFailureAssert
+
+
+## Verifies the failure message is equal to expected one.
+@abstract func has_message(expected: String) -> GdUnitFailureAssert
+
+
+## Verifies that the failure message starts with the expected message.
+@abstract func starts_with_message(expected: String) -> GdUnitFailureAssert
+
+
+## Verifies that the failure message contains the expected message.
+@abstract func contains_message(expected: String) -> GdUnitFailureAssert
diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid
new file mode 100644
index 00000000..204f1438
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid
@@ -0,0 +1 @@
+uid://x54vf4fue301
diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd b/addons/gdUnit4/src/GdUnitFileAssert.gd
new file mode 100644
index 00000000..771da906
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFileAssert.gd
@@ -0,0 +1,38 @@
+@abstract class_name GdUnitFileAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitFileAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitFileAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitFileAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitFileAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitFileAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitFileAssert
+
+
+@abstract func is_file() -> GdUnitFileAssert
+
+
+@abstract func exists() -> GdUnitFileAssert
+
+
+@abstract func is_script() -> GdUnitFileAssert
+
+
+@abstract func contains_exactly(expected_rows :Array) -> GdUnitFileAssert
diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd.uid b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid
new file mode 100644
index 00000000..d79b837d
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid
@@ -0,0 +1 @@
+uid://vt1hx0i6pg4h
diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd
new file mode 100644
index 00000000..2695ab0e
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd
@@ -0,0 +1,75 @@
+## An Assertion Tool to verify float values
+@abstract class_name GdUnitFloatAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitFloatAssert
+
+
+## Verifies that the current and expected value are approximately equal.
+@abstract func is_equal_approx(expected: float, approx: float) -> GdUnitFloatAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitFloatAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is less than the given one.
+@abstract func is_less(expected: float) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is less than or equal the given one.
+@abstract func is_less_equal(expected: float) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is greater than the given one.
+@abstract func is_greater(expected: float) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is greater than or equal the given one.
+@abstract func is_greater_equal(expected: float) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is negative.
+@abstract func is_negative() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is not negative.
+@abstract func is_not_negative() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is equal to zero.
+@abstract func is_zero() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is not equal to zero.
+@abstract func is_not_zero() -> GdUnitFloatAssert
+
+
+## Verifies that the current value is in the given set of values.
+@abstract func is_in(expected: Array) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is not in the given set of values.
+@abstract func is_not_in(expected: Array) -> GdUnitFloatAssert
+
+
+## Verifies that the current value is between the given boundaries (inclusive).
+@abstract func is_between(from: float, to: float) -> GdUnitFloatAssert
diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid
new file mode 100644
index 00000000..4f3ff1e4
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid
@@ -0,0 +1 @@
+uid://l487wamffax1
diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd
new file mode 100644
index 00000000..e8a49c51
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd
@@ -0,0 +1,42 @@
+## An Assertion Tool to verify function callback values
+@abstract class_name GdUnitFuncAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitFuncAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitFuncAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitFuncAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitFuncAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitFuncAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitFuncAssert
+
+
+## Verifies that the current value is true.
+@abstract func is_true() -> GdUnitFuncAssert
+
+
+## Verifies that the current value is false.
+@abstract func is_false() -> GdUnitFuncAssert
+
+
+## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br]
+## e.g.[br]
+## do wait until 5s the function `is_state` is returns 10 [br]
+## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code]
+@abstract func wait_until(timeout: int) -> GdUnitFuncAssert
diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid
new file mode 100644
index 00000000..f8c037d4
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid
@@ -0,0 +1 @@
+uid://bvvptcdhi1g14
diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd
new file mode 100644
index 00000000..01711f9a
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd
@@ -0,0 +1,59 @@
+## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error().
+@abstract class_name GdUnitGodotErrorAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitGodotErrorAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitGodotErrorAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitGodotErrorAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitGodotErrorAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitGodotErrorAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitGodotErrorAssert
+
+
+## Verifies if the executed code runs without any runtime errors
+## Usage:
+## [codeblock]
+## await assert_error().is_success()
+## [/codeblock]
+@abstract func is_success() -> GdUnitGodotErrorAssert
+
+
+## Verifies if the executed code runs into a runtime error
+## Usage:
+## [codeblock]
+## await assert_error().is_runtime_error()
+## [/codeblock]
+@abstract func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert
+
+
+## Verifies if the executed code has a push_warning() used
+## Usage:
+## [codeblock]
+## await assert_error().is_push_warning()
+## [/codeblock]
+@abstract func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert
+
+
+## Verifies if the executed code has a push_error() used
+## Usage:
+## [codeblock]
+## await assert_error().is_push_error()
+## [/codeblock]
+@abstract func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert
diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid
new file mode 100644
index 00000000..bdad5847
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid
@@ -0,0 +1 @@
+uid://bwkv3a1hhdt88
diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd
new file mode 100644
index 00000000..05eb9223
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitIntAssert.gd
@@ -0,0 +1,79 @@
+## An Assertion Tool to verify integer values
+@abstract class_name GdUnitIntAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitIntAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitIntAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitIntAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitIntAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitIntAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitIntAssert
+
+
+## Verifies that the current value is less than the given one.
+@abstract func is_less(expected: int) -> GdUnitIntAssert
+
+
+## Verifies that the current value is less than or equal the given one.
+@abstract func is_less_equal(expected: int) -> GdUnitIntAssert
+
+
+## Verifies that the current value is greater than the given one.
+@abstract func is_greater(expected: int) -> GdUnitIntAssert
+
+
+## Verifies that the current value is greater than or equal the given one.
+@abstract func is_greater_equal(expected: int) -> GdUnitIntAssert
+
+
+## Verifies that the current value is even.
+@abstract func is_even() -> GdUnitIntAssert
+
+
+## Verifies that the current value is odd.
+@abstract func is_odd() -> GdUnitIntAssert
+
+
+## Verifies that the current value is negative.
+@abstract func is_negative() -> GdUnitIntAssert
+
+
+## Verifies that the current value is not negative.
+@abstract func is_not_negative() -> GdUnitIntAssert
+
+
+## Verifies that the current value is equal to zero.
+@abstract func is_zero() -> GdUnitIntAssert
+
+
+## Verifies that the current value is not equal to zero.
+@abstract func is_not_zero() -> GdUnitIntAssert
+
+
+## Verifies that the current value is in the given set of values.
+@abstract func is_in(expected: Array) -> GdUnitIntAssert
+
+
+## Verifies that the current value is not in the given set of values.
+@abstract func is_not_in(expected: Array) -> GdUnitIntAssert
+
+
+## Verifies that the current value is between the given boundaries (inclusive).
+@abstract func is_between(from: int, to: int) -> GdUnitIntAssert
diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd.uid b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid
new file mode 100644
index 00000000..968a7b3d
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid
@@ -0,0 +1 @@
+uid://ghuy35olsym1
diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd b/addons/gdUnit4/src/GdUnitObjectAssert.gd
new file mode 100644
index 00000000..9d7e76ea
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd
@@ -0,0 +1,51 @@
+## An Assertion Tool to verify Object values
+@abstract class_name GdUnitObjectAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitObjectAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitObjectAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitObjectAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitObjectAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitObjectAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitObjectAssert
+
+
+## Verifies that the current object is the same as the given one.
+@abstract func is_same(expected: Variant) -> GdUnitObjectAssert
+
+
+## Verifies that the current object is not the same as the given one.
+@abstract func is_not_same(expected: Variant) -> GdUnitObjectAssert
+
+
+## Verifies that the current object is an instance of the given type.
+@abstract func is_instanceof(type: Variant) -> GdUnitObjectAssert
+
+
+## Verifies that the current object is not an instance of the given type.
+@abstract func is_not_instanceof(type: Variant) -> GdUnitObjectAssert
+
+
+## Checks whether the current object inherits from the specified type.
+@abstract func is_inheriting(type: Variant) -> GdUnitObjectAssert
+
+
+## Checks whether the current object does NOT inherit from the specified type.
+@abstract func is_not_inheriting(type: Variant) -> GdUnitObjectAssert
diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid
new file mode 100644
index 00000000..1ace27e5
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid
@@ -0,0 +1 @@
+uid://dmunl8xg53sym
diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd b/addons/gdUnit4/src/GdUnitResultAssert.gd
new file mode 100644
index 00000000..01eb8800
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitResultAssert.gd
@@ -0,0 +1,51 @@
+## An Assertion Tool to verify Results
+@abstract class_name GdUnitResultAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitResultAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitResultAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitResultAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitResultAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitResultAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitResultAssert
+
+
+## Verifies that the result is ends up with empty
+@abstract func is_empty() -> GdUnitResultAssert
+
+
+## Verifies that the result is ends up with success
+@abstract func is_success() -> GdUnitResultAssert
+
+
+## Verifies that the result is ends up with warning
+@abstract func is_warning() -> GdUnitResultAssert
+
+
+## Verifies that the result is ends up with error
+@abstract func is_error() -> GdUnitResultAssert
+
+
+## Verifies that the result contains the given message
+@abstract func contains_message(expected: String) -> GdUnitResultAssert
+
+
+## Verifies that the result contains the given value
+@abstract func is_value(expected: Variant) -> GdUnitResultAssert
diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd.uid b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid
new file mode 100644
index 00000000..1ac97d4a
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid
@@ -0,0 +1 @@
+uid://b4n45twg8y2ar
diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd
new file mode 100644
index 00000000..6b11918d
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd
@@ -0,0 +1,325 @@
+## The Scene Runner is a tool used for simulating interactions on a scene.
+## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames.
+## This tool is typically used for integration testing a scene.
+@abstract class_name GdUnitSceneRunner
+extends RefCounted
+
+
+## Simulates that an action has been pressed.[br]
+## [member action] : the action e.g. [code]"ui_up"[/code][br]
+## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
+@abstract func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner
+
+
+## Simulates that an action is pressed.[br]
+## [member action] : the action e.g. [code]"ui_up"[/code][br]
+## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
+@abstract func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner
+
+
+## Simulates that an action has been released.[br]
+## [member action] : the action e.g. [code]"ui_up"[/code][br]
+## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
+@abstract func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner
+
+
+## Simulates that a key has been pressed.[br]
+## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
+## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
+## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
+## [codeblock]
+## func test_key_presssed():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## await runner.simulate_key_pressed(KEY_SPACE)
+## [/codeblock]
+@abstract func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
+
+
+## Simulates that a key is pressed.[br]
+## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
+## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
+## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
+@abstract func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
+
+
+## Simulates that a key has been released.[br]
+## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
+## [member shift_pressed] : false by default set to true if simmulate shift is press[br]
+## [member ctrl_pressed] : false by default set to true if simmulate control is press[br]
+@abstract func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
+
+
+## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br]
+## [member position] : The absolute position in pixels as Vector2
+@abstract func set_mouse_position(position: Vector2) -> GdUnitSceneRunner
+
+
+## Returns the mouse's position in this Viewport using the coordinate system of this Viewport.
+@abstract func get_mouse_position() -> Vector2
+
+
+## Gets the current global mouse position of the current window
+@abstract func get_global_mouse_position() -> Vector2
+
+
+## Simulates a mouse moved to final position.[br]
+## [member position] : The final mouse position
+@abstract func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner
+
+
+## Simulates a mouse move to the relative coordinates (offset).[br]
+## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
+## [br]
+## [member relative] : The relative position, indicating the mouse position offset.[br]
+## [member time] : The time to move the mouse by the relative position in seconds (default is 1 second).[br]
+## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
+## [codeblock]
+## func test_move_mouse():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## await runner.simulate_mouse_move_relative(Vector2(100,100))
+## [/codeblock]
+@abstract func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
+
+
+## Simulates a mouse move to the absolute coordinates.[br]
+## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
+## [br]
+## [member position] : The final position of the mouse.[br]
+## [member time] : The time to move the mouse to the final position in seconds (default is 1 second).[br]
+## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
+## [codeblock]
+## func test_move_mouse():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## await runner.simulate_mouse_move_absolute(Vector2(100,100))
+## [/codeblock]
+@abstract func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
+
+
+## Simulates a mouse button pressed.[br]
+## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
+## [member double_click] : Set to true to simulate a double-click
+@abstract func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
+
+
+## Simulates a mouse button press (holding)[br]
+## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
+## [member double_click] : Set to true to simulate a double-click
+@abstract func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
+
+
+## Simulates a mouse button released.[br]
+## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
+@abstract func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner
+
+
+## Simulates a screen touch is pressed.[br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member position] : The position to touch the screen.[br]
+## [member double_tap] : If true, the touch's state is a double tab.
+@abstract func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
+
+
+## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member position] : The position to touch the screen.[br]
+## [member double_tap] : If true, the touch's state is a double tab.
+@abstract func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
+
+
+## Simulates a screen touch is released.[br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member double_tap] : If true, the touch's state is a double tab.
+@abstract func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner
+
+
+## Simulates a touch drag and drop event to a relative position.[br]
+## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br]
+## [br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member relative] : The relative position, indicating the drag&drop position offset.[br]
+## [member time] : The time to move to the relative position in seconds (default is 1 second).[br]
+## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
+## [codeblock]
+## func test_touch_drag_drop():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## # start drag at position 50,50
+## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
+## # and drop it at final at 150,50 relative (50,50 + 100,0)
+## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0))
+## [/codeblock]
+@abstract func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
+
+
+## Simulates a touch screen drop to the absolute coordinates (offset).[br]
+## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
+## [br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member position] : The final position, indicating the drop position.[br]
+## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
+## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
+## [codeblock]
+## func test_touch_drag_drop():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## # start drag at position 50,50
+## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
+## # and drop it at 100,50
+## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50))
+## [/codeblock]
+@abstract func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
+
+
+## Simulates a complete drag and drop event from one position to another.[br]
+## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br]
+## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
+## [br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member position] : The drag start position, indicating the drag position.[br]
+## [member drop_position] : The drop position, indicating the drop position.[br]
+## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
+## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
+## [codeblock]
+## func test_touch_drag_drop():
+## var runner = scene_runner("res://scenes/simple_scene.tscn")
+## # start drag at position 50,50 and drop it at 100,50
+## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50))
+## [/codeblock]
+@abstract func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
+
+
+## Simulates a touch screen drag event to given position.[br]
+## [member index] : The touch index in the case of a multi-touch event.[br]
+## [member position] : The drag start position, indicating the drag position.[br]
+@abstract func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner
+
+
+## Returns the actual position of the touchscreen drag position by given index.
+## [member index] : The touch index in the case of a multi-touch event.[br]
+@abstract func get_screen_touch_drag_position(index: int) -> Vector2
+
+
+## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br]
+## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life,
+## whilst a value of 0.5 means the game moves at half the regular speed.
+## [member time_factor] : A float representing the simulation speed.[br]
+## - Default is 1.0, meaning the simulation runs at normal speed.[br]
+## - A value of 2.0 means the simulation runs twice as fast as real time.[br]
+## - A value of 0.5 means the simulation runs at half the regular speed.[br]
+@abstract func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner
+
+
+## Simulates scene processing for a certain number of frames.[br]
+## [member frames] : amount of frames to process[br]
+## [member delta_milli] : the time delta between a frame in milliseconds
+@abstract func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner
+
+
+## Simulates scene processing until the given signal is emitted by the scene.[br]
+## [member signal_name] : the signal to stop the simulation[br]
+## [member args] : optional signal arguments to be matched for stop[br]
+@abstract func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner
+
+
+## Simulates scene processing until the given signal is emitted by the given object.[br]
+## [member source] : the object that should emit the signal[br]
+## [member signal_name] : the signal to stop the simulation[br]
+## [member args] : optional signal arguments to be matched for stop
+@abstract func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner
+
+
+## Waits for all input events to be processed by flushing any buffered input events
+## and then awaiting a full cycle of both the process and physics frames.[br]
+## [br]
+## This is typically used to ensure that any simulated or queued inputs are fully
+## processed before proceeding with the next steps in the scene.[br]
+## It's essential for reliable input simulation or when synchronizing logic based
+## on inputs.[br]
+##
+## Usage Example:
+## [codeblock]
+## await await_input_processed() # Ensure all inputs are processed before continuing
+## [/codeblock]
+@abstract func await_input_processed() -> void
+
+
+## The await_func function pauses execution until a specified function in the scene returns a value.[br]
+## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br]
+## [member func_name] : The name of the function to wait for.[br]
+## [member args] : Optional function arguments
+## [br]
+## Usage Example:
+## [codeblock]
+## # Waits for 'calculate_score' function and verifies the result is equal to 100.
+## await_func("calculate_score").is_equal(100)
+## [/codeblock]
+@abstract func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert
+
+
+## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br]
+## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br]
+## [member source] : The object where implements the function.[br]
+## [member func_name] : The name of the function to wait for.[br]
+## [member args] : optional function arguments
+## [br]
+## Usage Example:
+## [codeblock]
+## # Waits for 'calculate_score' function and verifies the result is equal to 100.
+## var my_instance := ScoreCalculator.new()
+## await_func(my_instance, "calculate_score").is_equal(100)
+## [/codeblock]
+@abstract func await_func_on(source: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert
+
+
+## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br]
+## [member signal_name] : The name of the signal to wait for[br]
+## [member args] : The signal arguments as an array[br]
+## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing
+@abstract func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void
+
+
+## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br]
+## [member source] : the object from which the signal is emitted[br]
+## [member signal_name] : The name of the signal to wait for[br]
+## [member args] : The signal arguments as an array[br]
+## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing
+@abstract func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void
+
+
+## Restores the scene window to a windowed mode and brings it to the foreground.[br]
+## This ensures that the scene is visible and active during testing, making it easier to observe and interact with.
+@abstract func move_window_to_foreground() -> GdUnitSceneRunner
+
+
+## Minimizes the scene window to a windowed mode and brings it to the background.[br]
+## This ensures that the scene is hidden during testing.
+@abstract func move_window_to_background() -> GdUnitSceneRunner
+
+
+## Return the current value of the property with the name .[br]
+## [member name] : name of property[br]
+## [member return] : the value of the property
+@abstract func get_property(name: String) -> Variant
+
+
+## Set the value of the property with the name .[br]
+## [member name] : name of property[br]
+## [member value] : value of property[br]
+## [member return] : true|false depending on valid property name.
+@abstract func set_property(name: String, value: Variant) -> bool
+
+
+## executes the function specified by in the scene and returns the result.[br]
+## [member name] : the name of the function to execute[br]
+## [member args] : optional function arguments[br]
+## [member return] : the function result
+@abstract func invoke(name: String, ...args: Array) -> Variant
+
+
+## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br]
+## [member name] : the name of the node to find[br]
+## [member recursive] : enables/disables seraching recursive[br]
+## [member return] : the node if find otherwise null
+@abstract func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node
+
+
+## Access to current running scene
+@abstract func scene() -> Node
diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid
new file mode 100644
index 00000000..a3745db0
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid
@@ -0,0 +1 @@
+uid://dn20c5e8kb3q3
diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd
new file mode 100644
index 00000000..bb975e89
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd
@@ -0,0 +1,46 @@
+## An Assertion Tool to verify for emitted signals until a waiting time
+@abstract class_name GdUnitSignalAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitSignalAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitSignalAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitSignalAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitSignalAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitSignalAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitSignalAssert
+
+
+## Verifies that given signal is emitted until waiting time
+@abstract func is_emitted(name: String, args := []) -> GdUnitSignalAssert
+
+
+## Verifies that given signal is NOT emitted until waiting time
+@abstract func is_not_emitted(name: String, args := []) -> GdUnitSignalAssert
+
+
+## Verifies the signal exists checked the emitter
+@abstract func is_signal_exists(name: String) -> GdUnitSignalAssert
+
+
+## Sets the assert signal timeout in ms, if the time over a failure is reported.[br]
+## e.g.[br]
+## do wait until 5s the instance has emitted the signal `signal_a`[br]
+## [code]assert_signal(instance).wait_until(5000).is_emitted("signal_a")[/code]
+@abstract func wait_until(timeout: int) -> GdUnitSignalAssert
diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid
new file mode 100644
index 00000000..1674748f
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid
@@ -0,0 +1 @@
+uid://572nse6u4l86
diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd b/addons/gdUnit4/src/GdUnitStringAssert.gd
new file mode 100644
index 00000000..2de698bd
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitStringAssert.gd
@@ -0,0 +1,71 @@
+## An Assertion Tool to verify String values
+@abstract class_name GdUnitStringAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitStringAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitStringAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitStringAssert
+
+
+## Verifies that the current String is equal to the given one, ignoring case considerations.
+@abstract func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitStringAssert
+
+
+## Verifies that the current String is not equal to the given one, ignoring case considerations.
+@abstract func is_not_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitStringAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String is empty, it has a length of 0.
+@abstract func is_empty() -> GdUnitStringAssert
+
+
+## Verifies that the current String is not empty, it has a length of minimum 1.
+@abstract func is_not_empty() -> GdUnitStringAssert
+
+
+## Verifies that the current String contains the given String.
+@abstract func contains(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String does not contain the given String.
+@abstract func not_contains(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String does not contain the given String, ignoring case considerations.
+@abstract func contains_ignoring_case(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String does not contain the given String, ignoring case considerations.
+@abstract func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String starts with the given prefix.
+@abstract func starts_with(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String ends with the given suffix.
+@abstract func ends_with(expected: String) -> GdUnitStringAssert
+
+
+## Verifies that the current String has the expected length by used comparator.
+@abstract func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert
diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd.uid b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid
new file mode 100644
index 00000000..0d26dde3
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid
@@ -0,0 +1 @@
+uid://ip241g801xri
diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd
new file mode 100644
index 00000000..48cde7ac
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitTestSuite.gd
@@ -0,0 +1,691 @@
+## The main class for all GdUnit test suites[br]
+## This class is the main class to implement your unit tests[br]
+## You have to extend and implement your test cases as described[br]
+## e.g MyTests.gd [br]
+## [codeblock]
+## extends GdUnitTestSuite
+## # testcase
+## func test_case_a():
+## assert_that("value").is_equal("value")
+## [/codeblock]
+## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/
+
+@icon("res://addons/gdUnit4/src/ui/settings/logo.png")
+class_name GdUnitTestSuite
+extends Node
+
+const NO_ARG :Variant = GdUnitConstants.NO_ARG
+
+### internal runtime variables that must not be overwritten!!!
+@warning_ignore("unused_private_class_variable")
+var __is_skipped := false
+@warning_ignore("unused_private_class_variable")
+var __skip_reason :String = "Unknow."
+var __active_test_case :String
+var __awaiter := __gdunit_awaiter()
+
+
+### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
+### in order to noticeably reduce the loading time of the test suite.
+# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
+# for more detailed info -> https://github.com/godotengine/godot/issues/67400
+func __lazy_load(script_path :String) -> GDScript:
+ return GdUnitAssertions.__lazy_load(script_path)
+
+
+func __gdunit_assert() -> GDScript:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
+
+
+func __gdunit_tools() -> GDScript:
+ return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+
+func __gdunit_file_access() -> GDScript:
+ return __lazy_load("res://addons/gdUnit4/src/core/GdUnitFileAccess.gd")
+
+
+func __gdunit_awaiter() -> Object:
+ return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new()
+
+
+func __gdunit_argument_matchers() -> GDScript:
+ return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd")
+
+
+func __gdunit_object_interactions() -> GDScript:
+ return __lazy_load("res://addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd")
+
+
+## This function is called before a test suite starts[br]
+## You can overwrite to prepare test data or initalizize necessary variables
+func before() -> void:
+ pass
+
+
+## This function is called at least when a test suite is finished[br]
+## You can overwrite to cleanup data created during test running
+func after() -> void:
+ pass
+
+
+## This function is called before a test case starts[br]
+## You can overwrite to prepare test case specific data
+func before_test() -> void:
+ pass
+
+
+## This function is called after the test case is finished[br]
+## You can overwrite to cleanup your test case specific data
+func after_test() -> void:
+ pass
+
+
+func is_failure(_expected_failure :String = NO_ARG) -> bool:
+ return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false
+
+
+func set_active_test_case(test_case :String) -> void:
+ __active_test_case = test_case
+
+
+# === Tools ====================================================================
+# Mapps Godot error number to a readable error message. See at ERROR
+# https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error
+func error_as_string(error_number :int) -> String:
+ return error_string(error_number)
+
+
+## A litle helper to auto freeing your created objects after test execution
+func auto_free(obj :Variant) -> Variant:
+ var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
+
+ assert(execution_context != null, "INTERNAL ERROR: The current execution_context is null! Please report this as bug.")
+ return execution_context.register_auto_free(obj)
+
+
+@warning_ignore("native_method_override")
+func add_child(node :Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void:
+ super.add_child(node, force_readable_name, internal)
+ var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
+ if execution_context != null:
+ execution_context.orphan_monitor_start()
+
+
+## Discard the error message triggered by a timeout (interruption).[br]
+## By default, an interrupted test is reported as an error.[br]
+## This function allows you to change the message to Success when an interrupted error is reported.
+func discard_error_interupted_by_timeout() -> void:
+ @warning_ignore("unsafe_method_access")
+ __gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case)
+
+
+## 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:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_file_access().create_temp_dir(relative_path)
+
+
+## Deletes the temporary base directory[br]
+## Is called automatically after each execution of the test suite
+func clean_temp_dir() -> void:
+ @warning_ignore("unsafe_method_access")
+ __gdunit_file_access().clear_tmp()
+
+
+## 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:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_file_access().create_temp_file(relative_path, file_name, mode)
+
+
+## Reads a resource by given path into a PackedStringArray.
+func resource_as_array(resource_path :String) -> PackedStringArray:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_file_access().resource_as_array(resource_path)
+
+
+## Reads a resource by given path and returned the content as String.
+func resource_as_string(resource_path :String) -> String:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_file_access().resource_as_string(resource_path)
+
+
+## Reads a resource by given path and return Variand translated by str_to_var
+func resource_as_var(resource_path :String) -> Variant:
+ @warning_ignore("unsafe_method_access", "unsafe_cast")
+ return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String)
+
+
+## Waits for given signal to be emitted by until a specified timeout to fail[br]
+## source: the object from which the signal is emitted[br]
+## signal_name: signal name[br]
+## args: the expected signal arguments as an array[br]
+## timeout: the timeout in ms, default is set to 2000ms
+func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout :int = 2000) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ return await __awaiter.await_signal_on(source, signal_name, args, timeout)
+
+
+## Waits until the next idle frame
+func await_idle_frame() -> void:
+ @warning_ignore("unsafe_method_access")
+ await __awaiter.await_idle_frame()
+
+
+## Waits for a given amount of milliseconds[br]
+## example:[br]
+## [codeblock]
+## # waits for 100ms
+## await await_millis(myNode, 100).completed
+## [/codeblock][br]
+## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
+func await_millis(timeout :int) -> void:
+ @warning_ignore("unsafe_method_access")
+ await __awaiter.await_millis(timeout)
+
+
+## Creates a new scene runner to allow simulate interactions checked a scene.[br]
+## The runner will manage the scene instance and release after the runner is released[br]
+## example:[br]
+## [codeblock]
+## # creates a runner by using a instanciated scene
+## var scene = load("res://foo/my_scne.tscn").instantiate()
+## var runner := scene_runner(scene)
+##
+## # or simply creates a runner by using the scene resource path
+## var runner := scene_runner("res://foo/my_scne.tscn")
+## [/codeblock]
+func scene_runner(scene :Variant, verbose := false) -> GdUnitSceneRunner:
+ return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose))
+
+
+# === Mocking & Spy ===========================================================
+
+## do return a default value for primitive types or null
+const RETURN_DEFAULTS = GdUnitMock.RETURN_DEFAULTS
+## do call the real implementation
+const CALL_REAL_FUNC = GdUnitMock.CALL_REAL_FUNC
+## do return a default value for primitive types and a fully mocked value for Object types
+## builds full deep mocked object
+const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB
+
+
+## Creates a mock for given class name
+func mock(clazz :Variant, mock_mode := RETURN_DEFAULTS) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode)
+
+
+## Creates a spy checked given object instance
+func spy(instance :Variant) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance)
+
+
+## Configures a return value for the specified function and used arguments.[br]
+## [b]Example:
+## [codeblock]
+## # overrides the return value of myMock.is_selected() to false
+## do_return(false).on(myMock).is_selected()
+## [/codeblock]
+func do_return(value :Variant) -> GdUnitMock:
+ return GdUnitMock.new(value)
+
+
+## Verifies certain behavior happened at least once or exact number of times
+func verify(obj :Variant, times := 1) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_object_interactions().verify(obj, times)
+
+
+## Verifies no interactions is happen checked this mock or spy
+func verify_no_interactions(obj :Variant) -> GdUnitAssert:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_object_interactions().verify_no_interactions(obj)
+
+
+## Verifies the given mock or spy has any unverified interaction.
+func verify_no_more_interactions(obj :Variant) -> GdUnitAssert:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_object_interactions().verify_no_more_interactions(obj)
+
+
+## Resets the saved function call counters checked a mock or spy
+func reset(obj :Variant) -> void:
+ @warning_ignore("unsafe_method_access")
+ __gdunit_object_interactions().reset(obj)
+
+
+## Starts monitoring the specified source to collect all transmitted signals.[br]
+## The collected signals can then be checked with 'assert_signal'.[br]
+## By default, the specified source is automatically released when the test ends.
+## You can control this behavior by setting auto_free to false if you do not want the source to be automatically freed.[br]
+## Usage:
+## [codeblock]
+## var emitter := monitor_signals(MyEmitter.new())
+## # call the function to send the signal
+## emitter.do_it()
+## # verify the signial is emitted
+## await assert_signal(emitter).is_emitted('my_signal')
+## [/codeblock]
+func monitor_signals(source :Object, _auto_free := true) -> Object:
+ @warning_ignore("unsafe_method_access")
+ __lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\
+ .get_current_context()\
+ .get_signal_collector()\
+ .register_emitter(source, true) # force recreate to start with a fresh monitoring
+ return auto_free(source) if _auto_free else source
+
+
+# === Argument matchers ========================================================
+## Argument matcher to match any argument
+func any() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().any()
+
+
+## Argument matcher to match any boolean value
+func any_bool() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_BOOL)
+
+
+## Argument matcher to match any integer value
+func any_int() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_INT)
+
+
+## Argument matcher to match any float value
+func any_float() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_FLOAT)
+
+
+## Argument matcher to match any String value
+func any_string() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_STRING)
+
+
+## Argument matcher to match any Color value
+func any_color() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_COLOR)
+
+
+## Argument matcher to match any Vector typed value
+func any_vector() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_types([
+ TYPE_VECTOR2,
+ TYPE_VECTOR2I,
+ TYPE_VECTOR3,
+ TYPE_VECTOR3I,
+ TYPE_VECTOR4,
+ TYPE_VECTOR4I,
+ ])
+
+
+## Argument matcher to match any Vector2 value
+func any_vector2() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR2)
+
+
+## Argument matcher to match any Vector2i value
+func any_vector2i() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I)
+
+
+## Argument matcher to match any Vector3 value
+func any_vector3() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR3)
+
+
+## Argument matcher to match any Vector3i value
+func any_vector3i() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I)
+
+
+## Argument matcher to match any Vector4 value
+func any_vector4() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR4)
+
+
+## Argument matcher to match any Vector4i value
+func any_vector4i() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I)
+
+
+## Argument matcher to match any Rect2 value
+func any_rect2() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_RECT2)
+
+
+## Argument matcher to match any Plane value
+func any_plane() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PLANE)
+
+
+## Argument matcher to match any Quaternion value
+func any_quat() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_QUATERNION)
+
+
+## Argument matcher to match any AABB value
+func any_aabb() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_AABB)
+
+
+## Argument matcher to match any Basis value
+func any_basis() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_BASIS)
+
+
+## Argument matcher to match any Transform2D value
+func any_transform_2d() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D)
+
+
+## Argument matcher to match any Transform3D value
+func any_transform_3d() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D)
+
+
+## Argument matcher to match any NodePath value
+func any_node_path() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH)
+
+
+## Argument matcher to match any RID value
+func any_rid() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_RID)
+
+
+## Argument matcher to match any Object value
+func any_object() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_OBJECT)
+
+
+## Argument matcher to match any Dictionary value
+func any_dictionary() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY)
+
+
+## Argument matcher to match any Array value
+func any_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_ARRAY)
+
+
+## Argument matcher to match any PackedByteArray value
+func any_packed_byte_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY)
+
+
+## Argument matcher to match any PackedInt32Array value
+func any_packed_int32_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY)
+
+
+## Argument matcher to match any PackedInt64Array value
+func any_packed_int64_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY)
+
+
+## Argument matcher to match any PackedFloat32Array value
+func any_packed_float32_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY)
+
+
+## Argument matcher to match any PackedFloat64Array value
+func any_packed_float64_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY)
+
+
+## Argument matcher to match any PackedStringArray value
+func any_packed_string_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY)
+
+
+## Argument matcher to match any PackedVector2Array value
+func any_packed_vector2_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY)
+
+
+## Argument matcher to match any PackedVector3Array value
+func any_packed_vector3_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY)
+
+
+## Argument matcher to match any PackedColorArray value
+func any_packed_color_array() -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY)
+
+
+## Argument matcher to match any instance of given class
+func any_class(clazz :Object) -> GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_method_access")
+ return __gdunit_argument_matchers().any_class(clazz)
+
+
+# === value extract utils ======================================================
+## Builds an extractor by given function name and optional arguments
+func extr(func_name :String, args := Array()) -> GdUnitValueExtractor:
+ return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args)
+
+
+## Constructs a tuple by given arguments
+func tuple(arg0 :Variant,
+ arg1 :Variant=NO_ARG,
+ arg2 :Variant=NO_ARG,
+ arg3 :Variant=NO_ARG,
+ arg4 :Variant=NO_ARG,
+ arg5 :Variant=NO_ARG,
+ arg6 :Variant=NO_ARG,
+ arg7 :Variant=NO_ARG,
+ arg8 :Variant=NO_ARG,
+ arg9 :Variant=NO_ARG) -> GdUnitTuple:
+ return GdUnitTuple.new(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
+
+
+# === Asserts ==================================================================
+
+## The common assertion tool to verify values.
+## It checks the given value by type to fit to the best assert
+func assert_that(current :Variant) -> GdUnitAssert:
+ match typeof(current):
+ TYPE_BOOL:
+ return assert_bool(current)
+ TYPE_INT:
+ return assert_int(current)
+ TYPE_FLOAT:
+ return assert_float(current)
+ TYPE_STRING:
+ return assert_str(current)
+ TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I:
+ return assert_vector(current, false)
+ TYPE_DICTIONARY:
+ return assert_dict(current)
+ TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\
+ TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\
+ TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY:
+ return assert_array(current, false)
+ TYPE_OBJECT, TYPE_NIL:
+ return assert_object(current)
+ _:
+ return __gdunit_assert().new(current)
+
+
+## An assertion tool to verify boolean values.
+func assert_bool(current :Variant) -> GdUnitBoolAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify String values.
+func assert_str(current :Variant) -> GdUnitStringAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify integer values.
+func assert_int(current :Variant) -> GdUnitIntAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify float values.
+func assert_float(current :Variant) -> GdUnitFloatAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify Vector values.[br]
+## This assertion supports all vector types.[br]
+## Usage:
+## [codeblock]
+## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001))
+## [/codeblock]
+func assert_vector(current :Variant, type_check := true) -> GdUnitVectorAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current, type_check)
+
+
+## An assertion tool to verify arrays.
+func assert_array(current :Variant, type_check := true) -> GdUnitArrayAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current, type_check)
+
+
+## An assertion tool to verify dictionaries.
+func assert_dict(current :Variant) -> GdUnitDictionaryAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify FileAccess.
+func assert_file(current :Variant) -> GdUnitFileAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current)
+
+
+## An assertion tool to verify Objects.
+func assert_object(current :Variant) -> GdUnitObjectAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current)
+
+
+func assert_result(current :Variant) -> GdUnitResultAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current)
+
+
+## An assertion tool that waits until a certain time for an expected function return value
+func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnitFuncAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args)
+
+
+## An assertion tool to verify for emitted signals until a certain time.
+func assert_signal(instance :Object) -> GdUnitSignalAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance)
+
+
+## An assertion tool to test for failing assertions.[br]
+## This assert is only designed for internal use to verify failing asserts working as expected.[br]
+## Usage:
+## [codeblock]
+## assert_failure(func(): assert_bool(true).is_not_equal(true)) \
+## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
+## [/codeblock]
+func assert_failure(assertion :Callable) -> GdUnitFailureAssert:
+ @warning_ignore("unsafe_method_access")
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion)
+
+
+## An assertion tool to test for failing assertions.[br]
+## This assert is only designed for internal use to verify failing asserts working as expected.[br]
+## Usage:
+## [codeblock]
+## await assert_failure_await(func(): assert_bool(true).is_not_equal(true)) \
+## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
+## [/codeblock]
+func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert:
+ @warning_ignore("unsafe_method_access")
+ return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion)
+
+
+## An assertion tool to verify Godot errors.[br]
+## You can use to verify certain Godot errors like failing assertions, push_error, push_warn.[br]
+## Usage:
+## [codeblock]
+## # tests no error occurred during execution of the code
+## await assert_error(func (): return 0 )\
+## .is_success()
+##
+## # tests a push_error('test error') occured during execution of the code
+## await assert_error(func (): push_error('test error') )\
+## .is_push_error('test error')
+## [/codeblock]
+func assert_error(current :Callable) -> GdUnitGodotErrorAssert:
+ return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current)
+
+
+## Explicitly fails the current test indicating that the feature is not yet implemented.[br]
+## This function is useful during development when you want to write test cases before implementing the actual functionality.[br]
+## It provides a clear indication that the test failure is expected because the feature is still under development.[br]
+## Usage:
+## [codeblock]
+## # Test for a feature that will be implemented later
+## func test_advanced_ai_behavior():
+## assert_not_yet_implemented()
+##
+## [/codeblock]
+func assert_not_yet_implemented() -> void:
+ @warning_ignore("unsafe_method_access")
+ __gdunit_assert().new(null).do_fail()
+
+
+## Explicitly fails the current test with a custom error message.[br]
+## This function reports an error but does not terminate test execution automatically.[br]
+## You must use 'return' after calling fail() to stop the test since GDScript has no exception support.[br]
+## Useful for complex conditional testing scenarios where standard assertions are insufficient.[br]
+## Usage:
+## [codeblock]
+## # Fail test when conditions are not met
+## if !custom_check(player):
+## fail("Player should be alive but has %d health" % player.health)
+## return
+##
+## # Continue with test if conditions pass
+## assert_that(player.health).is_greater(0)
+## [/codeblock]
+func fail(message: String) -> void:
+ @warning_ignore("unsafe_method_access")
+ __gdunit_assert().new(null).report_error(message)
+
+
+# --- internal stuff do not override!!!
+func ResourcePath() -> String:
+ return get_script().resource_path
diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd.uid b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid
new file mode 100644
index 00000000..2dc80ed3
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid
@@ -0,0 +1 @@
+uid://cgbfa4cflb5nl
diff --git a/addons/gdUnit4/src/GdUnitTuple.gd b/addons/gdUnit4/src/GdUnitTuple.gd
new file mode 100644
index 00000000..6c910023
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitTuple.gd
@@ -0,0 +1,28 @@
+## A tuple implementation to hold two or many values
+class_name GdUnitTuple
+extends RefCounted
+
+const NO_ARG :Variant = GdUnitConstants.NO_ARG
+
+var __values :Array = Array()
+
+
+func _init(arg0:Variant,
+ arg1 :Variant=NO_ARG,
+ arg2 :Variant=NO_ARG,
+ arg3 :Variant=NO_ARG,
+ arg4 :Variant=NO_ARG,
+ arg5 :Variant=NO_ARG,
+ arg6 :Variant=NO_ARG,
+ arg7 :Variant=NO_ARG,
+ arg8 :Variant=NO_ARG,
+ arg9 :Variant=NO_ARG) -> void:
+ __values = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
+
+
+func values() -> Array:
+ return __values
+
+
+func _to_string() -> String:
+ return "tuple(%s)" % str(__values)
diff --git a/addons/gdUnit4/src/GdUnitTuple.gd.uid b/addons/gdUnit4/src/GdUnitTuple.gd.uid
new file mode 100644
index 00000000..0a8b36ec
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitTuple.gd.uid
@@ -0,0 +1 @@
+uid://mjqw2uww51fk
diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd b/addons/gdUnit4/src/GdUnitValueExtractor.gd
new file mode 100644
index 00000000..1a344454
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd
@@ -0,0 +1,9 @@
+## This is the base interface for value extraction
+class_name GdUnitValueExtractor
+extends RefCounted
+
+
+## Extracts a value by given implementation
+func extract_value(value :Variant) -> Variant:
+ push_error("Uninplemented func 'extract_value'")
+ return value
diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid
new file mode 100644
index 00000000..40cc1111
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid
@@ -0,0 +1 @@
+uid://2dylh01qtb66
diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd b/addons/gdUnit4/src/GdUnitVectorAssert.gd
new file mode 100644
index 00000000..c186cba2
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd
@@ -0,0 +1,55 @@
+## An Assertion Tool to verify Vector values
+@abstract class_name GdUnitVectorAssert
+extends GdUnitAssert
+
+
+## Verifies that the current value is null.
+@abstract func is_null() -> GdUnitVectorAssert
+
+
+## Verifies that the current value is not null.
+@abstract func is_not_null() -> GdUnitVectorAssert
+
+
+## Verifies that the current value is equal to the given one.
+@abstract func is_equal(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is not equal to expected one.
+@abstract func is_not_equal(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current and expected value are approximately equal.
+@abstract func is_equal_approx(expected: Variant, approx: Variant) -> GdUnitVectorAssert
+
+
+## Overrides the default failure message by given custom message.
+@abstract func override_failure_message(message: String) -> GdUnitVectorAssert
+
+
+## Appends a custom message to the failure message.
+@abstract func append_failure_message(message: String) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is less than the given one.
+@abstract func is_less(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is less than or equal the given one.
+@abstract func is_less_equal(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is greater than the given one.
+@abstract func is_greater(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is greater than or equal the given one.
+@abstract func is_greater_equal(expected: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is between the given boundaries (inclusive).
+@abstract func is_between(from: Variant, to: Variant) -> GdUnitVectorAssert
+
+
+## Verifies that the current value is not between the given boundaries (inclusive).
+@abstract func is_not_between(from: Variant, to: Variant) -> GdUnitVectorAssert
diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid
new file mode 100644
index 00000000..a1926b28
--- /dev/null
+++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid
@@ -0,0 +1 @@
+uid://bcx6bgypklb3e
diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd
new file mode 100644
index 00000000..6be4b3ee
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd
@@ -0,0 +1,25 @@
+# a value provider unsing a callback to get `next` value from a certain function
+class_name CallBackValueProvider
+extends ValueProvider
+
+var _cb :Callable
+var _args :Array
+
+
+func _init(instance :Object, func_name :String, args :Array = Array(), force_error := true) -> void:
+ _cb = Callable(instance, func_name);
+ _args = args
+ if force_error and not _cb.is_valid():
+ push_error("Can't find function '%s' checked instance %s" % [func_name, instance])
+
+
+func get_value() -> Variant:
+ if not _cb.is_valid():
+ return null
+ if _args.is_empty():
+ return await _cb.call()
+ return await _cb.callv(_args)
+
+
+func dispose() -> void:
+ _cb = Callable()
diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid
new file mode 100644
index 00000000..50e1e8a7
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid
@@ -0,0 +1 @@
+uid://r43u2usutiss
diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd
new file mode 100644
index 00000000..2f828fa2
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd
@@ -0,0 +1,13 @@
+# default value provider, simple returns the initial value
+class_name DefaultValueProvider
+extends ValueProvider
+
+var _value: Variant
+
+
+func _init(value: Variant) -> void:
+ _value = value
+
+
+func get_value() -> Variant:
+ return _value
diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid
new file mode 100644
index 00000000..cd08d1f8
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid
@@ -0,0 +1 @@
+uid://coauynw7rnsij
diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd
new file mode 100644
index 00000000..f9bc7aa5
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd
@@ -0,0 +1,692 @@
+class_name GdAssertMessages
+extends Resource
+
+const WARN_COLOR = "#EFF883"
+const ERROR_COLOR = "#CD5C5C"
+const VALUE_COLOR = "#1E90FF"
+const SUB_COLOR := Color(1, 0, 0, .15)
+const ADD_COLOR := Color(0, 1, 0, .15)
+
+
+# Dictionary of control characters and their readable representations
+const CONTROL_CHARS = {
+ "\n": "", # Line Feed
+ "\r": "", # Carriage Return
+ "\t": "", # Tab
+ "\b": "", # Backspace
+ "\f": "", # Form Feed
+ "\v": "", # Vertical Tab
+ "\a": "", # Bell
+ "": "" # Escape
+}
+
+
+static func format_dict(value :Variant) -> String:
+ if not value is Dictionary:
+ return str(value)
+
+ var dict_value: Dictionary = value
+ if dict_value.is_empty():
+ return "{ }"
+ var as_rows := var_to_str(value).split("\n")
+ for index in range( 1, as_rows.size()-1):
+ as_rows[index] = " " + as_rows[index]
+ as_rows[-1] = " " + as_rows[-1]
+ return "\n".join(as_rows)
+
+
+# improved version of InputEvent as text
+static func input_event_as_text(event :InputEvent) -> String:
+ var text := ""
+ if event is InputEventKey:
+ var key_event := event as InputEventKey
+ text += "InputEventKey : key='%s', pressed=%s, keycode=%d, physical_keycode=%s" % [
+ event.as_text(), key_event.pressed, key_event.keycode, key_event.physical_keycode]
+ else:
+ text += event.as_text()
+ if event is InputEventMouse:
+ var mouse_event := event as InputEventMouse
+ text += ", global_position %s" % mouse_event.global_position
+ if event is InputEventWithModifiers:
+ var mouse_event := event as InputEventWithModifiers
+ text += ", shift=%s, alt=%s, control=%s, meta=%s, command=%s" % [
+ mouse_event.shift_pressed,
+ mouse_event.alt_pressed,
+ mouse_event.ctrl_pressed,
+ mouse_event.meta_pressed,
+ mouse_event.command_or_control_autoremap]
+ return text
+
+
+static func _colored_string_div(characters: String) -> String:
+ return colored_array_div(characters.to_utf32_buffer().to_int32_array())
+
+
+static func colored_array_div(characters: PackedInt32Array) -> String:
+ if characters.is_empty():
+ return ""
+ var result := PackedInt32Array()
+ var index := 0
+ var missing_chars := PackedInt32Array()
+ var additional_chars := PackedInt32Array()
+
+ while index < characters.size():
+ var character := characters[index]
+ match character:
+ GdDiffTool.DIV_ADD:
+ index += 1
+ @warning_ignore("return_value_discarded")
+ additional_chars.append(characters[index])
+ GdDiffTool.DIV_SUB:
+ index += 1
+ @warning_ignore("return_value_discarded")
+ missing_chars.append(characters[index])
+ _:
+ if not missing_chars.is_empty():
+ result.append_array(format_chars(missing_chars, SUB_COLOR))
+ missing_chars = PackedInt32Array()
+ if not additional_chars.is_empty():
+ result.append_array(format_chars(additional_chars, ADD_COLOR))
+ additional_chars = PackedInt32Array()
+ @warning_ignore("return_value_discarded")
+ result.append(character)
+ index += 1
+
+ result.append_array(format_chars(missing_chars, SUB_COLOR))
+ result.append_array(format_chars(additional_chars, ADD_COLOR))
+ return result.to_byte_array().get_string_from_utf32()
+
+
+static func _typed_value(value :Variant) -> String:
+ return GdDefaultValueDecoder.decode(value)
+
+
+static func _warning(error :String) -> String:
+ return "[color=%s]%s[/color]" % [WARN_COLOR, error]
+
+
+static func _error(error :String) -> String:
+ return "[color=%s]%s[/color]" % [ERROR_COLOR, error]
+
+
+static func _nerror(number :Variant) -> String:
+ match typeof(number):
+ TYPE_INT:
+ return "[color=%s]%d[/color]" % [ERROR_COLOR, number]
+ TYPE_FLOAT:
+ return "[color=%s]%f[/color]" % [ERROR_COLOR, number]
+ _:
+ return "[color=%s]%s[/color]" % [ERROR_COLOR, str(number)]
+
+
+static func _colored_value(value :Variant) -> String:
+ match typeof(value):
+ TYPE_STRING, TYPE_STRING_NAME:
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _colored_string_div(str(value))]
+ TYPE_INT:
+ return "'[color=%s]%d[/color]'" % [VALUE_COLOR, value]
+ TYPE_FLOAT:
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
+ TYPE_COLOR:
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
+ TYPE_OBJECT:
+ if value == null:
+ return "'[color=%s][/color]'" % [VALUE_COLOR]
+ if value is InputEvent:
+ var ie: InputEvent = value
+ return "[color=%s]<%s>[/color]" % [VALUE_COLOR, input_event_as_text(ie)]
+ var obj_value: Object = value
+ if obj_value.has_method("_to_string"):
+ return "[color=%s]<%s>[/color]" % [VALUE_COLOR, str(value)]
+ return "[color=%s]<%s>[/color]" % [VALUE_COLOR, obj_value.get_class()]
+ TYPE_DICTIONARY:
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, format_dict(value)]
+ _:
+ if GdArrayTools.is_array_type(value):
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, _typed_value(value)]
+ return "'[color=%s]%s[/color]'" % [VALUE_COLOR, value]
+
+
+
+static func _index_report_as_table(index_reports :Array) -> String:
+ var table := "[table=3]$cells[/table]"
+ var header := "[cell][right][b]$text[/b][/right]\t[/cell]"
+ var cell := "[cell][right]$text[/right]\t[/cell]"
+ var cells := header.replace("$text", "Index") + header.replace("$text", "Current") + header.replace("$text", "Expected")
+ for report :Variant in index_reports:
+ var index :String = str(report["index"])
+ var current :String = str(report["current"])
+ var expected :String = str(report["expected"])
+ cells += cell.replace("$text", index) + cell.replace("$text", current) + cell.replace("$text", expected)
+ return table.replace("$cells", cells)
+
+
+static func orphan_detected_on_suite_setup(count :int) -> String:
+ return "%s\n Detected <%d> orphan nodes during test suite setup stage! [b]Check before() and after()![/b]" % [
+ _warning("WARNING:"), count]
+
+
+static func orphan_detected_on_test_setup(count :int) -> String:
+ return "%s\n Detected <%d> orphan nodes during test setup! [b]Check before_test() and after_test()![/b]" % [
+ _warning("WARNING:"), count]
+
+
+static func orphan_detected_on_test(count :int) -> String:
+ return "%s\n Detected <%d> orphan nodes during test execution!" % [
+ _warning("WARNING:"), count]
+
+
+static func fuzzer_interuped(iterations: int, error: String) -> String:
+ return "%s %s %s\n %s" % [
+ _error("Found an error after"),
+ _colored_value(iterations + 1),
+ _error("test iterations"),
+ error]
+
+
+static func test_timeout(timeout :int) -> String:
+ return "%s\n %s" % [_error("Timeout !"), _colored_value("Test timed out after %s" % LocalTime.elapsed(timeout))]
+
+
+# gdlint:disable = mixed-tabs-and-spaces
+static func test_suite_skipped(hint :String, skip_count :int) -> String:
+ return """
+ %s
+ Skipped %s tests
+ Reason: %s
+ """.dedent().trim_prefix("\n")\
+ % [_error("The Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)]
+
+
+static func test_skipped(hint :String) -> String:
+ return """
+ %s
+ Reason: %s
+ """.dedent().trim_prefix("\n")\
+ % [_error("This test is skipped!"), _colored_value(hint)]
+
+
+static func error_not_implemented() -> String:
+ return _error("Test not implemented!")
+
+
+static func error_is_null(current :Variant) -> String:
+ return "%s %s but was %s" % [_error("Expecting:"), _colored_value(null), _colored_value(current)]
+
+
+static func error_is_not_null() -> String:
+ return "%s %s" % [_error("Expecting: not to be"), _colored_value(null)]
+
+
+static func error_equal(current :Variant, expected :Variant, index_reports :Array = []) -> String:
+ var report := """
+ %s
+ %s
+ but was
+ %s""".dedent().trim_prefix("\n") % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
+ if not index_reports.is_empty():
+ report += "\n\n%s\n%s" % [_error("Differences found:"), _index_report_as_table(index_reports)]
+ return report
+
+
+static func error_not_equal(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n not equal to\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
+
+
+static func error_not_equal_case_insensetiv(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n not equal to (case insensitiv)\n %s" % [
+ _error("Expecting:"), _colored_value(expected), _colored_value(current)]
+
+
+static func error_is_empty(current :Variant) -> String:
+ return "%s\n must be empty but was\n %s" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_not_empty() -> String:
+ return "%s\n must not be empty" % [_error("Expecting:")]
+
+
+static func error_is_same(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n to refer to the same object\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
+
+
+@warning_ignore("unused_parameter")
+static func error_not_same(_current :Variant, expected :Variant) -> String:
+ return "%s\n %s" % [_error("Expecting not same:"), _colored_value(expected)]
+
+
+static func error_not_same_error(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)]
+
+
+static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String:
+ return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\
+ _colored_value(expected.or_else(null)), _colored_value(current.or_else(null))]
+
+
+# -- Boolean Assert specific messages -----------------------------------------------------
+static func error_is_true(current :Variant) -> String:
+ return "%s %s but is %s" % [_error("Expecting:"), _colored_value(true), _colored_value(current)]
+
+
+static func error_is_false(current :Variant) -> String:
+ return "%s %s but is %s" % [_error("Expecting:"), _colored_value(false), _colored_value(current)]
+
+
+# - Integer/Float Assert specific messages -----------------------------------------------------
+
+static func error_is_even(current :Variant) -> String:
+ return "%s\n %s must be even" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_odd(current :Variant) -> String:
+ return "%s\n %s must be odd" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_negative(current :Variant) -> String:
+ return "%s\n %s be negative" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_not_negative(current :Variant) -> String:
+ return "%s\n %s be not negative" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_zero(current :Variant) -> String:
+ return "%s\n equal to 0 but is %s" % [_error("Expecting:"), _colored_value(current)]
+
+
+static func error_is_not_zero() -> String:
+ return "%s\n not equal to 0" % [_error("Expecting:")]
+
+
+static func error_is_wrong_type(current_type :Variant.Type, expected_type :Variant.Type) -> String:
+ return "%s\n Expecting type %s but is %s" % [
+ _error("Unexpected type comparison:"),
+ _colored_value(GdObjects.type_as_string(current_type)),
+ _colored_value(GdObjects.type_as_string(expected_type))]
+
+
+static func error_is_value(operation :int, current :Variant, expected :Variant, expected2 :Variant = null) -> String:
+ match operation:
+ Comparator.EQUAL:
+ return "%s\n %s but was '%s'" % [_error("Expecting:"), _colored_value(expected), _nerror(current)]
+ Comparator.LESS_THAN:
+ return "%s\n %s but was '%s'" % [_error("Expecting to be less than:"), _colored_value(expected), _nerror(current)]
+ Comparator.LESS_EQUAL:
+ return "%s\n %s but was '%s'" % [_error("Expecting to be less than or equal:"), _colored_value(expected), _nerror(current)]
+ Comparator.GREATER_THAN:
+ return "%s\n %s but was '%s'" % [_error("Expecting to be greater than:"), _colored_value(expected), _nerror(current)]
+ Comparator.GREATER_EQUAL:
+ return "%s\n %s but was '%s'" % [_error("Expecting to be greater than or equal:"), _colored_value(expected), _nerror(current)]
+ Comparator.BETWEEN_EQUAL:
+ return "%s\n %s\n in range between\n %s <> %s" % [
+ _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
+ Comparator.NOT_BETWEEN_EQUAL:
+ return "%s\n %s\n not in range between\n %s <> %s" % [
+ _error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
+ return "TODO create expected message"
+
+
+static func error_is_in(current :Variant, expected :Array) -> String:
+ return "%s\n %s\n is in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
+
+
+static func error_is_not_in(current :Variant, expected :Array) -> String:
+ return "%s\n %s\n is not in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
+
+
+# - StringAssert ---------------------------------------------------------------------------------
+static func error_equal_ignoring_case(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n but was\n %s (ignoring case)" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
+
+
+static func error_contains(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n do contains\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_not_contains(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n not do contain\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_contains_ignoring_case(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_not_contains_ignoring_case(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n not do contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_starts_with(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n to start with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_ends_with(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n to end with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
+
+
+static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String:
+ @warning_ignore("unsafe_method_access")
+ var current_length :Variant = current.length() if current != null else null
+ match compare_operator:
+ Comparator.EQUAL:
+ return "%s\n %s but was '%s' in\n %s" % [
+ _error("Expecting size:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
+ Comparator.LESS_THAN:
+ return "%s\n %s but was '%s' in\n %s" % [
+ _error("Expecting size to be less than:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
+ Comparator.LESS_EQUAL:
+ return "%s\n %s but was '%s' in\n %s" % [
+ _error("Expecting size to be less than or equal:"), _colored_value(expected),
+ _nerror(current_length), _colored_value(current)]
+ Comparator.GREATER_THAN:
+ return "%s\n %s but was '%s' in\n %s" % [
+ _error("Expecting size to be greater than:"), _colored_value(expected),
+ _nerror(current_length), _colored_value(current)]
+ Comparator.GREATER_EQUAL:
+ return "%s\n %s but was '%s' in\n %s" % [
+ _error("Expecting size to be greater than or equal:"), _colored_value(expected),
+ _nerror(current_length), _colored_value(current)]
+ return "TODO create expected message"
+
+
+# - ArrayAssert specific messgaes ---------------------------------------------------
+
+static func error_arr_contains(current: Variant, expected: Variant, not_expect: Variant, not_found: Variant, by_reference: bool) -> String:
+ var failure_message := "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:"
+ var error := "%s\n %s\n do contains (in any order)\n %s" % [
+ _error(failure_message), _colored_value(current), _colored_value(expected)]
+ if not is_empty(not_expect):
+ error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
+ if not is_empty(not_found):
+ var prefix := "but" if is_empty(not_expect) else "and"
+ error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
+ return error
+
+
+static func error_arr_contains_exactly(
+ current: Variant,
+ expected: Variant,
+ not_expect: Variant,
+ not_found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
+ var failure_message := (
+ "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
+ else "Expecting contains SAME exactly elements:"
+ )
+ if is_empty(not_expect) and is_empty(not_found):
+ var arr_current: Array = current
+ var arr_expected: Array = expected
+ var diff := _find_first_diff(arr_current, arr_expected)
+ return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [
+ _error(failure_message), _colored_value(current), _colored_value(expected), diff]
+
+ var error := "%s\n %s\n do contains (in same order)\n %s" % [
+ _error(failure_message), _colored_value(current), _colored_value(expected)]
+ if not is_empty(not_expect):
+ error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
+ if not is_empty(not_found):
+ var prefix := "but" if is_empty(not_expect) else "and"
+ error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
+ return error
+
+
+static func error_arr_contains_exactly_in_any_order(
+ current: Variant,
+ expected: Variant,
+ not_expect: Variant,
+ not_found: Variant,
+ compare_mode: GdObjects.COMPARE_MODE) -> String:
+
+ var failure_message := (
+ "Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
+ else "Expecting contains SAME exactly elements:"
+ )
+ var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [
+ _error(failure_message), _colored_value(current), _colored_value(expected)]
+ if not is_empty(not_expect):
+ error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
+ if not is_empty(not_found):
+ var prefix := "but" if is_empty(not_expect) else "and"
+ error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
+ return error
+
+
+static func error_arr_not_contains(current: Variant, expected: Variant, found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
+ var failure_message := "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:"
+ var error := "%s\n %s\n do not contains\n %s" % [
+ _error(failure_message), _colored_value(current), _colored_value(expected)]
+ if not is_empty(found):
+ error += "\n but found elements:\n %s" % _colored_value(found)
+ return error
+
+
+# - DictionaryAssert specific messages ----------------------------------------------
+static func error_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
+ var failure := (
+ "Expecting contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
+ else "Expecting contains SAME keys:"
+ )
+ return "%s\n %s\n to contains:\n %s\n but can't find key's:\n %s" % [
+ _error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
+
+
+static func error_not_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
+ var failure := (
+ "Expecting NOT contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
+ else "Expecting NOT contains SAME keys"
+ )
+ return "%s\n %s\n do not contains:\n %s\n but contains key's:\n %s" % [
+ _error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
+
+
+static func error_contains_key_value(key :Variant, value :Variant, current_value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String:
+ var failure := (
+ "Expecting contains key and value:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
+ else "Expecting contains SAME key and value:"
+ )
+ return "%s\n %s : %s\n but contains\n %s : %s" % [
+ _error(failure), _colored_value(key), _colored_value(value), _colored_value(key), _colored_value(current_value)]
+
+
+# - ResultAssert specific errors ----------------------------------------------------
+static func error_result_is_empty(current :GdUnitResult) -> String:
+ return _result_error_message(current, GdUnitResult.EMPTY)
+
+
+static func error_result_is_success(current :GdUnitResult) -> String:
+ return _result_error_message(current, GdUnitResult.SUCCESS)
+
+
+static func error_result_is_warning(current :GdUnitResult) -> String:
+ return _result_error_message(current, GdUnitResult.WARN)
+
+
+static func error_result_is_error(current :GdUnitResult) -> String:
+ return _result_error_message(current, GdUnitResult.ERROR)
+
+
+static func error_result_has_message(current :String, expected :String) -> String:
+ return "%s\n %s\n but was\n %s." % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
+
+
+static func error_result_has_message_on_success(expected :String) -> String:
+ return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)]
+
+
+static func error_result_is_value(current :Variant, expected :Variant) -> String:
+ return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)]
+
+
+static func _result_error_message(current :GdUnitResult, expected_type :int) -> String:
+ if current == null:
+ return _error("Expecting the result must be a %s but was ." % result_type(expected_type))
+ if current.is_success():
+ return _error("Expecting the result must be a %s but was SUCCESS." % result_type(expected_type))
+ var error := "Expecting the result must be a %s but was %s:" % [result_type(expected_type), result_type(current._state)]
+ return "%s\n %s" % [_error(error), _colored_value(result_message(current))]
+
+
+static func error_interrupted(func_name :String, expected :Variant, elapsed :String) -> String:
+ func_name = humanized(func_name)
+ if expected == null:
+ return "%s %s but timed out after %s" % [_error("Expected:"), func_name, elapsed]
+ return "%s %s %s but timed out after %s" % [_error("Expected:"), func_name, _colored_value(expected), elapsed]
+
+
+static func error_wait_signal(signal_name :String, args :Array, elapsed :String) -> String:
+ if args.is_empty():
+ return "%s %s but timed out after %s" % [
+ _error("Expecting emit signal:"), _colored_value(signal_name + "()"), elapsed]
+ return "%s %s but timed out after %s" % [
+ _error("Expecting emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
+
+
+static func error_signal_emitted(signal_name :String, args :Array, elapsed :String) -> String:
+ if args.is_empty():
+ return "%s %s but is emitted after %s" % [
+ _error("Expecting do not emit signal:"), _colored_value(signal_name + "()"), elapsed]
+ return "%s %s but is emitted after %s" % [
+ _error("Expecting do not emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
+
+
+static func error_await_signal_on_invalid_instance(source :Variant, signal_name :String, args :Array) -> String:
+ return "%s\n await_signal_on(%s, %s, %s)" % [
+ _error("Invalid source! Can't await on signal:"), _colored_value(source), signal_name, args]
+
+
+static func result_type(type :int) -> String:
+ match type:
+ GdUnitResult.SUCCESS: return "SUCCESS"
+ GdUnitResult.WARN: return "WARNING"
+ GdUnitResult.ERROR: return "ERROR"
+ GdUnitResult.EMPTY: return "EMPTY"
+ return "UNKNOWN"
+
+
+static func result_message(result :GdUnitResult) -> String:
+ match result._state:
+ GdUnitResult.SUCCESS: return ""
+ GdUnitResult.WARN: return result.warn_message()
+ GdUnitResult.ERROR: return result.error_message()
+ GdUnitResult.EMPTY: return ""
+ return "UNKNOWN"
+# -----------------------------------------------------------------------------------
+
+# - Spy|Mock specific errors ----------------------------------------------------
+static func error_no_more_interactions(summary :Dictionary) -> String:
+ var interactions := PackedStringArray()
+ for args :Array in summary.keys():
+ var times :int = summary[args]
+ @warning_ignore("return_value_discarded")
+ interactions.append(_format_arguments(args, times))
+ return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)]
+
+
+static func error_validate_interactions(current_interactions: Dictionary, expected_interactions: Dictionary) -> String:
+ var collected_interactions := PackedStringArray()
+ for args: Array in current_interactions.keys():
+ var times: int = current_interactions[args]
+ @warning_ignore("return_value_discarded")
+ collected_interactions.append(_format_arguments(args, times))
+
+ var arguments: Array = expected_interactions.keys()[0]
+ var interactions: int = expected_interactions.values()[0]
+ var expected_interaction := _format_arguments(arguments, interactions)
+ return "%s\n%s\n%s\n%s" % [
+ _error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(collected_interactions)]
+
+
+static func _format_arguments(args :Array, times :int) -> String:
+ var fname :String = args[0]
+ var fargs := args.slice(1) as Array
+ var typed_args := _to_typed_args(fargs)
+ var fsignature := _colored_value("%s(%s)" % [fname, ", ".join(typed_args)])
+ return " %s %d time's" % [fsignature, times]
+
+
+static func _to_typed_args(args :Array) -> PackedStringArray:
+ var typed := PackedStringArray()
+ for arg :Variant in args:
+ @warning_ignore("return_value_discarded")
+ typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg)))
+ return typed
+
+
+static func _format_arg(arg :Variant) -> String:
+ if arg is InputEvent:
+ var ie: InputEvent = arg
+ return input_event_as_text(ie)
+ return str(arg)
+
+
+static func _find_first_diff(left :Array, right :Array) -> String:
+ for index in left.size():
+ var l :Variant = left[index]
+ var r :Variant = "" if index >= right.size() else right[index]
+ if not GdObjects.equals(l, r):
+ return "at position %s\n '%s' vs '%s'" % [_colored_value(index), _typed_value(l), _typed_value(r)]
+ return ""
+
+
+static func error_has_size(current :Variant, expected: int) -> String:
+ @warning_ignore("unsafe_method_access")
+ var current_size :Variant = null if current == null else current.size()
+ return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)]
+
+
+static func error_contains_exactly(current: Array, expected: Array) -> String:
+ return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)]
+
+
+static func format_chars(characters: PackedInt32Array, type: Color) -> PackedInt32Array:
+ if characters.size() == 0:# or characters[0] == 10:
+ return characters
+
+ # Replace each control character with its readable form
+ var formatted_text := characters.to_byte_array().get_string_from_utf32()
+ for control_char: String in CONTROL_CHARS:
+ var replace_text: String = CONTROL_CHARS[control_char]
+ formatted_text = formatted_text.replace(control_char, replace_text)
+
+ # Handle special ASCII control characters (0x00-0x1F, 0x7F)
+ var ascii_text := ""
+ for i in formatted_text.length():
+ var character := formatted_text[i]
+ var code := character.unicode_at(0)
+ if code < 0x20 and not CONTROL_CHARS.has(character): # Control characters not handled above
+ ascii_text += "<0x%02X>" % code
+ elif code == 0x7F: # DEL character
+ ascii_text += ""
+ else:
+ ascii_text += character
+
+ var message := "[bgcolor=#%s][color=white]%s[/color][/bgcolor]" % [
+ type.to_html(),
+ ascii_text
+ ]
+
+ var result := PackedInt32Array()
+ result.append_array(message.to_utf32_buffer().to_int32_array())
+ return result
+
+
+static func format_invalid(value :String) -> String:
+ return "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [SUB_COLOR.to_html(), value]
+
+
+static func humanized(value :String) -> String:
+ return value.replace("_", " ")
+
+
+static func build_failure_message(failure :String, additional_failure_message: String, custom_failure_message: String) -> String:
+ var message := failure if custom_failure_message.is_empty() else custom_failure_message
+ if additional_failure_message.is_empty():
+ return message
+ return """
+ %s
+ [color=LIME_GREEN][b]Additional info:[/b][/color]
+ %s""".dedent().trim_prefix("\n") % [message, additional_failure_message]
+
+
+static func is_empty(value: Variant) -> bool:
+ var arry_value: Array = value
+ return arry_value != null and arry_value.is_empty()
diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid
new file mode 100644
index 00000000..ea0b25ef
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid
@@ -0,0 +1 @@
+uid://vl7cfc01g5wl
diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd b/addons/gdUnit4/src/asserts/GdAssertReports.gd
new file mode 100644
index 00000000..06b72edb
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd
@@ -0,0 +1,54 @@
+class_name GdAssertReports
+extends RefCounted
+
+const LAST_ERROR = "last_assert_error_message"
+const LAST_ERROR_LINE = "last_assert_error_line"
+
+
+static func report_success() -> void:
+ GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
+ GdAssertReports.set_last_error_line_number(-1)
+ Engine.remove_meta(LAST_ERROR)
+
+
+static func report_warning(message :String, line_number :int) -> void:
+ GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
+ send_report(GdUnitReport.new().create(GdUnitReport.WARN, line_number, message))
+
+
+static func report_error(message:String, line_number :int) -> void:
+ GdUnitSignals.instance().gdunit_set_test_failed.emit(true)
+ GdAssertReports.set_last_error_line_number(line_number)
+ Engine.set_meta(LAST_ERROR, message)
+ # if we expect to fail we handle as success test
+ if _do_expect_assert_failing():
+ return
+ send_report(GdUnitReport.new().create(GdUnitReport.FAILURE, line_number, message))
+
+
+static func reset_last_error_line_number() -> void:
+ Engine.remove_meta(LAST_ERROR_LINE)
+
+
+static func set_last_error_line_number(line_number :int) -> void:
+ Engine.set_meta(LAST_ERROR_LINE, line_number)
+
+
+static func get_last_error_line_number() -> int:
+ if Engine.has_meta(LAST_ERROR_LINE):
+ return Engine.get_meta(LAST_ERROR_LINE)
+ return -1
+
+
+static func _do_expect_assert_failing() -> bool:
+ if Engine.has_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES):
+ return Engine.get_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES)
+ return false
+
+
+static func current_failure() -> String:
+ return Engine.get_meta(LAST_ERROR)
+
+
+static func send_report(report :GdUnitReport) -> void:
+ GdUnitThreadManager.get_current_context().get_execution_context().add_report(report)
diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid
new file mode 100644
index 00000000..e1b6b858
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid
@@ -0,0 +1 @@
+uid://brxvavm3ml0om
diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd
new file mode 100644
index 00000000..730b5da9
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd
@@ -0,0 +1,436 @@
+class_name GdUnitArrayAssertImpl
+extends GdUnitArrayAssert
+
+
+var _base: GdUnitAssertImpl
+var _current_value_provider: ValueProvider
+var _type_check: bool
+
+
+func _init(current: Variant, type_check := true) -> void:
+ _type_check = type_check
+ _current_value_provider = DefaultValueProvider.new(current)
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not _validate_value_type(current):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event: int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func report_success() -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error: String) -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func _validate_value_type(value: Variant) -> bool:
+ return value == null or GdArrayTools.is_array_type(value)
+
+
+func get_current_value() -> Variant:
+ return _current_value_provider.get_value()
+
+
+func max_length(left: Variant, right: Variant) -> int:
+ var ls := str(left).length()
+ var rs := str(right).length()
+ return rs if ls < rs else ls
+
+
+# gdlint: disable=function-name
+func _toPackedStringArray(value: Variant) -> PackedStringArray:
+ if GdArrayTools.is_array_type(value):
+ @warning_ignore("unsafe_cast")
+ return PackedStringArray(value as Array)
+ return PackedStringArray([str(value)])
+
+
+func _array_equals_div(current: Variant, expected: Variant, case_sensitive: bool = false) -> Array[Array]:
+ var current_value := _toPackedStringArray(current)
+ var expected_value := _toPackedStringArray(expected)
+ var index_report := Array()
+ for index in current_value.size():
+ var c := current_value[index]
+ if index < expected_value.size():
+ var e := expected_value[index]
+ if not GdObjects.equals(c, e, case_sensitive):
+ var length := max_length(c, e)
+ current_value[index] = GdAssertMessages.format_invalid(c.lpad(length))
+ expected_value[index] = e.lpad(length)
+ index_report.push_back({"index": index, "current": c, "expected": e})
+ else:
+ current_value[index] = GdAssertMessages.format_invalid(c)
+ index_report.push_back({"index": index, "current": c, "expected": ""})
+
+ for index in range(current_value.size(), expected_value.size()):
+ var value := expected_value[index]
+ expected_value[index] = GdAssertMessages.format_invalid(value)
+ index_report.push_back({"index": index, "current": "", "expected": value})
+ return [current_value, expected_value, index_report]
+
+
+func _array_div(compare_mode: GdObjects.COMPARE_MODE, left: Array[Variant], right: Array[Variant], _same_order := false) -> Array[Variant]:
+ var not_expect := left.duplicate(true)
+ var not_found := right.duplicate(true)
+ for index_c in left.size():
+ var c: Variant = left[index_c]
+ for index_e in right.size():
+ var e: Variant = right[index_e]
+ if GdObjects.equals(c, e, false, compare_mode):
+ GdArrayTools.erase_value(not_expect, e)
+ GdArrayTools.erase_value(not_found, c)
+ break
+ return [not_expect, not_found]
+
+
+func _contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
+ var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+
+ if current_value == null:
+ return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], expected_value, by_reference))
+ @warning_ignore("unsafe_cast")
+ var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant])
+ #var not_expect := diffs[0] as Array
+ var not_found: Array = diffs[1]
+ if not not_found.is_empty():
+ return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], not_found, by_reference))
+ return report_success()
+
+
+func _contains_exactly(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+
+ if current_value == null:
+ return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected_value, [], expected_value, compare_mode))
+ # has same content in same order
+ if _is_equal(current_value, expected_value, false, compare_mode):
+ return report_success()
+ # check has same elements but in different order
+ if _is_equals_sorted(current_value, expected_value, false, compare_mode):
+ return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, [], [], compare_mode))
+ # find the difference
+ @warning_ignore("unsafe_cast")
+ var diffs := _array_div(compare_mode,
+ current_value as Array[Variant],
+ expected_value as Array[Variant],
+ GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+ var not_expect: Array[Variant] = diffs[0]
+ var not_found: Array[Variant] = diffs[1]
+ return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, not_expect, not_found, compare_mode))
+
+
+func _contains_exactly_in_any_order(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+
+ if current_value == null:
+ return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [],
+ expected_value, compare_mode))
+ # find the difference
+ @warning_ignore("unsafe_cast")
+ var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant], false)
+ var not_expect: Array[Variant] = diffs[0]
+ var not_found: Array[Variant] = diffs[1]
+ if not_expect.is_empty() and not_found.is_empty():
+ return report_success()
+ return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, not_expect,
+ not_found, compare_mode))
+
+
+func _not_contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+ if current_value == null:
+ return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [],
+ expected_value, compare_mode))
+ @warning_ignore("unsafe_cast")
+ var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant])
+ var found: Array[Variant] = diffs[0]
+ @warning_ignore("unsafe_cast")
+ if found.size() == (current_value as Array).size():
+ return report_success()
+ @warning_ignore("unsafe_cast")
+ var diffs2 := _array_div(compare_mode, expected_value as Array[Variant], diffs[1] as Array[Variant])
+ return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected_value, diffs2[0], compare_mode))
+
+
+func is_null() -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitArrayAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(...expected: Array) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant= _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+ if current_value == null and expected_value != null:
+ return report_error(GdAssertMessages.error_equal(null, expected_value))
+
+ if not _is_equal(current_value, expected_value):
+ var diff := _array_equals_div(current_value, expected_value)
+ var expected_as_list := GdArrayTools.as_string(diff[0], false)
+ var current_as_list := GdArrayTools.as_string(diff[1], false)
+ var index_report: Array = diff[2]
+ return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
+ return report_success()
+
+
+# Verifies that the current Array is equal to the given one, ignoring case considerations.
+func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+ if current_value == null and expected_value != null:
+ @warning_ignore("unsafe_cast")
+ return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected_value)))
+
+ if not _is_equal(current_value, expected_value, true):
+ @warning_ignore("unsafe_cast")
+ var diff := _array_equals_div(current_value, expected_value, true)
+ var expected_as_list := GdArrayTools.as_string(diff[0])
+ var current_as_list := GdArrayTools.as_string(diff[1])
+ var index_report: Array = diff[2]
+ return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
+ return report_success()
+
+
+func is_not_equal(...expected: Array) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+
+ if _is_equal(current_value, expected_value):
+ return report_error(GdAssertMessages.error_not_equal(current_value, expected_value))
+ return report_success()
+
+
+func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ var expected_value: Variant = _extract_variadic_value(expected)
+ if not _validate_value_type(expected_value):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
+
+ if _is_equal(current_value, expected_value, true):
+ @warning_ignore("unsafe_cast")
+ var c := GdArrayTools.as_string(current_value as Array)
+ @warning_ignore("unsafe_cast")
+ var e := GdArrayTools.as_string(expected_value)
+ return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e))
+ return report_success()
+
+
+func is_empty() -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ @warning_ignore("unsafe_cast")
+ if current_value == null or (current_value as Array).size() > 0:
+ return report_error(GdAssertMessages.error_is_empty(current_value))
+ return report_success()
+
+
+func is_not_empty() -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ @warning_ignore("unsafe_cast")
+ if current_value != null and (current_value as Array).size() == 0:
+ return report_error(GdAssertMessages.error_is_not_empty())
+ return report_success()
+
+
+@warning_ignore("unused_parameter", "shadowed_global_identifier")
+func is_same(expected: Variant) -> GdUnitArrayAssert:
+ if not _validate_value_type(expected):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
+ var current: Variant = get_current_value()
+ if not is_same(current, expected):
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_is_same(current, expected))
+ return self
+
+
+func is_not_same(expected: Variant) -> GdUnitArrayAssert:
+ if not _validate_value_type(expected):
+ return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
+ var current: Variant = get_current_value()
+ if is_same(current, expected):
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_not_same(current, expected))
+ return self
+
+
+func has_size(expected: int) -> GdUnitArrayAssert:
+ var current_value: Variant = get_current_value()
+ @warning_ignore("unsafe_cast")
+ if current_value == null or (current_value as Array).size() != expected:
+ return report_error(GdAssertMessages.error_has_size(current_value, expected))
+ return report_success()
+
+
+func contains(...expected: Array) -> GdUnitArrayAssert:
+ return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func contains_exactly(...expected: Array) -> GdUnitArrayAssert:
+ return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert:
+ return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func contains_same(...expected: Array) -> GdUnitArrayAssert:
+ return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert:
+ return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert:
+ return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func not_contains(...expected: Array) -> GdUnitArrayAssert:
+ return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func not_contains_same(...expected: Array) -> GdUnitArrayAssert:
+ return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func is_instanceof(expected: Variant) -> GdUnitAssert:
+ @warning_ignore("unsafe_method_access")
+ _base.is_instanceof(expected)
+ return self
+
+
+func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert:
+ var extracted_elements := Array()
+ var args: Array = _extract_variadic_value(func_args)
+ var extractor := GdUnitFuncValueExtractor.new(func_name, args)
+ var current: Variant = get_current_value()
+ if current == null:
+ _current_value_provider = DefaultValueProvider.new(null)
+ else:
+ for element: Variant in current:
+ extracted_elements.append(extractor.extract_value(element))
+ _current_value_provider = DefaultValueProvider.new(extracted_elements)
+ return self
+
+
+func extractv(...extractors: Array) -> GdUnitArrayAssert:
+ var extracted_elements := Array()
+ var current: Variant = get_current_value()
+ if current == null:
+ _current_value_provider = DefaultValueProvider.new(null)
+ else:
+ for element: Variant in current:
+ var ev: Array[Variant] = [
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG,
+ GdUnitTuple.NO_ARG
+ ]
+
+ for index: int in extractors.size():
+ var extractor: GdUnitValueExtractor = extractors[index]
+ ev[index] = extractor.extract_value(element)
+ if extractors.size() > 1:
+ extracted_elements.append(GdUnitTuple.new(ev[0], ev[1], ev[2], ev[3], ev[4], ev[5], ev[6], ev[7], ev[8], ev[9]))
+ else:
+ extracted_elements.append(ev[0])
+ _current_value_provider = DefaultValueProvider.new(extracted_elements)
+ return self
+
+
+## Small helper to support the old expected arguments as single array and variadic arguments
+func _extract_variadic_value(values: Variant) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ if values != null and values.size() == 1 and GdArrayTools.is_array_type(values[0]):
+ return values[0]
+ return values
+
+
+@warning_ignore("incompatible_ternary")
+func _is_equal(
+ left: Variant,
+ right: Variant,
+ case_sensitive := false,
+ compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
+
+ @warning_ignore("unsafe_cast")
+ return GdObjects.equals(
+ (left as Array) if GdArrayTools.is_array_type(left) else left,
+ (right as Array) if GdArrayTools.is_array_type(right) else right,
+ case_sensitive,
+ compare_mode
+ )
+
+
+func _is_equals_sorted(
+ left: Variant,
+ right: Variant,
+ case_sensitive := false,
+ compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
+
+ @warning_ignore("unsafe_cast")
+ return GdObjects.equals_sorted(
+ left as Array,
+ right as Array,
+ case_sensitive,
+ compare_mode)
diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid
new file mode 100644
index 00000000..e49cbab9
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://bx7cehfdh2x4w
diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd
new file mode 100644
index 00000000..9f578c4d
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd
@@ -0,0 +1,80 @@
+class_name GdUnitAssertImpl
+extends GdUnitAssert
+
+
+var _current :Variant
+var _current_failure_message :String = ""
+var _custom_failure_message :String = ""
+var _additional_failure_message: String = ""
+
+
+func _init(current :Variant) -> void:
+ _current = current
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ GdAssertReports.reset_last_error_line_number()
+
+
+
+func failure_message() -> String:
+ return _current_failure_message
+
+
+func current_value() -> Variant:
+ return _current
+
+
+func report_success() -> GdUnitAssert:
+ GdAssertReports.report_success()
+ return self
+
+
+func report_error(failure :String, failure_line_number: int = -1) -> GdUnitAssert:
+ var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
+ GdAssertReports.set_last_error_line_number(line_number)
+ _current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
+ GdAssertReports.report_error(_current_failure_message, line_number)
+ Engine.set_meta("GD_TEST_FAILURE", true)
+ return self
+
+
+func do_fail() -> GdUnitAssert:
+ return report_error(GdAssertMessages.error_not_implemented())
+
+
+func override_failure_message(message: String) -> GdUnitAssert:
+ _custom_failure_message = message
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitAssert:
+ _additional_failure_message = message
+ return self
+
+
+func is_null() -> GdUnitAssert:
+ var current :Variant = current_value()
+ if current != null:
+ return report_error(GdAssertMessages.error_is_null(current))
+ return report_success()
+
+
+func is_not_null() -> GdUnitAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ return report_success()
+
+
+func is_equal(expected: Variant) -> GdUnitAssert:
+ var current: Variant = current_value()
+ if not GdObjects.equals(current, expected):
+ return report_error(GdAssertMessages.error_equal(current, expected))
+ return report_success()
+
+
+func is_not_equal(expected: Variant) -> GdUnitAssert:
+ var current: Variant = current_value()
+ if GdObjects.equals(current, expected):
+ return report_error(GdAssertMessages.error_not_equal(current, expected))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid
new file mode 100644
index 00000000..e60fa2ed
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://cq38mcld2thyl
diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd
new file mode 100644
index 00000000..a5b53c17
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd
@@ -0,0 +1,68 @@
+# Preloads all GdUnit assertions
+class_name GdUnitAssertions
+extends RefCounted
+
+
+@warning_ignore("return_value_discarded")
+func _init() -> void:
+ # preload all gdunit assertions to speedup testsuite loading time
+ # gdlint:disable=private-method-call
+ @warning_ignore_start("return_value_discarded")
+ 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")
+ @warning_ignore_restore("return_value_discarded")
+
+
+### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
+### in order to noticeably reduce the loading time of the test suite.
+# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
+# for more detailed info -> https://github.com/godotengine/godot/issues/67400
+# gdlint:disable=function-name
+static func __lazy_load(script_path :String) -> GDScript:
+ return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE)
+
+
+static func validate_value_type(value :Variant, type :Variant.Type) -> bool:
+ return value == null or typeof(value) == type
+
+
+# Scans the current stack trace for the root cause to extract the line number
+static func get_line_number() -> int:
+ var stack_trace := get_stack()
+ if stack_trace == null or stack_trace.is_empty():
+ return -1
+ for index in stack_trace.size():
+ var stack_info :Dictionary = stack_trace[index]
+ var function :String = stack_info.get("function")
+ # we catch helper asserts to skip over to return the correct line number
+ if function.begins_with("assert_"):
+ continue
+ if function.begins_with("test_"):
+ return stack_info.get("line")
+ var source :String = stack_info.get("source")
+ if source.is_empty() \
+ or source.begins_with("user://") \
+ or source.ends_with("GdUnitAssert.gd") \
+ or source.ends_with("GdUnitAssertions.gd") \
+ or source.ends_with("AssertImpl.gd") \
+ or source.ends_with("GdUnitTestSuite.gd") \
+ or source.ends_with("GdUnitSceneRunnerImpl.gd") \
+ or source.ends_with("GdUnitObjectInteractions.gd") \
+ or source.ends_with("GdUnitObjectInteractionsVerifier.gd") \
+ or source.ends_with("GdUnitAwaiter.gd"):
+ continue
+ return stack_info.get("line")
+ return -1
diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid
new file mode 100644
index 00000000..a8600308
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid
@@ -0,0 +1 @@
+uid://61d7pdgldg0r
diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd
new file mode 100644
index 00000000..2fc0ce43
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd
@@ -0,0 +1,87 @@
+extends GdUnitBoolAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not GdUnitAssertions.validate_value_type(current, TYPE_BOOL):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitBoolAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitBoolAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+func is_true() -> GdUnitBoolAssert:
+ if current_value() != true:
+ return report_error(GdAssertMessages.error_is_true(current_value()))
+ return report_success()
+
+
+func is_false() -> GdUnitBoolAssert:
+ if current_value() == true || current_value() == null:
+ return report_error(GdAssertMessages.error_is_false(current_value()))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid
new file mode 100644
index 00000000..76e9a3a0
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://cxndss6mdq7de
diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd
new file mode 100644
index 00000000..c57bbc23
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd
@@ -0,0 +1,207 @@
+extends GdUnitDictionaryAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not GdUnitAssertions.validate_value_type(current, TYPE_DICTIONARY):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitDictionaryAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func report_success() -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func is_null() -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitDictionaryAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected)))
+ if not GdObjects.equals(current, expected):
+ var c := GdAssertMessages.format_dict(current)
+ var e := GdAssertMessages.format_dict(expected)
+ return report_error(GdAssertMessages.error_equal(c, e))
+ return report_success()
+
+
+func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert:
+ var current: Variant = current_value()
+ if GdObjects.equals(current, expected):
+ return report_error(GdAssertMessages.error_not_equal(current, expected))
+ return report_success()
+
+
+@warning_ignore("unused_parameter", "shadowed_global_identifier")
+func is_same(expected :Variant) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected)))
+ if not is_same(current, expected):
+ var c := GdAssertMessages.format_dict(current)
+ var e := GdAssertMessages.format_dict(expected)
+ return report_error(GdAssertMessages.error_is_same(c, e))
+ return report_success()
+
+
+@warning_ignore("unused_parameter", "shadowed_global_identifier")
+func is_not_same(expected :Variant) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ if is_same(current, expected):
+ return report_error(GdAssertMessages.error_not_same(current, expected))
+ return report_success()
+
+
+func is_empty() -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or not (current as Dictionary).is_empty():
+ return report_error(GdAssertMessages.error_is_empty(current))
+ return report_success()
+
+
+func is_not_empty() -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or (current as Dictionary).is_empty():
+ return report_error(GdAssertMessages.error_is_not_empty())
+ return report_success()
+
+
+func has_size(expected: int) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ @warning_ignore("unsafe_cast")
+ if (current as Dictionary).size() != expected:
+ return report_error(GdAssertMessages.error_has_size(current, expected))
+ return report_success()
+
+
+func _contains_keys(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ var expected_value: Array = _extract_variadic_value(expected)
+ if current == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ # find expected keys
+ @warning_ignore("unsafe_cast")
+ var keys_not_found :Array = expected_value.filter(_filter_by_key.bind((current as Dictionary).keys(), compare_mode))
+ if not keys_not_found.is_empty():
+ @warning_ignore("unsafe_cast")
+ return report_error(GdAssertMessages.error_contains_keys((current as Dictionary).keys() as Array, expected_value,
+ keys_not_found, compare_mode))
+ return report_success()
+
+
+func _contains_key_value(key :Variant, value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ var expected := [key]
+ if current == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ var dict_current: Dictionary = current
+ var keys_not_found :Array = expected.filter(_filter_by_key.bind(dict_current.keys(), compare_mode))
+ if not keys_not_found.is_empty():
+ return report_error(GdAssertMessages.error_contains_keys(dict_current.keys() as Array, expected, keys_not_found, compare_mode))
+ if not GdObjects.equals(dict_current[key], value, false, compare_mode):
+ return report_error(GdAssertMessages.error_contains_key_value(key, value, dict_current[key], compare_mode))
+ return report_success()
+
+
+func _not_contains_keys(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert:
+ var current :Variant = current_value()
+ var expected_value: Array = _extract_variadic_value(expected)
+ if current == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ var dict_current: Dictionary = current
+ var keys_found :Array = dict_current.keys().filter(_filter_by_key.bind(expected_value, compare_mode, true))
+ if not keys_found.is_empty():
+ return report_error(GdAssertMessages.error_not_contains_keys(dict_current.keys() as Array, expected_value, keys_found, compare_mode))
+ return report_success()
+
+
+func contains_keys(...expected: Array) -> GdUnitDictionaryAssert:
+ return _contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func contains_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
+ return _contains_key_value(key, value, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert:
+ return _not_contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
+
+
+func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert:
+ return _contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func contains_same_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert:
+ return _contains_key_value(key, value, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert:
+ return _not_contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
+
+
+func _filter_by_key(element :Variant, values :Array, compare_mode :GdObjects.COMPARE_MODE, is_not :bool = false) -> bool:
+ for key :Variant in values:
+ if GdObjects.equals(key, element, false, compare_mode):
+ return is_not
+ return !is_not
+
+
+## Small helper to support the old expected arguments as single array and variadic arguments
+func _extract_variadic_value(values: Variant) -> Variant:
+ @warning_ignore("unsafe_method_access")
+ if values != null and values.size() == 1 and GdArrayTools.is_array_type(values[0]):
+ return values[0]
+ return values
diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid
new file mode 100644
index 00000000..10a744ef
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://dqrp7csbeyvon
diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd
new file mode 100644
index 00000000..198624c6
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd
@@ -0,0 +1,136 @@
+extends GdUnitFailureAssert
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+var _is_failed := false
+var _failure_message: String
+var _current_failure_message := ""
+var _custom_failure_message := ""
+var _additional_failure_message := ""
+
+
+func _set_do_expect_fail(enabled :bool = true) -> void:
+ Engine.set_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES, enabled)
+
+
+func execute_and_await(assertion :Callable, do_await := true) -> GdUnitFailureAssert:
+ # do not report any failure from the original assertion we want to test
+ _set_do_expect_fail(true)
+ var thread_context := GdUnitThreadManager.get_current_context()
+ thread_context.set_assert(null)
+ @warning_ignore("return_value_discarded")
+ GdUnitSignals.instance().gdunit_set_test_failed.connect(_on_test_failed)
+ # execute the given assertion as callable
+ if do_await:
+ await assertion.call()
+ else:
+ assertion.call()
+ _set_do_expect_fail(false)
+ # get the assert instance from current tread context
+ var current_assert := thread_context.get_assert()
+ if not is_instance_of(current_assert, GdUnitAssert):
+ _is_failed = true
+ _failure_message = "Invalid Callable! It must be a callable of 'GdUnitAssert'"
+ return self
+ @warning_ignore("unsafe_method_access")
+ _failure_message = current_assert.failure_message()
+ return self
+
+
+func execute(assertion :Callable) -> GdUnitFailureAssert:
+ @warning_ignore("return_value_discarded")
+ execute_and_await(assertion, false)
+ return self
+
+
+func _on_test_failed(value :bool) -> void:
+ _is_failed = value
+
+
+func is_equal(_expected: Variant) -> GdUnitFailureAssert:
+ return _report_error("Not implemented")
+
+
+func is_not_equal(_expected: Variant) -> GdUnitFailureAssert:
+ return _report_error("Not implemented")
+
+
+func is_null() -> GdUnitFailureAssert:
+ return _report_error("Not implemented")
+
+
+func is_not_null() -> GdUnitFailureAssert:
+ return _report_error("Not implemented")
+
+
+func override_failure_message(message: String) -> GdUnitFailureAssert:
+ _custom_failure_message = message
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitFailureAssert:
+ _additional_failure_message = message
+ return self
+
+
+func is_success() -> GdUnitFailureAssert:
+ if _is_failed:
+ return _report_error("Expect: assertion ends successfully.")
+ return self
+
+
+func is_failed() -> GdUnitFailureAssert:
+ if not _is_failed:
+ return _report_error("Expect: assertion fails.")
+ return self
+
+
+func has_line(expected :int) -> GdUnitFailureAssert:
+ var current := GdAssertReports.get_last_error_line_number()
+ if current != expected:
+ return _report_error("Expect: to failed on line '%d'\n but was '%d'." % [expected, current])
+ return self
+
+
+func has_message(expected :String) -> GdUnitFailureAssert:
+ @warning_ignore("return_value_discarded")
+ is_failed()
+ var expected_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(expected))
+ var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
+ if current_error != expected_error:
+ var diffs := GdDiffTool.string_diff(current_error, expected_error)
+ var current := GdAssertMessages.colored_array_div(diffs[1])
+ return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
+ return self
+
+
+func contains_message(expected :String) -> GdUnitFailureAssert:
+ var expected_error := GdUnitTools.normalize_text(expected)
+ var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
+ if not current_error.contains(expected_error):
+ var diffs := GdDiffTool.string_diff(current_error, expected_error)
+ var current := GdAssertMessages.colored_array_div(diffs[1])
+ return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
+ return self
+
+
+func starts_with_message(expected :String) -> GdUnitFailureAssert:
+ var expected_error := GdUnitTools.normalize_text(expected)
+ var current_error := GdUnitTools.normalize_text(GdUnitTools.richtext_normalize(_failure_message))
+ if current_error.find(expected_error) != 0:
+ var diffs := GdDiffTool.string_diff(current_error, expected_error)
+ var current := GdAssertMessages.colored_array_div(diffs[1])
+ return _report_error(GdAssertMessages.error_not_same_error(current, expected_error))
+ return self
+
+
+func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert:
+ var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
+ _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message)
+ GdAssertReports.report_error(_current_failure_message, line_number)
+ return self
+
+
+func _report_success() -> GdUnitFailureAssert:
+ GdAssertReports.report_success()
+ return self
diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid
new file mode 100644
index 00000000..61645532
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://cbrj7dsr235i0
diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd
new file mode 100644
index 00000000..c4f9570e
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd
@@ -0,0 +1,116 @@
+extends GdUnitFileAssert
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not GdUnitAssertions.validate_value_type(current, TYPE_STRING):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitFileAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func current_value() -> String:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitFileAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+func is_file() -> GdUnitFileAssert:
+ var current := current_value()
+ if FileAccess.open(current, FileAccess.READ) == null:
+ return report_error("Is not a file '%s', error code %s" % [current, FileAccess.get_open_error()])
+ return report_success()
+
+
+func exists() -> GdUnitFileAssert:
+ var current := current_value()
+ if not FileAccess.file_exists(current):
+ return report_error("The file '%s' not exists" %current)
+ return report_success()
+
+
+func is_script() -> GdUnitFileAssert:
+ var current := current_value()
+ if FileAccess.open(current, FileAccess.READ) == null:
+ return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()])
+
+ var script := load(current)
+ if not script is GDScript:
+ return report_error("The file '%s' is not a GdScript" % current)
+ return report_success()
+
+
+func contains_exactly(expected_rows: Array) -> GdUnitFileAssert:
+ var current := current_value()
+ if FileAccess.open(current, FileAccess.READ) == null:
+ return report_error("Can't acces the file '%s'! Error code %s" % [current, FileAccess.get_open_error()])
+
+ var script: GDScript = load(current)
+ if script is GDScript:
+ var source_code := GdScriptParser.to_unix_format(script.source_code)
+ var rows := Array(source_code.split("\n"))
+ @warning_ignore("return_value_discarded")
+ GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows)
+ return self
diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid
new file mode 100644
index 00000000..7141b12b
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://2s6h0titid8y
diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd
new file mode 100644
index 00000000..83d7e05e
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd
@@ -0,0 +1,159 @@
+extends GdUnitFloatAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not GdUnitAssertions.validate_value_type(current, TYPE_FLOAT):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitFloatAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitFloatAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+@warning_ignore("shadowed_global_identifier")
+func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert:
+ return is_between(expected-approx, expected+approx)
+
+
+func is_less(expected :float) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current >= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
+ return report_success()
+
+
+func is_less_equal(expected :float) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current > expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
+ return report_success()
+
+
+func is_greater(expected :float) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current <= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
+ return report_success()
+
+
+func is_greater_equal(expected :float) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current < expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
+ return report_success()
+
+
+func is_negative() -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current >= 0.0:
+ return report_error(GdAssertMessages.error_is_negative(current))
+ return report_success()
+
+
+func is_not_negative() -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current < 0.0:
+ return report_error(GdAssertMessages.error_is_not_negative(current))
+ return report_success()
+
+
+func is_zero() -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or not is_equal_approx(0.00000000, current as float):
+ return report_error(GdAssertMessages.error_is_zero(current))
+ return report_success()
+
+
+func is_not_zero() -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or is_equal_approx(0.00000000, current as float):
+ return report_error(GdAssertMessages.error_is_not_zero())
+ return report_success()
+
+
+func is_in(expected :Array) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if not expected.has(current):
+ return report_error(GdAssertMessages.error_is_in(current, expected))
+ return report_success()
+
+
+func is_not_in(expected :Array) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if expected.has(current):
+ return report_error(GdAssertMessages.error_is_not_in(current, expected))
+ return report_success()
+
+
+func is_between(from :float, to :float) -> GdUnitFloatAssert:
+ var current :Variant = current_value()
+ if current == null or current < from or current > to:
+ return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid
new file mode 100644
index 00000000..a5add1a5
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://dvce6xeybbh1i
diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd
new file mode 100644
index 00000000..c38acf08
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd
@@ -0,0 +1,177 @@
+extends GdUnitFuncAssert
+
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+const DEFAULT_TIMEOUT := 2000
+
+
+var _current_value_provider :ValueProvider
+var _current_failure_message :String = ""
+var _custom_failure_message :String = ""
+var _additional_failure_message: String = ""
+var _line_number := -1
+var _timeout := DEFAULT_TIMEOUT
+var _interrupted := false
+var _sleep_timer :Timer = null
+
+
+func _init(instance :Object, func_name :String, args := Array()) -> void:
+ _line_number = GdUnitAssertions.get_line_number()
+ GdAssertReports.reset_last_error_line_number()
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ # verify at first the function name exists
+ if not instance.has_method(func_name):
+ @warning_ignore("return_value_discarded")
+ report_error("The function '%s' do not exists checked instance '%s'." % [func_name, instance])
+ _interrupted = true
+ else:
+ _current_value_provider = CallBackValueProvider.new(instance, func_name, args)
+
+
+func _notification(what :int) -> void:
+ if what == NOTIFICATION_PREDELETE:
+ _interrupted = true
+ var main_node :Node = (Engine.get_main_loop() as SceneTree).root
+ if is_instance_valid(_current_value_provider):
+ _current_value_provider.dispose()
+ _current_value_provider = null
+ if is_instance_valid(_sleep_timer):
+ _sleep_timer.set_wait_time(0.0001)
+ _sleep_timer.stop()
+ main_node.remove_child(_sleep_timer)
+ _sleep_timer.free()
+ _sleep_timer = null
+
+
+func report_success() -> GdUnitFuncAssert:
+ GdAssertReports.report_success()
+ return self
+
+
+func report_error(failure :String) -> GdUnitFuncAssert:
+ _current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
+ GdAssertReports.report_error(_current_failure_message, _line_number)
+ return self
+
+
+func failure_message() -> String:
+ return _current_failure_message
+
+
+func override_failure_message(message: String) -> GdUnitFuncAssert:
+ _custom_failure_message = message
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitFuncAssert:
+ _additional_failure_message = message
+ return self
+
+
+func wait_until(timeout := 2000) -> GdUnitFuncAssert:
+ if timeout <= 0:
+ push_warning("Invalid timeout param, alloed timeouts must be grater than 0. Use default timeout instead")
+ _timeout = DEFAULT_TIMEOUT
+ else:
+ _timeout = timeout
+ return self
+
+
+func is_null() -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_null)
+ return self
+
+
+func is_not_null() -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_not_null)
+ return self
+
+
+func is_false() -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_false)
+ return self
+
+
+func is_true() -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_true)
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_equal, expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitFuncAssert:
+ await _validate_callback(cb_is_not_equal, expected)
+ return self
+
+
+# we need actually to define this Callable as functions otherwise we results into leaked scripts here
+# this is actually a Godot bug and needs this kind of workaround
+func cb_is_null(c :Variant, _e :Variant) -> bool: return c == null
+func cb_is_not_null(c :Variant, _e :Variant) -> bool: return c != null
+func cb_is_false(c :Variant, _e :Variant) -> bool: return c == false
+func cb_is_true(c :Variant, _e :Variant) -> bool: return c == true
+func cb_is_equal(c :Variant, e :Variant) -> bool: return GdObjects.equals(c,e)
+func cb_is_not_equal(c :Variant, e :Variant) -> bool: return not GdObjects.equals(c, e)
+
+
+func do_interrupt() -> void:
+ _interrupted = true
+
+
+func _validate_callback(predicate :Callable, expected :Variant = null) -> void:
+ if _interrupted:
+ return
+ GdUnitMemoryObserver.guard_instance(self)
+ var time_scale := Engine.get_time_scale()
+ var timer := Timer.new()
+ timer.set_name("gdunit_funcassert_interrupt_timer_%d" % timer.get_instance_id())
+ var scene_tree := Engine.get_main_loop() as SceneTree
+ scene_tree.root.add_child(timer)
+ timer.add_to_group("GdUnitTimers")
+ @warning_ignore("return_value_discarded")
+ timer.timeout.connect(do_interrupt, CONNECT_DEFERRED)
+ timer.set_one_shot(true)
+ timer.start((_timeout/1000.0)*time_scale)
+ _sleep_timer = Timer.new()
+ _sleep_timer.set_name("gdunit_funcassert_sleep_timer_%d" % _sleep_timer.get_instance_id() )
+ scene_tree.root.add_child(_sleep_timer)
+
+ while true:
+ var current :Variant = await next_current_value()
+ # is interupted or predicate success
+ if _interrupted or predicate.call(current, expected):
+ break
+ if is_instance_valid(_sleep_timer):
+ _sleep_timer.start(0.05)
+ await _sleep_timer.timeout
+
+ _sleep_timer.stop()
+ await scene_tree.process_frame
+ if _interrupted:
+ # https://github.com/godotengine/godot/issues/73052
+ #var predicate_name = predicate.get_method()
+ var predicate_name :String = str(predicate).split('::')[1]
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_interrupted(
+ predicate_name.strip_edges().trim_prefix("cb_"),
+ expected,
+ LocalTime.elapsed(_timeout)
+ )
+ )
+ else:
+ @warning_ignore("return_value_discarded")
+ report_success()
+ _sleep_timer.free()
+ timer.free()
+ GdUnitMemoryObserver.unguard_instance(self)
+
+
+func next_current_value() -> Variant:
+ @warning_ignore("redundant_await")
+ if is_instance_valid(_current_value_provider):
+ return await _current_value_provider.get_value()
+ return "invalid value"
diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid
new file mode 100644
index 00000000..dbde7c4f
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://c2jdw0vv5nldq
diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd
new file mode 100644
index 00000000..fc010db3
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd
@@ -0,0 +1,141 @@
+extends GdUnitGodotErrorAssert
+
+var _current_failure_message := ""
+var _custom_failure_message := ""
+var _additional_failure_message := ""
+var _callable: Callable
+
+
+func _init(callable: Callable) -> void:
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ GdAssertReports.reset_last_error_line_number()
+ _callable = callable
+
+
+func _execute() -> Array[ErrorLogEntry]:
+ # execute the given code and monitor for runtime errors
+ if _callable == null or not _callable.is_valid():
+ @warning_ignore("return_value_discarded")
+ _report_error("Invalid Callable '%s'" % _callable)
+ else:
+ await _callable.call()
+ return await _error_monitor().scan(true)
+
+
+func _error_monitor() -> GodotGdErrorMonitor:
+ return GdUnitThreadManager.get_current_context().get_execution_context().error_monitor
+
+
+func failure_message() -> String:
+ return _current_failure_message
+
+
+func _report_success() -> GdUnitAssert:
+ GdAssertReports.report_success()
+ return self
+
+
+func _report_error(error_message: String, failure_line_number: int = -1) -> GdUnitAssert:
+ var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number()
+ _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message)
+ GdAssertReports.report_error(_current_failure_message, line_number)
+ return self
+
+
+func _has_log_entry(log_entries: Array[ErrorLogEntry], type: ErrorLogEntry.TYPE, error: Variant) -> bool:
+ for entry in log_entries:
+ if entry._type == type and GdObjects.equals(entry._message, error):
+ # Erase the log entry we already handled it by this assertion, otherwise it will report at twice
+ _error_monitor().erase_log_entry(entry)
+ return true
+ return false
+
+
+func _to_list(log_entries: Array[ErrorLogEntry]) -> String:
+ if log_entries.is_empty():
+ return "no errors"
+ if log_entries.size() == 1:
+ return log_entries[0]._message
+ var value := ""
+ for entry in log_entries:
+ value += "'%s'\n" % entry._message
+ return value
+
+
+func is_null() -> GdUnitGodotErrorAssert:
+ return _report_error("Not implemented")
+
+
+func is_not_null() -> GdUnitGodotErrorAssert:
+ return _report_error("Not implemented")
+
+
+func is_equal(_expected: Variant) -> GdUnitGodotErrorAssert:
+ return _report_error("Not implemented")
+
+
+func is_not_equal(_expected: Variant) -> GdUnitGodotErrorAssert:
+ return _report_error("Not implemented")
+
+
+func override_failure_message(message: String) -> GdUnitGodotErrorAssert:
+ _custom_failure_message = message
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitGodotErrorAssert:
+ _additional_failure_message = message
+ return self
+
+
+func is_success() -> GdUnitGodotErrorAssert:
+ var log_entries := await _execute()
+ if log_entries.is_empty():
+ return _report_success()
+ return _report_error("""
+ Expecting: no error's are ocured.
+ but found: '%s'
+ """.dedent().trim_prefix("\n") % _to_list(log_entries))
+
+
+func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert:
+ var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error)
+ if result.is_error():
+ return _report_error(result.error_message())
+ var log_entries := await _execute()
+ if _has_log_entry(log_entries, ErrorLogEntry.TYPE.SCRIPT_ERROR, expected_error):
+ return _report_success()
+ return _report_error("""
+ Expecting: a runtime error is triggered.
+ message: '%s'
+ found: %s
+ """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)])
+
+
+func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert:
+ var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_warning)
+ if result.is_error():
+ return _report_error(result.error_message())
+ var log_entries := await _execute()
+ if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_WARNING, expected_warning):
+ return _report_success()
+ return _report_error("""
+ Expecting: push_warning() is called.
+ message: '%s'
+ found: %s
+ """.dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)])
+
+
+func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert:
+ var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error)
+ if result.is_error():
+ return _report_error(result.error_message())
+ var log_entries := await _execute()
+ if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_ERROR, expected_error):
+ return _report_success()
+ return _report_error("""
+ Expecting: push_error() is called.
+ message: '%s'
+ found: %s
+ """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)])
diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid
new file mode 100644
index 00000000..6da674e1
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://cyi6ooahncq7q
diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd
new file mode 100644
index 00000000..bdee249e
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd
@@ -0,0 +1,166 @@
+extends GdUnitIntAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not GdUnitAssertions.validate_value_type(current, TYPE_INT):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitIntAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitIntAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+func is_less(expected :int) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current >= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
+ return report_success()
+
+
+func is_less_equal(expected :int) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current > expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
+ return report_success()
+
+
+func is_greater(expected :int) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current <= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
+ return report_success()
+
+
+func is_greater_equal(expected :int) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current < expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
+ return report_success()
+
+
+func is_even() -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current % 2 != 0:
+ return report_error(GdAssertMessages.error_is_even(current))
+ return report_success()
+
+
+func is_odd() -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current % 2 == 0:
+ return report_error(GdAssertMessages.error_is_odd(current))
+ return report_success()
+
+
+func is_negative() -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current >= 0:
+ return report_error(GdAssertMessages.error_is_negative(current))
+ return report_success()
+
+
+func is_not_negative() -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current < 0:
+ return report_error(GdAssertMessages.error_is_not_negative(current))
+ return report_success()
+
+
+func is_zero() -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current != 0:
+ return report_error(GdAssertMessages.error_is_zero(current))
+ return report_success()
+
+
+func is_not_zero() -> GdUnitIntAssert:
+ var current :Variant= current_value()
+ if current == 0:
+ return report_error(GdAssertMessages.error_is_not_zero())
+ return report_success()
+
+
+func is_in(expected :Array) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if not expected.has(current):
+ return report_error(GdAssertMessages.error_is_in(current, expected))
+ return report_success()
+
+
+func is_not_in(expected :Array) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if expected.has(current):
+ return report_error(GdAssertMessages.error_is_not_in(current, expected))
+ return report_success()
+
+
+func is_between(from :int, to :int) -> GdUnitIntAssert:
+ var current :Variant = current_value()
+ if current == null or current < from or current > to:
+ return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid
new file mode 100644
index 00000000..2424b41a
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://j4mpmwm2hw61
diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd
new file mode 100644
index 00000000..955e1ef3
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd
@@ -0,0 +1,166 @@
+extends GdUnitObjectAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current: Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if (current != null
+ and (GdUnitAssertions.validate_value_type(current, TYPE_BOOL)
+ or GdUnitAssertions.validate_value_type(current, TYPE_INT)
+ or GdUnitAssertions.validate_value_type(current, TYPE_FLOAT)
+ or GdUnitAssertions.validate_value_type(current, TYPE_STRING))):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event: int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error: String) -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+func is_null() -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitObjectAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+@warning_ignore("shadowed_global_identifier")
+func is_same(expected: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if not is_same(current, expected):
+ return report_error(GdAssertMessages.error_is_same(current, expected))
+ return report_success()
+
+
+func is_not_same(expected: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if is_same(current, expected):
+ return report_error(GdAssertMessages.error_not_same(current, expected))
+ return report_success()
+
+
+func is_instanceof(type: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if current == null or not is_instance_of(current, type):
+ var result_expected := GdObjects.extract_class_name(type)
+ var result_current := GdObjects.extract_class_name(current)
+ return report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected))
+ return report_success()
+
+
+func is_not_instanceof(type: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if is_instance_of(current, type):
+ var result := GdObjects.extract_class_name(type)
+ if result.is_success():
+ return report_error("Expected not be a instance of <%s>" % str(result.value()))
+
+ push_error("Internal ERROR: %s" % result.error_message())
+ return self
+ return report_success()
+
+
+## Checks whether the current object inherits from the specified type.
+func is_inheriting(type: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if not is_instance_of(current, TYPE_OBJECT):
+ return report_error("Expected '%s' to inherit from at least Object." % str(current))
+ var result := _inherits(current, type)
+ if result.is_success():
+ return report_success()
+ return report_error(result.error_message())
+
+
+## Checks whether the current object does NOT inherit from the specified type.
+func is_not_inheriting(type: Variant) -> GdUnitObjectAssert:
+ var current: Variant = current_value()
+ if not is_instance_of(current, TYPE_OBJECT):
+ return report_error("Expected '%s' to inherit from at least Object." % str(current))
+ var result := _inherits(current, type)
+ if result.is_success():
+ return report_error("Expected type to not inherit from <%s>" % _extract_class_type(type))
+ return report_success()
+
+
+func _inherits(current: Variant, type: Variant) -> GdUnitResult:
+ var type_as_string := _extract_class_type(type)
+ if type_as_string == "Object":
+ return GdUnitResult.success("")
+
+ var obj: Object = current
+ for p in obj.get_property_list():
+ var clazz_name :String = p["name"]
+ if p["usage"] == PROPERTY_USAGE_CATEGORY and clazz_name == p["hint_string"] and clazz_name == type_as_string:
+ return GdUnitResult.success("")
+ var script: Script = obj.get_script()
+ if script != null:
+ while script != null:
+ var result := GdObjects.extract_class_name(script)
+ if result.is_success() and result.value() == type_as_string:
+ return GdUnitResult.success("")
+ script = script.get_base_script()
+ return GdUnitResult.error("Expected type to inherit from <%s>" % type_as_string)
+
+
+func _extract_class_type(type: Variant) -> String:
+ if type is String:
+ return type
+ var result := GdObjects.extract_class_name(type)
+ if result.is_error():
+ return ""
+ return result.value()
diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid
new file mode 100644
index 00000000..5db4c8b3
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://bm6qm58a0dacq
diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd
new file mode 100644
index 00000000..98a6768f
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd
@@ -0,0 +1,128 @@
+extends GdUnitResultAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not validate_value_type(current):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitResultAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func validate_value_type(value :Variant) -> bool:
+ return value == null or value is GdUnitResult
+
+
+func current_value() -> GdUnitResult:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitResultAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitResultAssert:
+ return is_value(expected)
+
+
+func is_not_equal(expected: Variant) -> GdUnitResultAssert:
+ var result := current_value()
+ var value :Variant = null if result == null else result.value()
+ if GdObjects.equals(value, expected):
+ return report_error(GdAssertMessages.error_not_equal(value, expected))
+ return report_success()
+
+
+func is_empty() -> GdUnitResultAssert:
+ var result := current_value()
+ if result == null or not result.is_empty():
+ return report_error(GdAssertMessages.error_result_is_empty(result))
+ return report_success()
+
+
+func is_success() -> GdUnitResultAssert:
+ var result := current_value()
+ if result == null or not result.is_success():
+ return report_error(GdAssertMessages.error_result_is_success(result))
+ return report_success()
+
+
+func is_warning() -> GdUnitResultAssert:
+ var result := current_value()
+ if result == null or not result.is_warn():
+ return report_error(GdAssertMessages.error_result_is_warning(result))
+ return report_success()
+
+
+func is_error() -> GdUnitResultAssert:
+ var result := current_value()
+ if result == null or not result.is_error():
+ return report_error(GdAssertMessages.error_result_is_error(result))
+ return report_success()
+
+
+func contains_message(expected :String) -> GdUnitResultAssert:
+ var result := current_value()
+ if result == null:
+ return report_error(GdAssertMessages.error_result_has_message("", expected))
+ if result.is_success():
+ return report_error(GdAssertMessages.error_result_has_message_on_success(expected))
+ if result.is_error() and result.error_message() != expected:
+ return report_error(GdAssertMessages.error_result_has_message(result.error_message(), expected))
+ if result.is_warn() and result.warn_message() != expected:
+ return report_error(GdAssertMessages.error_result_has_message(result.warn_message(), expected))
+ return report_success()
+
+
+func is_value(expected: Variant) -> GdUnitResultAssert:
+ var result := current_value()
+ var value :Variant = null if result == null else result.value()
+ if not GdObjects.equals(value, expected):
+ return report_error(GdAssertMessages.error_result_is_value(value, expected))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid
new file mode 100644
index 00000000..6d1ed11d
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://b0dlq6jyjcvps
diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd
new file mode 100644
index 00000000..6f5878c8
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd
@@ -0,0 +1,143 @@
+extends GdUnitSignalAssert
+
+const DEFAULT_TIMEOUT := 2000
+
+var _signal_collector :GdUnitSignalCollector
+var _emitter :Object
+var _current_failure_message :String = ""
+var _custom_failure_message :String = ""
+var _additional_failure_message: String = ""
+var _line_number := -1
+var _timeout := DEFAULT_TIMEOUT
+var _interrupted := false
+
+
+func _init(emitter :Object) -> void:
+ # save the actual assert instance on the current thread context
+ var context := GdUnitThreadManager.get_current_context()
+ context.set_assert(self)
+ _signal_collector = context.get_signal_collector()
+ _line_number = GdUnitAssertions.get_line_number()
+ _emitter = emitter
+ GdAssertReports.reset_last_error_line_number()
+
+
+func _notification(what :int) -> void:
+ if what == NOTIFICATION_PREDELETE:
+ _interrupted = true
+ if is_instance_valid(_emitter):
+ _signal_collector.unregister_emitter(_emitter)
+ _emitter = null
+
+
+func report_success() -> GdUnitAssert:
+ GdAssertReports.report_success()
+ return self
+
+
+func report_warning(message :String) -> GdUnitAssert:
+ GdAssertReports.report_warning(message, GdUnitAssertions.get_line_number())
+ return self
+
+
+func report_error(failure :String) -> GdUnitAssert:
+ _current_failure_message = GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
+ GdAssertReports.report_error(_current_failure_message, _line_number)
+ return self
+
+
+func failure_message() -> String:
+ return _current_failure_message
+
+
+func override_failure_message(message: String) -> GdUnitSignalAssert:
+ _custom_failure_message = message
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitSignalAssert:
+ _additional_failure_message = message
+ return self
+
+
+func wait_until(timeout := 2000) -> GdUnitSignalAssert:
+ if timeout <= 0:
+ @warning_ignore("return_value_discarded")
+ report_warning("Invalid timeout parameter, allowed timeouts must be greater than 0, use default timeout instead!")
+ _timeout = DEFAULT_TIMEOUT
+ else:
+ _timeout = timeout
+ return self
+
+
+func is_null() -> GdUnitSignalAssert:
+ if _emitter != null:
+ return report_error(GdAssertMessages.error_is_null(_emitter))
+ return report_success()
+
+
+func is_not_null() -> GdUnitSignalAssert:
+ if _emitter == null:
+ return report_error(GdAssertMessages.error_is_not_null())
+ return report_success()
+
+
+func is_equal(_expected: Variant) -> GdUnitSignalAssert:
+ return report_error("Not implemented")
+
+
+func is_not_equal(_expected: Variant) -> GdUnitSignalAssert:
+ return report_error("Not implemented")
+
+
+# Verifies the signal exists checked the emitter
+func is_signal_exists(signal_name :String) -> GdUnitSignalAssert:
+ if not _emitter.has_signal(signal_name):
+ @warning_ignore("return_value_discarded")
+ report_error("The signal '%s' not exists checked object '%s'." % [signal_name, _emitter.get_class()])
+ return self
+
+
+# Verifies that given signal is emitted until waiting time
+func is_emitted(name :String, args := []) -> GdUnitSignalAssert:
+ _line_number = GdUnitAssertions.get_line_number()
+ return await _wail_until_signal(name, args, false)
+
+
+# Verifies that given signal is NOT emitted until waiting time
+func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert:
+ _line_number = GdUnitAssertions.get_line_number()
+ return await _wail_until_signal(name, args, true)
+
+
+func _wail_until_signal(signal_name :String, expected_args :Array, expect_not_emitted: bool) -> GdUnitSignalAssert:
+ if _emitter == null:
+ return report_error("Can't wait for signal checked a NULL object.")
+ # first verify the signal is defined
+ if not _emitter.has_signal(signal_name):
+ return report_error("Can't wait for non-existion signal '%s' checked object '%s'." % [signal_name,_emitter.get_class()])
+ _signal_collector.register_emitter(_emitter)
+ var time_scale := Engine.get_time_scale()
+ var timer := Timer.new()
+ (Engine.get_main_loop() as SceneTree).root.add_child(timer)
+ timer.add_to_group("GdUnitTimers")
+ timer.set_one_shot(true)
+ @warning_ignore("return_value_discarded")
+ timer.timeout.connect(func on_timeout() -> void: _interrupted = true)
+ timer.start((_timeout/1000.0)*time_scale)
+ var is_signal_emitted := false
+ while not _interrupted and not is_signal_emitted:
+ await (Engine.get_main_loop() as SceneTree).process_frame
+ if is_instance_valid(_emitter):
+ is_signal_emitted = _signal_collector.match(_emitter, signal_name, expected_args)
+ if is_signal_emitted and expect_not_emitted:
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_signal_emitted(signal_name, expected_args, LocalTime.elapsed(int(_timeout-timer.time_left*1000))))
+
+ if _interrupted and not expect_not_emitted:
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_wait_signal(signal_name, expected_args, LocalTime.elapsed(_timeout)))
+ timer.free()
+ if is_instance_valid(_emitter):
+ _signal_collector.reset_received_signals(_emitter, signal_name, expected_args)
+ return self
diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid
new file mode 100644
index 00000000..0feeb0f2
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://dlh37yc086vr5
diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd
new file mode 100644
index 00000000..cdbcdff8
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd
@@ -0,0 +1,208 @@
+extends GdUnitStringAssert
+
+var _base: GdUnitAssertImpl
+
+
+func _init(current :Variant) -> void:
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if current != null and typeof(current) != TYPE_STRING and typeof(current) != TYPE_STRING_NAME:
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitStringAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func override_failure_message(message: String) -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message: String) -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitStringAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitStringAssert:
+ return _is_equal(expected, false, GdAssertMessages.error_equal)
+
+
+func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert:
+ return _is_equal(expected, true, GdAssertMessages.error_equal_ignoring_case)
+
+
+@warning_ignore_start("unsafe_call_argument")
+func _is_equal(expected: Variant, ignore_case: bool, message_cb: Callable) -> GdUnitStringAssert:
+ var current: Variant = current_value()
+ if current == null:
+ return report_error(message_cb.call(current, expected))
+ var cur_value := str(current)
+ if not GdObjects.equals(cur_value, expected, ignore_case):
+ var exp_value := str(expected)
+ if contains_bbcode(cur_value):
+ # mask user bbcode
+ # https://docs.godotengine.org/en/4.5/tutorials/ui/bbcode_in_richtextlabel.html#handling-user-input-safely
+ return report_error(message_cb.call(cur_value.replace("[", "[lb]"), exp_value.replace("[", "[lb]")))
+ var diffs := GdDiffTool.string_diff(cur_value, exp_value)
+ var formatted_current := GdAssertMessages.colored_array_div(diffs[1])
+ return report_error(message_cb.call(formatted_current, exp_value))
+ return report_success()
+@warning_ignore_restore("unsafe_call_argument")
+
+
+func is_not_equal(expected: Variant) -> GdUnitStringAssert:
+ var current: Variant = current_value()
+ if GdObjects.equals(current, expected):
+ return report_error(GdAssertMessages.error_not_equal(current, expected))
+ return report_success()
+
+
+func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ if GdObjects.equals(current, expected, true):
+ return report_error(GdAssertMessages.error_not_equal(current, expected))
+ return report_success()
+
+
+func is_empty() -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or not (current as String).is_empty():
+ return report_error(GdAssertMessages.error_is_empty(current))
+ return report_success()
+
+
+func is_not_empty() -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or (current as String).is_empty():
+ return report_error(GdAssertMessages.error_is_not_empty())
+ return report_success()
+
+
+func contains(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or (current as String).find(expected) == -1:
+ return report_error(GdAssertMessages.error_contains(current, expected))
+ return report_success()
+
+
+func not_contains(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current != null and (current as String).find(expected) != -1:
+ return report_error(GdAssertMessages.error_not_contains(current, expected))
+ return report_success()
+
+
+func contains_ignoring_case(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or (current as String).findn(expected) == -1:
+ return report_error(GdAssertMessages.error_contains_ignoring_case(current, expected))
+ return report_success()
+
+
+func not_contains_ignoring_case(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current != null and (current as String).findn(expected) != -1:
+ return report_error(GdAssertMessages.error_not_contains_ignoring_case(current, expected))
+ return report_success()
+
+
+func starts_with(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ @warning_ignore("unsafe_cast")
+ if current == null or (current as String).find(expected) != 0:
+ return report_error(GdAssertMessages.error_starts_with(current, expected))
+ return report_success()
+
+
+func ends_with(expected :String) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_ends_with(current, expected))
+ @warning_ignore("unsafe_cast")
+ var find :int = (current as String).length() - expected.length()
+ @warning_ignore("unsafe_cast")
+ if (current as String).rfind(expected) != find:
+ return report_error(GdAssertMessages.error_ends_with(current, expected))
+ return report_success()
+
+
+# gdlint:disable=max-returns
+func has_length(expected :int, comparator := Comparator.EQUAL) -> GdUnitStringAssert:
+ var current :Variant = current_value()
+ if current == null:
+ return report_error(GdAssertMessages.error_has_length(current, expected, comparator))
+ var str_current: String = current
+ match comparator:
+ Comparator.EQUAL:
+ if str_current.length() != expected:
+ return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
+ Comparator.LESS_THAN:
+ if str_current.length() >= expected:
+ return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
+ Comparator.LESS_EQUAL:
+ if str_current.length() > expected:
+ return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
+ Comparator.GREATER_THAN:
+ if str_current.length() <= expected:
+ return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
+ Comparator.GREATER_EQUAL:
+ if str_current.length() < expected:
+ return report_error(GdAssertMessages.error_has_length(str_current, expected, comparator))
+ _:
+ return report_error("Comparator '%d' not implemented!" % comparator)
+ return report_success()
+
+
+func contains_bbcode(value: String) -> bool:
+ var rtl := RichTextLabel.new()
+ rtl.bbcode_enabled = true
+ rtl.parse_bbcode(value)
+ var has_bbcode := rtl.get_parsed_text() != value
+ rtl.free()
+ return has_bbcode
diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid
new file mode 100644
index 00000000..ba34078a
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://dxqvilchqqeta
diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd
new file mode 100644
index 00000000..fbc031a4
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd
@@ -0,0 +1,187 @@
+extends GdUnitVectorAssert
+
+var _base: GdUnitAssertImpl
+var _current_type: int
+var _type_check: bool
+
+func _init(current: Variant, type_check := true) -> void:
+ _type_check = type_check
+ _base = GdUnitAssertImpl.new(current)
+ # save the actual assert instance on the current thread context
+ GdUnitThreadManager.get_current_context().set_assert(self)
+ if not _validate_value_type(current):
+ @warning_ignore("return_value_discarded")
+ report_error("GdUnitVectorAssert error, the type <%s> is not supported." % GdObjects.typeof_as_string(current))
+ _current_type = typeof(current)
+
+
+func _notification(event :int) -> void:
+ if event == NOTIFICATION_PREDELETE:
+ if _base != null:
+ _base.notification(event)
+ _base = null
+
+
+func _validate_value_type(value :Variant) -> bool:
+ return (
+ value == null
+ or typeof(value) in [
+ TYPE_VECTOR2,
+ TYPE_VECTOR2I,
+ TYPE_VECTOR3,
+ TYPE_VECTOR3I,
+ TYPE_VECTOR4,
+ TYPE_VECTOR4I
+ ]
+ )
+
+
+func _validate_is_vector_type(value :Variant) -> bool:
+ var type := typeof(value)
+ if type == _current_type or _current_type == TYPE_NIL:
+ return true
+ @warning_ignore("return_value_discarded")
+ report_error(GdAssertMessages.error_is_wrong_type(_current_type, type))
+ return false
+
+
+func current_value() -> Variant:
+ return _base.current_value()
+
+
+func report_success() -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_success()
+ return self
+
+
+func report_error(error :String) -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.report_error(error)
+ return self
+
+
+func failure_message() -> String:
+ return _base.failure_message()
+
+
+func override_failure_message(message: String) -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.override_failure_message(message)
+ return self
+
+
+func append_failure_message(message :String) -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.append_failure_message(message)
+ return self
+
+
+func is_null() -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_null()
+ return self
+
+
+func is_not_null() -> GdUnitVectorAssert:
+ @warning_ignore("return_value_discarded")
+ _base.is_not_null()
+ return self
+
+
+func is_equal(expected: Variant) -> GdUnitVectorAssert:
+ if _type_check and not _validate_is_vector_type(expected):
+ return self
+ @warning_ignore("return_value_discarded")
+ _base.is_equal(expected)
+ return self
+
+
+func is_not_equal(expected: Variant) -> GdUnitVectorAssert:
+ if _type_check and not _validate_is_vector_type(expected):
+ return self
+ @warning_ignore("return_value_discarded")
+ _base.is_not_equal(expected)
+ return self
+
+
+@warning_ignore("shadowed_global_identifier")
+func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(expected) or not _validate_is_vector_type(approx):
+ return self
+ var current :Variant = current_value()
+ var from :Variant = expected - approx
+ var to :Variant = expected + approx
+ if current == null or (not _is_equal_approx(current, from, to)):
+ return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
+ return report_success()
+
+
+func _is_equal_approx(current :Variant, from :Variant, to :Variant) -> bool:
+ match typeof(current):
+ TYPE_VECTOR2, TYPE_VECTOR2I:
+ return ((current.x >= from.x and current.y >= from.y)
+ and (current.x <= to.x and current.y <= to.y))
+ TYPE_VECTOR3, TYPE_VECTOR3I:
+ return ((current.x >= from.x and current.y >= from.y and current.z >= from.z)
+ and (current.x <= to.x and current.y <= to.y and current.z <= to.z))
+ TYPE_VECTOR4, TYPE_VECTOR4I:
+ return ((current.x >= from.x and current.y >= from.y and current.z >= from.z and current.w >= from.w)
+ and (current.x <= to.x and current.y <= to.y and current.z <= to.z and current.w <= to.w))
+ _:
+ push_error("Missing implementation '_is_equal_approx' for vector type %s" % typeof(current))
+ return false
+
+
+func is_less(expected :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(expected):
+ return self
+ var current :Variant = current_value()
+ if current == null or current >= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_THAN, current, expected))
+ return report_success()
+
+
+func is_less_equal(expected :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(expected):
+ return self
+ var current :Variant = current_value()
+ if current == null or current > expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.LESS_EQUAL, current, expected))
+ return report_success()
+
+
+func is_greater(expected :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(expected):
+ return self
+ var current :Variant = current_value()
+ if current == null or current <= expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_THAN, current, expected))
+ return report_success()
+
+
+func is_greater_equal(expected :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(expected):
+ return self
+ var current :Variant = current_value()
+ if current == null or current < expected:
+ return report_error(GdAssertMessages.error_is_value(Comparator.GREATER_EQUAL, current, expected))
+ return report_success()
+
+
+func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(from) or not _validate_is_vector_type(to):
+ return self
+ var current :Variant = current_value()
+ if current == null or not (current >= from and current <= to):
+ return report_error(GdAssertMessages.error_is_value(Comparator.BETWEEN_EQUAL, current, from, to))
+ return report_success()
+
+
+func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert:
+ if not _validate_is_vector_type(from) or not _validate_is_vector_type(to):
+ return self
+ var current :Variant = current_value()
+ if (current != null and current >= from and current <= to):
+ return report_error(GdAssertMessages.error_is_value(Comparator.NOT_BETWEEN_EQUAL, current, from, to))
+ return report_success()
diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid
new file mode 100644
index 00000000..c0a7e13a
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid
@@ -0,0 +1 @@
+uid://r4avfcakvscw
diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd b/addons/gdUnit4/src/asserts/ValueProvider.gd
new file mode 100644
index 00000000..be01f70b
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/ValueProvider.gd
@@ -0,0 +1,10 @@
+# base interface for assert value provider
+class_name ValueProvider
+extends RefCounted
+
+func get_value() -> Variant:
+ return null
+
+
+func dispose() -> void:
+ pass
diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd.uid b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid
new file mode 100644
index 00000000..a34788ef
--- /dev/null
+++ b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid
@@ -0,0 +1 @@
+uid://8y15b6ts3kss
diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd
new file mode 100644
index 00000000..aa023194
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd
@@ -0,0 +1,62 @@
+class_name CmdArgumentParser
+extends RefCounted
+
+var _options :CmdOptions
+var _tool_name :String
+var _parsed_commands :Dictionary = Dictionary()
+
+
+func _init(p_options :CmdOptions, p_tool_name :String) -> void:
+ _options = p_options
+ _tool_name = p_tool_name
+
+
+func parse(args :Array, ignore_unknown_cmd := false) -> GdUnitResult:
+ _parsed_commands.clear()
+
+ # parse until first program argument
+ while not args.is_empty():
+ var arg :String = args.pop_front()
+ if arg.find(_tool_name) != -1:
+ break
+
+ if args.is_empty():
+ return GdUnitResult.empty()
+
+ # now parse all arguments
+ while not args.is_empty():
+ var cmd :String = args.pop_front()
+ var option := _options.get_option(cmd)
+
+ if option:
+ if _parse_cmd_arguments(option, args) == -1:
+ return GdUnitResult.error("The '%s' command requires an argument!" % option.short_command())
+ elif not ignore_unknown_cmd:
+ return GdUnitResult.error("Unknown '%s' command!" % cmd)
+ return GdUnitResult.success(_parsed_commands.values())
+
+
+func options() -> CmdOptions:
+ return _options
+
+
+func _parse_cmd_arguments(option: CmdOption, args: Array) -> int:
+ var command_name := option.short_command()
+ var command: CmdCommand = _parsed_commands.get(command_name, CmdCommand.new(command_name))
+
+ if option.has_argument():
+ if not option.is_argument_optional() and args.is_empty():
+ return -1
+ if _is_next_value_argument(args):
+ var value: String = args.pop_front()
+ command.add_argument(value)
+ elif not option.is_argument_optional():
+ return -1
+ _parsed_commands[command_name] = command
+ return 0
+
+
+func _is_next_value_argument(args: PackedStringArray) -> bool:
+ if args.is_empty():
+ return false
+ return _options.get_option(args[0]) == null
diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid
new file mode 100644
index 00000000..f0bc4b67
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid
@@ -0,0 +1 @@
+uid://d4hd3vc50jltg
diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd b/addons/gdUnit4/src/cmd/CmdCommand.gd
new file mode 100644
index 00000000..92e8c1fe
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdCommand.gd
@@ -0,0 +1,27 @@
+class_name CmdCommand
+extends RefCounted
+
+var _name: String
+var _arguments: PackedStringArray
+
+
+func _init(p_name :String, p_arguments := []) -> void:
+ _name = p_name
+ _arguments = PackedStringArray(p_arguments)
+
+
+func name() -> String:
+ return _name
+
+
+func arguments() -> PackedStringArray:
+ return _arguments
+
+
+func add_argument(arg :String) -> void:
+ @warning_ignore("return_value_discarded")
+ _arguments.append(arg)
+
+
+func _to_string() -> String:
+ return "%s:%s" % [_name, ", ".join(_arguments)]
diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd.uid b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid
new file mode 100644
index 00000000..f087f8c7
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid
@@ -0,0 +1 @@
+uid://w4mr1j0k0l
diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd
new file mode 100644
index 00000000..2a7ed553
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd
@@ -0,0 +1,136 @@
+class_name CmdCommandHandler
+extends RefCounted
+
+const CB_SINGLE_ARG = 0
+const CB_MULTI_ARGS = 1
+const NO_CB := Callable()
+
+var _cmd_options :CmdOptions
+# holds the command callbacks by key::String and value: [, ]:Array
+# Dictionary[String, Array[Callback]
+var _command_cbs :Dictionary
+
+
+
+func _init(cmd_options: CmdOptions) -> void:
+ _cmd_options = cmd_options
+
+
+# register a callback function for given command
+# cmd_name short name of the command
+# fr_arg a funcref to a function with a single argument
+func register_cb(cmd_name: String, cb: Callable) -> CmdCommandHandler:
+ var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB])
+ if registered_cb[CB_SINGLE_ARG]:
+ push_error("A function for command '%s' is already registered!" % cmd_name)
+ return self
+
+ if not _validate_cb_signature(cb, TYPE_STRING):
+ push_error(
+ ("The callback '%s:%s' for command '%s' has invalid function signature. "
+ +"The callback signature must be 'func name(value: PackedStringArray)'")
+ % [cb.get_object().get_class(), cb.get_method(), cmd_name])
+ return null
+
+ registered_cb[CB_SINGLE_ARG] = cb
+ _command_cbs[cmd_name] = registered_cb
+ return self
+
+
+# register a callback function for given command
+# cb a funcref to a function with a variable number of arguments but expects all parameters to be passed via a single Array.
+func register_cbv(cmd_name: String, cb: Callable) -> CmdCommandHandler:
+ var registered_cb: Array = _command_cbs.get(cmd_name, [NO_CB, NO_CB])
+ if registered_cb[CB_MULTI_ARGS]:
+ push_error("A function for command '%s' is already registered!" % cmd_name)
+ return self
+
+ if not _validate_cb_signature(cb, TYPE_PACKED_STRING_ARRAY):
+ push_error(
+ ("The callback '%s:%s' for command '%s' has invalid function signature. "
+ +"The callback signature must be 'func name(value: PackedStringArray)'")
+ % [cb.get_object().get_class(), cb.get_method(), cmd_name])
+ return null
+
+ registered_cb[CB_MULTI_ARGS] = cb
+ _command_cbs[cmd_name] = registered_cb
+ return self
+
+
+func _validate() -> GdUnitResult:
+ var errors := PackedStringArray()
+ # Dictionary[StringName, String]
+ var registered_cbs := Dictionary()
+
+ for cmd_name in _command_cbs.keys() as Array[String]:
+ var cb: Callable = (_command_cbs[cmd_name][CB_SINGLE_ARG]
+ if _command_cbs[cmd_name][CB_SINGLE_ARG]
+ else _command_cbs[cmd_name][CB_MULTI_ARGS])
+ if cb != NO_CB and not cb.is_valid():
+ @warning_ignore("return_value_discarded")
+ errors.append("Invalid function reference for command '%s', Check the function reference!" % cmd_name)
+ if _cmd_options.get_option(cmd_name) == null:
+ @warning_ignore("return_value_discarded")
+ errors.append("The command '%s' is unknown, verify your CmdOptions!" % cmd_name)
+ # verify for multiple registered command callbacks
+ if cb != NO_CB:
+ var cb_method := cb.get_method()
+ if registered_cbs.has(cb_method):
+ var already_registered_cmd :String = registered_cbs[cb_method]
+ @warning_ignore("return_value_discarded")
+ errors.append("The function reference '%s' already registerd for command '%s'!" % [cb_method, already_registered_cmd])
+ else:
+ registered_cbs[cb_method] = cmd_name
+ if errors.is_empty():
+ return GdUnitResult.success(true)
+ return GdUnitResult.error("\n".join(errors))
+
+
+func execute(commands: Array[CmdCommand]) -> GdUnitResult:
+ var result := _validate()
+ if result.is_error():
+ return result
+ for cmd in commands:
+ var cmd_name := cmd.name()
+ if _command_cbs.has(cmd_name):
+ var cb_s: Callable = _command_cbs.get(cmd_name)[CB_SINGLE_ARG]
+ var arguments := cmd.arguments()
+ var cmd_option := _cmd_options.get_option(cmd_name)
+
+ if arguments.is_empty():
+ cb_s.call()
+ elif arguments.size() > 1:
+ var cb_m: Callable = _command_cbs.get(cmd_name)[CB_MULTI_ARGS]
+ cb_m.call(arguments)
+ else:
+ if cmd_option.type() == TYPE_BOOL:
+ cb_s.call(true if arguments[0] == "true" else false)
+ else:
+ cb_s.call(arguments[0])
+
+ return GdUnitResult.success(true)
+
+
+func _validate_cb_signature(cb: Callable, arg_type: int) -> bool:
+ for m in cb.get_object().get_method_list():
+ if m["name"] == cb.get_method():
+ @warning_ignore("unsafe_cast")
+ return _validate_func_arguments(m["args"] as Array, arg_type)
+ return true
+
+
+func _validate_func_arguments(arguments: Array, arg_type: int) -> bool:
+ # validate we have a single argument
+ if arguments.size() > 1:
+ return false
+ # a cb with no arguments is also valid
+ if arguments.size() == 0:
+ return true
+ # validate argument type
+ var arg: Dictionary = arguments[0]
+ @warning_ignore("unsafe_cast")
+ if arg["usage"] as int == PROPERTY_USAGE_NIL_IS_VARIANT:
+ return true
+ if arg["type"] != arg_type:
+ return false
+ return true
diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid
new file mode 100644
index 00000000..a1a2ddc6
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid
@@ -0,0 +1 @@
+uid://ccm3ivfiaf3i7
diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd b/addons/gdUnit4/src/cmd/CmdOption.gd
new file mode 100644
index 00000000..a4982de2
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdOption.gd
@@ -0,0 +1,61 @@
+class_name CmdOption
+extends RefCounted
+
+
+var _commands :PackedStringArray
+var _help :String
+var _description :String
+var _type :int
+var _arg_optional :bool = false
+
+
+# constructs a command option by given arguments
+# commands : a string with comma separated list of available commands begining with the short form
+# help: a help text show howto use
+# description: a full description of the command
+# type: the argument type
+# arg_optional: defines of the argument optional
+func _init(p_commands :String, p_help :String, p_description :String, p_type :int = TYPE_NIL, p_arg_optional :bool = false) -> void:
+ _commands = p_commands.replace(" ", "").replace("\t", "").split(",")
+ _help = p_help
+ _description = p_description
+ _type = p_type
+ _arg_optional = p_arg_optional
+
+
+func commands() -> PackedStringArray:
+ return _commands
+
+
+func short_command() -> String:
+ return _commands[0]
+
+
+func help() -> String:
+ return _help
+
+
+func description() -> String:
+ return _description
+
+
+func type() -> int:
+ return _type
+
+
+func is_argument_optional() -> bool:
+ return _arg_optional
+
+
+func has_argument() -> bool:
+ return _type != TYPE_NIL
+
+
+func describe() -> String:
+ if help().is_empty():
+ return " %-32s %s \n" % [commands(), description()]
+ return " %-32s %s \n %-32s %s\n" % [commands(), description(), "", help()]
+
+
+func _to_string() -> String:
+ return describe()
diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd.uid b/addons/gdUnit4/src/cmd/CmdOption.gd.uid
new file mode 100644
index 00000000..0b077447
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdOption.gd.uid
@@ -0,0 +1 @@
+uid://ccnb2ah35atho
diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd b/addons/gdUnit4/src/cmd/CmdOptions.gd
new file mode 100644
index 00000000..c6105298
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdOptions.gd
@@ -0,0 +1,31 @@
+class_name CmdOptions
+extends RefCounted
+
+
+var _default_options :Array[CmdOption]
+var _advanced_options :Array[CmdOption]
+
+
+func _init(p_options :Array[CmdOption] = [], p_advanced_options :Array[CmdOption] = []) -> void:
+ # default help options
+ _default_options = p_options
+ _advanced_options = p_advanced_options
+
+
+func default_options() -> Array[CmdOption]:
+ return _default_options
+
+
+func advanced_options() -> Array[CmdOption]:
+ return _advanced_options
+
+
+func options() -> Array[CmdOption]:
+ return default_options() + advanced_options()
+
+
+func get_option(cmd :String) -> CmdOption:
+ for option in options():
+ if Array(option.commands()).has(cmd):
+ return option
+ return null
diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd.uid b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid
new file mode 100644
index 00000000..6d1112bf
--- /dev/null
+++ b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid
@@ -0,0 +1 @@
+uid://0p8udx4tdwol
diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd
new file mode 100644
index 00000000..74f0e175
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdArrayTools.gd
@@ -0,0 +1,127 @@
+## Small helper tool to work with Godot Arrays
+class_name GdArrayTools
+extends RefCounted
+
+
+const max_elements := 32
+const ARRAY_TYPES := [
+ TYPE_ARRAY,
+ TYPE_PACKED_BYTE_ARRAY,
+ TYPE_PACKED_INT32_ARRAY,
+ TYPE_PACKED_INT64_ARRAY,
+ TYPE_PACKED_FLOAT32_ARRAY,
+ TYPE_PACKED_FLOAT64_ARRAY,
+ TYPE_PACKED_STRING_ARRAY,
+ TYPE_PACKED_VECTOR2_ARRAY,
+ TYPE_PACKED_VECTOR3_ARRAY,
+ TYPE_PACKED_VECTOR4_ARRAY,
+ TYPE_PACKED_COLOR_ARRAY
+]
+
+
+static func is_array_type(value: Variant) -> bool:
+ return is_type_array(typeof(value))
+
+
+static func is_type_array(type :int) -> bool:
+ return type in ARRAY_TYPES
+
+
+## Filters an array by given value[br]
+## If the given value not an array it returns null, will remove all occurence of given value.
+static func filter_value(array: Variant, value: Variant) -> Variant:
+ if not is_array_type(array):
+ return null
+
+ @warning_ignore("unsafe_method_access")
+ var filtered_array: Variant = array.duplicate()
+ @warning_ignore("unsafe_method_access")
+ var index: int = filtered_array.find(value)
+ while index != -1:
+ @warning_ignore("unsafe_method_access")
+ filtered_array.remove_at(index)
+ @warning_ignore("unsafe_method_access")
+ index = filtered_array.find(value)
+ return filtered_array
+
+
+## Groups an array by a custom key selector
+## The function should take an item and return the group key
+static func group_by(array: Array, key_selector: Callable) -> Dictionary:
+ var result := {}
+
+ for item: Variant in array:
+ var group_key: Variant = key_selector.call(item)
+ var values: Array = result.get_or_add(group_key, [])
+ values.append(item)
+
+ return result
+
+
+## Erases a value from given array by using equals(l,r) to find the element to erase
+static func erase_value(array :Array, value :Variant) -> void:
+ for element :Variant in array:
+ if GdObjects.equals(element, value):
+ array.erase(element)
+
+
+## Scans for the array build in type on a untyped array[br]
+## Returns the buildin type by scan all values and returns the type if all values has the same type.
+## If the values has different types TYPE_VARIANT is returend
+static func scan_typed(array :Array) -> int:
+ if array.is_empty():
+ return TYPE_NIL
+ var actual_type := GdObjects.TYPE_VARIANT
+ for value :Variant in array:
+ var current_type := typeof(value)
+ if not actual_type in [GdObjects.TYPE_VARIANT, current_type]:
+ return GdObjects.TYPE_VARIANT
+ actual_type = current_type
+ return actual_type
+
+
+## Converts given array into a string presentation.[br]
+## This function is different to the original Godot str() implementation.
+## The string presentaion contains fullquallified typed informations.
+##[br]
+## Examples:
+## [codeblock]
+## # will result in PackedString(["a", "b"])
+## GdArrayTools.as_string(PackedStringArray("a", "b"))
+## # will result in PackedString(["a", "b"])
+## GdArrayTools.as_string(PackedColorArray(Color.RED, COLOR.GREEN))
+## [/codeblock]
+static func as_string(elements: Variant, encode_value := true) -> String:
+ var delemiter := ", "
+ if elements == null:
+ return ""
+ @warning_ignore("unsafe_cast")
+ if (elements as Array).is_empty():
+ return ""
+ var prefix := _typeof_as_string(elements) if encode_value else ""
+ var formatted := ""
+ var index := 0
+ for element :Variant in elements:
+ if max_elements != -1 and index > max_elements:
+ return prefix + "[" + formatted + delemiter + "...]"
+ if formatted.length() > 0 :
+ formatted += delemiter
+ formatted += GdDefaultValueDecoder.decode(element) if encode_value else str(element)
+ index += 1
+ return prefix + "[" + formatted + "]"
+
+
+static func has_same_content(current: Array, other: Array) -> bool:
+ if current.size() != other.size(): return false
+ for element: Variant in current:
+ if not other.has(element): return false
+ if current.count(element) != other.count(element): return false
+ return true
+
+
+static func _typeof_as_string(value :Variant) -> String:
+ var type := typeof(value)
+ # for untyped array we retun empty string
+ if type == TYPE_ARRAY:
+ return ""
+ return GdObjects.typeof_as_string(value)
diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd.uid b/addons/gdUnit4/src/core/GdArrayTools.gd.uid
new file mode 100644
index 00000000..98d9d57d
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdArrayTools.gd.uid
@@ -0,0 +1 @@
+uid://bk60ywsj4ekp7
diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd
new file mode 100644
index 00000000..5131df71
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdDiffTool.gd
@@ -0,0 +1,224 @@
+# Myers' Diff Algorithm implementation
+# Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers
+class_name GdDiffTool
+extends RefCounted
+
+
+const DIV_ADD :int = 214
+const DIV_SUB :int = 215
+
+
+class Edit:
+ enum Type { EQUAL, INSERT, DELETE }
+ var type: Type
+ var character: int
+
+ func _init(t: Type, chr: int) -> void:
+ type = t
+ character = chr
+
+
+# Main entry point - returns [ldiff, rdiff]
+static func string_diff(left: Variant, right: Variant) -> Array[PackedInt32Array]:
+ var lb := PackedInt32Array() if left == null else str(left).to_utf32_buffer().to_int32_array()
+ var rb := PackedInt32Array() if right == null else str(right).to_utf32_buffer().to_int32_array()
+
+ # Early exit for identical strings
+ if lb == rb:
+ return [lb.duplicate(), rb.duplicate()]
+
+ var edits := _myers_diff(lb, rb)
+ return _edits_to_diff_format(edits)
+
+
+# Core Myers' algorithm
+static func _myers_diff(a: PackedInt32Array, b: PackedInt32Array) -> Array[Edit]:
+ var n := a.size()
+ var m := b.size()
+ var max_d := n + m
+
+ # V array stores the furthest reaching x coordinate for each k-line
+ # We need indices from -max_d to max_d, so we offset by max_d
+ var v := PackedInt32Array()
+ v.resize(2 * max_d + 1)
+ v.fill(-1)
+ v[max_d + 1] = 0 # k=1 starts at x=0
+
+ var trace := [] # Store V arrays for each d to backtrack later
+
+ # Find the edit distance
+ for d in range(0, max_d + 1):
+ # Store current V for backtracking
+ trace.append(v.duplicate())
+
+ for k in range(-d, d + 1, 2):
+ var k_offset := k + max_d
+
+ # Decide whether to move down or right
+ var x: int
+ if k == -d or (k != d and v[k_offset - 1] < v[k_offset + 1]):
+ x = v[k_offset + 1] # Move down (insert from b)
+ else:
+ x = v[k_offset - 1] + 1 # Move right (delete from a)
+
+ var y := x - k
+
+ # Follow diagonal as far as possible (matching characters)
+ while x < n and y < m and a[x] == b[y]:
+ x += 1
+ y += 1
+
+ v[k_offset] = x
+
+ # Check if we've reached the end
+ if x >= n and y >= m:
+ return _backtrack(a, b, trace, d, max_d)
+
+ # Should never reach here for valid inputs
+ return []
+
+
+# Backtrack through the edit graph to build the edit script
+static func _backtrack(a: PackedInt32Array, b: PackedInt32Array, trace: Array, d: int, max_d: int) -> Array[Edit]:
+ var edits: Array[Edit] = []
+ var x := a.size()
+ var y := b.size()
+
+ # Walk backwards through each d value
+ for depth in range(d, -1, -1):
+ var v: PackedInt32Array = trace[depth]
+ var k := x - y
+ var k_offset := k + max_d
+
+ # Determine previous k
+ var prev_k: int
+ if k == -depth or (k != depth and v[k_offset - 1] < v[k_offset + 1]):
+ prev_k = k + 1
+ else:
+ prev_k = k - 1
+
+ var prev_k_offset := prev_k + max_d
+ var prev_x := v[prev_k_offset]
+ var prev_y := prev_x - prev_k
+
+ # Extract diagonal (equal) characters
+ while x > prev_x and y > prev_y:
+ x -= 1
+ y -= 1
+ #var char_array := PackedInt32Array([a[x]])
+ edits.insert(0, Edit.new(Edit.Type.EQUAL, a[x]))
+
+ # Record the edit operation
+ if depth > 0:
+ if x == prev_x:
+ # Insert from b
+ y -= 1
+ #var char_array := PackedInt32Array([b[y]])
+ edits.insert(0, Edit.new(Edit.Type.INSERT, b[y]))
+ else:
+ # Delete from a
+ x -= 1
+ #var char_array := PackedInt32Array([a[x]])
+ edits.insert(0, Edit.new(Edit.Type.DELETE, a[x]))
+
+ return edits
+
+
+# Convert edit script to the DIV_ADD/DIV_SUB format
+static func _edits_to_diff_format(edits: Array[Edit]) -> Array[PackedInt32Array]:
+ var ldiff := PackedInt32Array()
+ var rdiff := PackedInt32Array()
+
+ for edit in edits:
+ match edit.type:
+ Edit.Type.EQUAL:
+ ldiff.append(edit.character)
+ rdiff.append(edit.character)
+ Edit.Type.INSERT:
+ ldiff.append(DIV_ADD)
+ ldiff.append(edit.character)
+ rdiff.append(DIV_SUB)
+ rdiff.append(edit.character)
+ Edit.Type.DELETE:
+ ldiff.append(DIV_SUB)
+ ldiff.append(edit.character)
+ rdiff.append(DIV_ADD)
+ rdiff.append(edit.character)
+
+ return [ldiff, rdiff]
+
+
+# prototype
+static func longestCommonSubsequence(text1 :String, text2 :String) -> PackedStringArray:
+ var text1Words := text1.split(" ")
+ var text2Words := text2.split(" ")
+ var text1WordCount := text1Words.size()
+ var text2WordCount := text2Words.size()
+ var solutionMatrix := Array()
+ for i in text1WordCount+1:
+ var ar := Array()
+ for n in text2WordCount+1:
+ ar.append(0)
+ solutionMatrix.append(ar)
+
+ for i in range(text1WordCount-1, 0, -1):
+ for j in range(text2WordCount-1, 0, -1):
+ if text1Words[i] == text2Words[j]:
+ solutionMatrix[i][j] = solutionMatrix[i + 1][j + 1] + 1;
+ else:
+ solutionMatrix[i][j] = max(solutionMatrix[i + 1][j], solutionMatrix[i][j + 1]);
+
+ var i := 0
+ var j := 0
+ var lcsResultList := PackedStringArray();
+ while (i < text1WordCount && j < text2WordCount):
+ if text1Words[i] == text2Words[j]:
+ @warning_ignore("return_value_discarded")
+ lcsResultList.append(text2Words[j])
+ i += 1
+ j += 1
+ else: if (solutionMatrix[i + 1][j] >= solutionMatrix[i][j + 1]):
+ i += 1
+ else:
+ j += 1
+ return lcsResultList
+
+
+static func markTextDifferences(text1 :String, text2 :String, lcsList :PackedStringArray, insertColor :Color, deleteColor:Color) -> String:
+ var stringBuffer := ""
+ if text1 == null and lcsList == null:
+ return stringBuffer
+
+ var text1Words := text1.split(" ")
+ var text2Words := text2.split(" ")
+ var i := 0
+ var j := 0
+ var word1LastIndex := 0
+ var word2LastIndex := 0
+ for k in lcsList.size():
+ while i < text1Words.size() and j < text2Words.size():
+ if text1Words[i] == lcsList[k] and text2Words[j] == lcsList[k]:
+ stringBuffer += "" + lcsList[k] + " "
+ word1LastIndex = i + 1
+ word2LastIndex = j + 1
+ i = text1Words.size()
+ j = text2Words.size()
+
+ else: if text1Words[i] != lcsList[k]:
+ while i < text1Words.size() and text1Words[i] != lcsList[k]:
+ stringBuffer += "" + text1Words[i] + " "
+ i += 1
+ else: if text2Words[j] != lcsList[k]:
+ while j < text2Words.size() and text2Words[j] != lcsList[k]:
+ stringBuffer += "" + text2Words[j] + " "
+ j += 1
+ i = word1LastIndex
+ j = word2LastIndex
+
+ while word1LastIndex < text1Words.size():
+ stringBuffer += "" + text1Words[word1LastIndex] + " "
+ word1LastIndex += 1
+ while word2LastIndex < text2Words.size():
+ stringBuffer += "" + text2Words[word2LastIndex] + " "
+ word2LastIndex += 1
+ return stringBuffer
diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd.uid b/addons/gdUnit4/src/core/GdDiffTool.gd.uid
new file mode 100644
index 00000000..89041b66
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdDiffTool.gd.uid
@@ -0,0 +1 @@
+uid://b5sli0lem5xca
diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd
new file mode 100644
index 00000000..279e5188
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdObjects.gd
@@ -0,0 +1,726 @@
+# This is a helper class to compare two objects by equals
+class_name GdObjects
+extends Resource
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+
+# introduced with Godot 4.3.beta1
+const TYPE_VOID = 1000
+const TYPE_VARARG = 1001
+const TYPE_VARIANT = 1002
+const TYPE_FUNC = 1003
+const TYPE_FUZZER = 1004
+# missing Godot types
+const TYPE_NODE = 2001
+const TYPE_CONTROL = 2002
+const TYPE_CANVAS = 2003
+const TYPE_ENUM = 2004
+
+
+const TYPE_AS_STRING_MAPPINGS := {
+ TYPE_NIL: "null",
+ TYPE_BOOL: "bool",
+ TYPE_INT: "int",
+ TYPE_FLOAT: "float",
+ TYPE_STRING: "String",
+ TYPE_VECTOR2: "Vector2",
+ TYPE_VECTOR2I: "Vector2i",
+ TYPE_RECT2: "Rect2",
+ TYPE_RECT2I: "Rect2i",
+ TYPE_VECTOR3: "Vector3",
+ TYPE_VECTOR3I: "Vector3i",
+ TYPE_TRANSFORM2D: "Transform2D",
+ TYPE_VECTOR4: "Vector4",
+ TYPE_VECTOR4I: "Vector4i",
+ TYPE_PLANE: "Plane",
+ TYPE_QUATERNION: "Quaternion",
+ TYPE_AABB: "AABB",
+ TYPE_BASIS: "Basis",
+ TYPE_TRANSFORM3D: "Transform3D",
+ TYPE_PROJECTION: "Projection",
+ TYPE_COLOR: "Color",
+ TYPE_STRING_NAME: "StringName",
+ TYPE_NODE_PATH: "NodePath",
+ TYPE_RID: "RID",
+ TYPE_OBJECT: "Object",
+ TYPE_CALLABLE: "Callable",
+ TYPE_SIGNAL: "Signal",
+ TYPE_DICTIONARY: "Dictionary",
+ TYPE_ARRAY: "Array",
+ TYPE_PACKED_BYTE_ARRAY: "PackedByteArray",
+ TYPE_PACKED_INT32_ARRAY: "PackedInt32Array",
+ TYPE_PACKED_INT64_ARRAY: "PackedInt64Array",
+ TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array",
+ TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array",
+ TYPE_PACKED_STRING_ARRAY: "PackedStringArray",
+ TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array",
+ TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array",
+ TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array",
+ TYPE_PACKED_COLOR_ARRAY: "PackedColorArray",
+ TYPE_VOID: "void",
+ TYPE_VARARG: "VarArg",
+ TYPE_FUNC: "Func",
+ TYPE_FUZZER: "Fuzzer",
+ TYPE_VARIANT: "Variant"
+}
+
+
+class EditorNotifications:
+ # NOTE: Hardcoding to avoid runtime errors in exported projects when editor
+ # classes are not available. These values are unlikely to change.
+ # See: EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED
+ const NOTIFICATION_EDITOR_SETTINGS_CHANGED := 10000
+
+
+const NOTIFICATION_AS_STRING_MAPPINGS := {
+ TYPE_OBJECT: {
+ Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE",
+ Object.NOTIFICATION_PREDELETE: "PREDELETE",
+ EditorNotifications.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED",
+ },
+ TYPE_NODE: {
+ Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE",
+ Node.NOTIFICATION_EXIT_TREE: "EXIT_TREE",
+ Node.NOTIFICATION_CHILD_ORDER_CHANGED: "CHILD_ORDER_CHANGED",
+ Node.NOTIFICATION_READY: "READY",
+ Node.NOTIFICATION_PAUSED: "PAUSED",
+ Node.NOTIFICATION_UNPAUSED: "UNPAUSED",
+ Node.NOTIFICATION_PHYSICS_PROCESS: "PHYSICS_PROCESS",
+ Node.NOTIFICATION_PROCESS: "PROCESS",
+ Node.NOTIFICATION_PARENTED: "PARENTED",
+ Node.NOTIFICATION_UNPARENTED: "UNPARENTED",
+ Node.NOTIFICATION_SCENE_INSTANTIATED: "INSTANCED",
+ Node.NOTIFICATION_DRAG_BEGIN: "DRAG_BEGIN",
+ Node.NOTIFICATION_DRAG_END: "DRAG_END",
+ Node.NOTIFICATION_PATH_RENAMED: "PATH_CHANGED",
+ Node.NOTIFICATION_INTERNAL_PROCESS: "INTERNAL_PROCESS",
+ Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS: "INTERNAL_PHYSICS_PROCESS",
+ Node.NOTIFICATION_POST_ENTER_TREE: "POST_ENTER_TREE",
+ Node.NOTIFICATION_WM_MOUSE_ENTER: "WM_MOUSE_ENTER",
+ Node.NOTIFICATION_WM_MOUSE_EXIT: "WM_MOUSE_EXIT",
+ Node.NOTIFICATION_APPLICATION_FOCUS_IN: "WM_FOCUS_IN",
+ Node.NOTIFICATION_APPLICATION_FOCUS_OUT: "WM_FOCUS_OUT",
+ #Node.NOTIFICATION_WM_QUIT_REQUEST: "WM_QUIT_REQUEST",
+ Node.NOTIFICATION_WM_GO_BACK_REQUEST: "WM_GO_BACK_REQUEST",
+ Node.NOTIFICATION_WM_WINDOW_FOCUS_OUT: "WM_UNFOCUS_REQUEST",
+ Node.NOTIFICATION_OS_MEMORY_WARNING: "OS_MEMORY_WARNING",
+ Node.NOTIFICATION_TRANSLATION_CHANGED: "TRANSLATION_CHANGED",
+ Node.NOTIFICATION_WM_ABOUT: "WM_ABOUT",
+ Node.NOTIFICATION_CRASH: "CRASH",
+ Node.NOTIFICATION_OS_IME_UPDATE: "OS_IME_UPDATE",
+ Node.NOTIFICATION_APPLICATION_RESUMED: "APP_RESUMED",
+ Node.NOTIFICATION_APPLICATION_PAUSED: "APP_PAUSED",
+ Node3D.NOTIFICATION_TRANSFORM_CHANGED: "TRANSFORM_CHANGED",
+ Node3D.NOTIFICATION_ENTER_WORLD: "ENTER_WORLD",
+ Node3D.NOTIFICATION_EXIT_WORLD: "EXIT_WORLD",
+ Node3D.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
+ Skeleton3D.NOTIFICATION_UPDATE_SKELETON: "UPDATE_SKELETON",
+ CanvasItem.NOTIFICATION_DRAW: "DRAW",
+ CanvasItem.NOTIFICATION_VISIBILITY_CHANGED: "VISIBILITY_CHANGED",
+ CanvasItem.NOTIFICATION_ENTER_CANVAS: "ENTER_CANVAS",
+ CanvasItem.NOTIFICATION_EXIT_CANVAS: "EXIT_CANVAS",
+ #Popup.NOTIFICATION_POST_POPUP: "POST_POPUP",
+ #Popup.NOTIFICATION_POPUP_HIDE: "POPUP_HIDE",
+ },
+ TYPE_CONTROL : {
+ Object.NOTIFICATION_PREDELETE: "PREDELETE",
+ Container.NOTIFICATION_SORT_CHILDREN: "SORT_CHILDREN",
+ Control.NOTIFICATION_RESIZED: "RESIZED",
+ Control.NOTIFICATION_MOUSE_ENTER: "MOUSE_ENTER",
+ Control.NOTIFICATION_MOUSE_EXIT: "MOUSE_EXIT",
+ Control.NOTIFICATION_FOCUS_ENTER: "FOCUS_ENTER",
+ Control.NOTIFICATION_FOCUS_EXIT: "FOCUS_EXIT",
+ Control.NOTIFICATION_THEME_CHANGED: "THEME_CHANGED",
+ #Control.NOTIFICATION_MODAL_CLOSE: "MODAL_CLOSE",
+ Control.NOTIFICATION_SCROLL_BEGIN: "SCROLL_BEGIN",
+ Control.NOTIFICATION_SCROLL_END: "SCROLL_END",
+ }
+}
+
+
+enum COMPARE_MODE {
+ OBJECT_REFERENCE,
+ PARAMETER_DEEP_TEST
+}
+
+
+# prototype of better object to dictionary
+static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary:
+ if obj == null:
+ return {}
+ var clazz_name := obj.get_class()
+ var dict := Dictionary()
+ var clazz_path := ""
+
+ if is_instance_valid(obj) and obj.get_script() != null:
+ var script: Script = obj.get_script()
+ # handle build-in scripts
+ if script.resource_path != null and script.resource_path.contains(".tscn"):
+ var path_elements := script.resource_path.split(".tscn")
+ clazz_name = path_elements[0].get_file()
+ clazz_path = script.resource_path
+ else:
+ var d := inst_to_dict(obj)
+ clazz_path = d["@path"]
+ if d["@subpath"] != NodePath(""):
+ clazz_name = d["@subpath"]
+ dict["@inner_class"] = true
+ else:
+ clazz_name = clazz_path.get_file().replace(".gd", "")
+ dict["@path"] = clazz_path
+
+ for property in obj.get_property_list():
+ var property_name :String = property["name"]
+ var property_type :int = property["type"]
+ var property_value :Variant = obj.get(property_name)
+ if property_value is GDScript or property_value is Callable or property_value is RegEx:
+ continue
+ if (property["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE|PROPERTY_USAGE_DEFAULT
+ and not property["usage"] & PROPERTY_USAGE_CATEGORY
+ and not property["usage"] == 0):
+ if property_type == TYPE_OBJECT:
+ # prevent recursion
+ if hashed_objects.has(obj):
+ dict[property_name] = str(property_value)
+ continue
+ hashed_objects[obj] = true
+ @warning_ignore("unsafe_cast")
+ dict[property_name] = obj2dict(property_value as Object, hashed_objects)
+ else:
+ dict[property_name] = property_value
+ if obj is Node:
+ var childrens :Array = (obj as Node).get_children()
+ dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
+ if obj is TreeItem:
+ var childrens :Array = (obj as TreeItem).get_children()
+ dict["childrens"] = childrens.map(func (child :Object) -> Dictionary: return obj2dict(child, hashed_objects))
+
+ return {"%s" % clazz_name : dict}
+
+
+static func equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool = false, compare_mode :COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
+ return _equals(obj_a, obj_b, case_sensitive, compare_mode, [], 0)
+
+
+static func equals_sorted(obj_a: Array[Variant], obj_b: Array[Variant], case_sensitive: bool = false, compare_mode: COMPARE_MODE = COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
+ var a: Array[Variant] = obj_a.duplicate()
+ var b: Array[Variant] = obj_b.duplicate()
+ a.sort()
+ b.sort()
+ return equals(a, b, case_sensitive, compare_mode)
+
+
+static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool:
+ var type_a := typeof(obj_a)
+ var type_b := typeof(obj_b)
+ if stack_depth > 32:
+ prints("stack_depth", stack_depth, deep_stack)
+ push_error("GdUnit equals has max stack deep reached!")
+ return false
+
+ # use argument matcher if requested
+ if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_cast")
+ return (obj_a as GdUnitArgumentMatcher).is_match(obj_b)
+ if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher:
+ @warning_ignore("unsafe_cast")
+ return (obj_b as GdUnitArgumentMatcher).is_match(obj_a)
+
+ stack_depth += 1
+ # fast fail is different types
+ if not _is_type_equivalent(type_a, type_b):
+ return false
+ # is same instance
+ if obj_a == obj_b:
+ return true
+ # handle null values
+ if obj_a == null and obj_b != null:
+ return false
+ if obj_b == null and obj_a != null:
+ return false
+
+ match type_a:
+ TYPE_OBJECT:
+ if deep_stack.has(obj_a) or deep_stack.has(obj_b):
+ return true
+ deep_stack.append(obj_a)
+ deep_stack.append(obj_b)
+ if compare_mode == COMPARE_MODE.PARAMETER_DEEP_TEST:
+ # fail fast
+ if not is_instance_valid(obj_a) or not is_instance_valid(obj_b):
+ return false
+ @warning_ignore("unsafe_method_access")
+ if obj_a.get_class() != obj_b.get_class():
+ return false
+ @warning_ignore("unsafe_cast")
+ var a := obj2dict(obj_a as Object)
+ @warning_ignore("unsafe_cast")
+ var b := obj2dict(obj_b as Object)
+ return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth)
+ return obj_a == obj_b
+
+ TYPE_ARRAY:
+ @warning_ignore("unsafe_method_access")
+ if obj_a.size() != obj_b.size():
+ return false
+ @warning_ignore("unsafe_method_access")
+ for index :int in obj_a.size():
+ if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth):
+ return false
+ return true
+
+ TYPE_DICTIONARY:
+ @warning_ignore("unsafe_method_access")
+ if obj_a.size() != obj_b.size():
+ return false
+ @warning_ignore("unsafe_method_access")
+ for key :Variant in obj_a.keys():
+ @warning_ignore("unsafe_method_access")
+ var value_a :Variant = obj_a[key] if obj_a.has(key) else null
+ @warning_ignore("unsafe_method_access")
+ var value_b :Variant = obj_b[key] if obj_b.has(key) else null
+ if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth):
+ return false
+ return true
+
+ TYPE_STRING:
+ if case_sensitive:
+ @warning_ignore("unsafe_method_access")
+ return obj_a.to_lower() == obj_b.to_lower()
+ else:
+ return obj_a == obj_b
+ return obj_a == obj_b
+
+
+@warning_ignore("shadowed_variable_base_class")
+static func notification_as_string(instance :Variant, notification :int) -> String:
+ var error := "Unknown notification: '%s' at instance: %s" % [notification, instance]
+ if instance is Node and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].has(notification):
+ return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_NODE].get(notification, error)
+ if instance is Control and NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].has(notification):
+ return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_CONTROL].get(notification, error)
+ return NOTIFICATION_AS_STRING_MAPPINGS[TYPE_OBJECT].get(notification, error)
+
+
+static func string_to_type(value :String) -> int:
+ for type :int in TYPE_AS_STRING_MAPPINGS.keys():
+ if TYPE_AS_STRING_MAPPINGS.get(type) == value:
+ return type
+ return TYPE_NIL
+
+
+static func to_camel_case(value :String) -> String:
+ var p := to_pascal_case(value)
+ if not p.is_empty():
+ p[0] = p[0].to_lower()
+ return p
+
+
+static func to_pascal_case(value :String) -> String:
+ return value.capitalize().replace(" ", "")
+
+
+@warning_ignore("return_value_discarded")
+static func to_snake_case(value :String) -> String:
+ var result := PackedStringArray()
+ for ch in value:
+ var lower_ch := ch.to_lower()
+ if ch != lower_ch and result.size() > 1:
+ result.append('_')
+ result.append(lower_ch)
+ return ''.join(result)
+
+
+static func is_snake_case(value :String) -> bool:
+ for ch in value:
+ if ch == '_':
+ continue
+ if ch == ch.to_upper():
+ return false
+ return true
+
+
+static func type_as_string(type :int) -> String:
+ if type < TYPE_MAX:
+ return type_string(type)
+ return TYPE_AS_STRING_MAPPINGS.get(type, "Variant")
+
+
+static func typeof_as_string(value :Variant) -> String:
+ return TYPE_AS_STRING_MAPPINGS.get(typeof(value), "Unknown type")
+
+
+static func all_types() -> PackedInt32Array:
+ return PackedInt32Array(TYPE_AS_STRING_MAPPINGS.keys())
+
+
+static func string_as_typeof(type_name :String) -> int:
+ var type :Variant = TYPE_AS_STRING_MAPPINGS.find_key(type_name)
+ return type if type != null else TYPE_VARIANT
+
+
+static func is_primitive_type(value :Variant) -> bool:
+ return typeof(value) in [TYPE_BOOL, TYPE_STRING, TYPE_STRING_NAME, TYPE_INT, TYPE_FLOAT]
+
+
+static func _is_type_equivalent(type_a :int, type_b :int) -> bool:
+ # don't test for TYPE_STRING_NAME equivalenz
+ if type_a == TYPE_STRING_NAME or type_b == TYPE_STRING_NAME:
+ return true
+ if GdUnitSettings.is_strict_number_type_compare():
+ return type_a == type_b
+ return (
+ (type_a == TYPE_FLOAT and type_b == TYPE_INT)
+ or (type_a == TYPE_INT and type_b == TYPE_FLOAT)
+ or type_a == type_b)
+
+
+static func is_engine_type(value :Variant) -> bool:
+ if value is GDScript or value is ScriptExtension:
+ return false
+ var obj: Object = value
+ if is_instance_valid(obj) and obj.has_method("is_class"):
+ return obj.is_class("GDScriptNativeClass")
+ return false
+
+
+static func is_type(value :Variant) -> bool:
+ # is an build-in type
+ if typeof(value) != TYPE_OBJECT:
+ return false
+ # is a engine class type
+ if is_engine_type(value):
+ return true
+ # is a custom class type
+ @warning_ignore("unsafe_cast")
+ if value is GDScript and (value as GDScript).can_instantiate():
+ return true
+ return false
+
+
+static func _is_same(left :Variant, right :Variant) -> bool:
+ var left_type := -1 if left == null else typeof(left)
+ var right_type := -1 if right == null else typeof(right)
+
+ # if typ different can't be the same
+ if left_type != right_type:
+ return false
+ if left_type == TYPE_OBJECT and right_type == TYPE_OBJECT:
+ @warning_ignore("unsafe_cast")
+ return (left as Object).get_instance_id() == (right as Object).get_instance_id()
+ return equals(left, right)
+
+
+static func is_object(value :Variant) -> bool:
+ return typeof(value) == TYPE_OBJECT
+
+
+static func is_script(value :Variant) -> bool:
+ return is_object(value) and value is Script
+
+
+static func is_native_class(value :Variant) -> bool:
+ return is_object(value) and is_engine_type(value)
+
+
+static func is_scene(value :Variant) -> bool:
+ return is_object(value) and value is PackedScene
+
+
+static func is_scene_resource_path(value :Variant) -> bool:
+ @warning_ignore("unsafe_cast")
+ return value is String and (value as String).ends_with(".tscn")
+
+
+static func is_singleton(value: Variant) -> bool:
+ if not is_instance_valid(value) or is_native_class(value):
+ return false
+ for name in Engine.get_singleton_list():
+ @warning_ignore("unsafe_cast")
+ if (value as Object).is_class(name):
+ return true
+ return false
+
+
+static func is_instance(value :Variant) -> bool:
+ if not is_instance_valid(value) or is_native_class(value):
+ return false
+ @warning_ignore("unsafe_cast")
+ if is_script(value) and (value as Script).get_instance_base_type() == "":
+ return true
+ if is_scene(value):
+ return true
+ @warning_ignore("unsafe_cast")
+ return not (value as Object).has_method('new') and not (value as Object).has_method('instance')
+
+
+# only object form type Node and attached filename
+static func is_instance_scene(instance :Variant) -> bool:
+ if instance is Node:
+ var node: Node = instance
+ return node.get_scene_file_path() != null and not node.get_scene_file_path().is_empty()
+ return false
+
+
+static func can_be_instantiate(obj :Variant) -> bool:
+ if not obj or is_engine_type(obj):
+ return false
+ @warning_ignore("unsafe_cast")
+ return (obj as Object).has_method("new")
+
+
+static func create_instance(clazz :Variant) -> GdUnitResult:
+ match typeof(clazz):
+ TYPE_OBJECT:
+ # test is given clazz already an instance
+ if is_instance(clazz):
+ return GdUnitResult.success(clazz)
+ @warning_ignore("unsafe_method_access")
+ return GdUnitResult.success(clazz.new())
+ TYPE_STRING:
+ var clazz_name: String = clazz
+ if ClassDB.class_exists(clazz_name):
+ if Engine.has_singleton(clazz_name):
+ return GdUnitResult.error("Not allowed to create a instance for singelton '%s'." % clazz_name)
+ if not ClassDB.can_instantiate(clazz_name):
+ return GdUnitResult.error("Can't instance Engine class '%s'." % clazz_name)
+ return GdUnitResult.success(ClassDB.instantiate(clazz_name))
+ else:
+ var clazz_path :String = extract_class_path(clazz_name)[0]
+ if not FileAccess.file_exists(clazz_path):
+ return GdUnitResult.error("Class '%s' not found." % clazz_name)
+ var script: GDScript = load(clazz_path)
+ if script != null:
+ return GdUnitResult.success(script.new())
+ else:
+ return GdUnitResult.error("Can't create instance for '%s'." % clazz_name)
+ return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz))
+
+
+## We do dispose 'GDScriptFunctionState' in a kacky style because the class is not visible anymore
+static func dispose_function_state(func_state: Variant) -> void:
+ if func_state != null and str(func_state).contains("GDScriptFunctionState"):
+ @warning_ignore("unsafe_method_access")
+ func_state.completed.emit()
+
+
+@warning_ignore("return_value_discarded")
+static func extract_class_path(clazz :Variant) -> PackedStringArray:
+ var clazz_path := PackedStringArray()
+ if clazz is String:
+ @warning_ignore("unsafe_cast")
+ clazz_path.append(clazz as String)
+ return clazz_path
+ if is_instance(clazz):
+ # is instance a script instance?
+ var script: GDScript = clazz.script
+ if script != null:
+ return extract_class_path(script)
+ return clazz_path
+
+ if clazz is GDScript:
+ var script: GDScript = clazz
+ if not script.resource_path.is_empty():
+ clazz_path.append(script.resource_path)
+ return clazz_path
+ # if not found we go the expensive way and extract the path form the script by creating an instance
+ var arg_list := build_function_default_arguments(script, "_init")
+ var instance: Object = script.callv("new", arg_list)
+ var clazz_info := inst_to_dict(instance)
+ GdUnitTools.free_instance(instance)
+ @warning_ignore("unsafe_cast")
+ clazz_path.append(clazz_info["@path"] as String)
+ if clazz_info.has("@subpath"):
+ var sub_path :String = clazz_info["@subpath"]
+ if not sub_path.is_empty():
+ var sub_paths := sub_path.split("/")
+ clazz_path += sub_paths
+ return clazz_path
+ return clazz_path
+
+
+static func extract_class_name_from_class_path(clazz_path :PackedStringArray) -> String:
+ var base_clazz := clazz_path[0]
+ # return original class name if engine class
+ if ClassDB.class_exists(base_clazz):
+ return base_clazz
+ var clazz_name := to_pascal_case(base_clazz.get_basename().get_file())
+ for path_index in range(1, clazz_path.size()):
+ clazz_name += "." + clazz_path[path_index]
+ return clazz_name
+
+
+static func extract_class_name(clazz :Variant) -> GdUnitResult:
+ if clazz == null:
+ return GdUnitResult.error("Can't extract class name form a null value.")
+
+ if is_instance(clazz):
+ # is instance a script instance?
+ var script: GDScript = clazz.script
+ if script != null:
+ return extract_class_name(script)
+ @warning_ignore("unsafe_cast")
+ return GdUnitResult.success((clazz as Object).get_class())
+
+ # extract name form full qualified class path
+ if clazz is String:
+ var clazz_name: String = clazz
+ if ClassDB.class_exists(clazz_name):
+ return GdUnitResult.success(clazz_name)
+ var source_script :GDScript = load(clazz_name)
+ clazz_name = GdScriptParser.new().get_class_name(source_script)
+ return GdUnitResult.success(to_pascal_case(clazz_name))
+
+ if is_primitive_type(clazz):
+ return GdUnitResult.error("Can't extract class name for an primitive '%s'" % type_as_string(typeof(clazz)))
+
+ if is_script(clazz):
+ @warning_ignore("unsafe_cast")
+ if (clazz as Script).resource_path.is_empty():
+ var class_path := extract_class_name_from_class_path(extract_class_path(clazz))
+ return GdUnitResult.success(class_path);
+ return extract_class_name(clazz.resource_path)
+
+ # need to create an instance for a class typ the extract the class name
+ @warning_ignore("unsafe_method_access")
+ var instance :Variant = clazz.new()
+ if instance == null:
+ return GdUnitResult.error("Can't create a instance for class '%s'" % str(clazz))
+ var result := extract_class_name(instance)
+ @warning_ignore("return_value_discarded")
+ GdUnitTools.free_instance(instance)
+ return result
+
+
+static func extract_inner_clazz_names(clazz_name :String, script_path :PackedStringArray) -> PackedStringArray:
+ var inner_classes := PackedStringArray()
+
+ if ClassDB.class_exists(clazz_name):
+ return inner_classes
+ var script :GDScript = load(script_path[0])
+ var map := script.get_script_constant_map()
+ for key :String in map.keys():
+ var value :Variant = map.get(key)
+ if value is GDScript:
+ var class_path := extract_class_path(value)
+ @warning_ignore("return_value_discarded")
+ inner_classes.append(class_path[1])
+ return inner_classes
+
+
+static func extract_class_functions(clazz_name :String, script_path :PackedStringArray) -> Array:
+ if ClassDB.class_get_method_list(clazz_name):
+ return ClassDB.class_get_method_list(clazz_name)
+
+ if not FileAccess.file_exists(script_path[0]):
+ return Array()
+ var script :GDScript = load(script_path[0])
+ if script is GDScript:
+ # if inner class on class path we have to load the script from the script_constant_map
+ if script_path.size() == 2 and script_path[1] != "":
+ var inner_classes := script_path[1]
+ var map := script.get_script_constant_map()
+ script = map[inner_classes]
+ var clazz_functions :Array = script.get_method_list()
+ var base_clazz :String = script.get_instance_base_type()
+ if base_clazz:
+ return extract_class_functions(base_clazz, script_path)
+ return clazz_functions
+ return Array()
+
+
+# scans all registert script classes for given
+# if the class is public in the global space than return true otherwise false
+# public class means the script class is defined by 'class_name '
+static func is_public_script_class(clazz_name :String) -> bool:
+ var script_classes:Array[Dictionary] = ProjectSettings.get_global_class_list()
+ for class_info in script_classes:
+ if class_info.has("class"):
+ if class_info["class"] == clazz_name:
+ return true
+ return false
+
+
+static func build_function_default_arguments(script :GDScript, func_name :String) -> Array:
+ var arg_list := Array()
+ for func_sig in script.get_script_method_list():
+ if func_sig["name"] == func_name:
+ var args :Array[Dictionary] = func_sig["args"]
+ for arg in args:
+ var value_type :int = arg["type"]
+ var default_value :Variant = default_value_by_type(value_type)
+ arg_list.append(default_value)
+ return arg_list
+ return arg_list
+
+
+static func default_value_by_type(type :int) -> Variant:
+ assert(type < TYPE_MAX)
+ assert(type >= 0)
+
+ match type:
+ TYPE_NIL: return null
+ TYPE_BOOL: return false
+ TYPE_INT: return 0
+ TYPE_FLOAT: return 0.0
+ TYPE_STRING: return ""
+ TYPE_VECTOR2: return Vector2.ZERO
+ TYPE_VECTOR2I: return Vector2i.ZERO
+ TYPE_VECTOR3: return Vector3.ZERO
+ TYPE_VECTOR3I: return Vector3i.ZERO
+ TYPE_VECTOR4: return Vector4.ZERO
+ TYPE_VECTOR4I: return Vector4i.ZERO
+ TYPE_RECT2: return Rect2()
+ TYPE_RECT2I: return Rect2i()
+ TYPE_TRANSFORM2D: return Transform2D()
+ TYPE_PLANE: return Plane()
+ TYPE_QUATERNION: return Quaternion()
+ TYPE_AABB: return AABB()
+ TYPE_BASIS: return Basis()
+ TYPE_TRANSFORM3D: return Transform3D()
+ TYPE_COLOR: return Color()
+ TYPE_NODE_PATH: return NodePath()
+ TYPE_RID: return RID()
+ TYPE_OBJECT: return null
+ TYPE_CALLABLE: return Callable()
+ TYPE_ARRAY: return []
+ TYPE_DICTIONARY: return {}
+ TYPE_PACKED_BYTE_ARRAY: return PackedByteArray()
+ TYPE_PACKED_COLOR_ARRAY: return PackedColorArray()
+ TYPE_PACKED_INT32_ARRAY: return PackedInt32Array()
+ TYPE_PACKED_INT64_ARRAY: return PackedInt64Array()
+ TYPE_PACKED_FLOAT32_ARRAY: return PackedFloat32Array()
+ TYPE_PACKED_FLOAT64_ARRAY: return PackedFloat64Array()
+ TYPE_PACKED_STRING_ARRAY: return PackedStringArray()
+ TYPE_PACKED_VECTOR2_ARRAY: return PackedVector2Array()
+ TYPE_PACKED_VECTOR3_ARRAY: return PackedVector3Array()
+
+ push_error("Can't determine a default value for type: '%s', Please create a Bug issue and attach the stacktrace please." % type)
+ return null
+
+
+static func find_nodes_by_class(root: Node, cls: String, recursive: bool = false) -> Array[Node]:
+ if not recursive:
+ return _find_nodes_by_class_no_rec(root, cls)
+ return _find_nodes_by_class(root, cls)
+
+
+static func _find_nodes_by_class_no_rec(parent: Node, cls: String) -> Array[Node]:
+ var result :Array[Node] = []
+ for ch in parent.get_children():
+ if ch.get_class() == cls:
+ result.append(ch)
+ return result
+
+
+static func _find_nodes_by_class(root: Node, cls: String) -> Array[Node]:
+ var result :Array[Node] = []
+ var stack :Array[Node] = [root]
+ while stack:
+ var node :Node = stack.pop_back()
+ if node.get_class() == cls:
+ result.append(node)
+ for ch in node.get_children():
+ stack.push_back(ch)
+ return result
diff --git a/addons/gdUnit4/src/core/GdObjects.gd.uid b/addons/gdUnit4/src/core/GdObjects.gd.uid
new file mode 100644
index 00000000..fd8f6d6e
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdObjects.gd.uid
@@ -0,0 +1 @@
+uid://b7ldhc4ryfh1v
diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd
new file mode 100644
index 00000000..3e6a334e
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnit4Version.gd
@@ -0,0 +1,65 @@
+class_name GdUnit4Version
+extends RefCounted
+
+const VERSION_PATTERN = "[center][color=#9887c4]gd[/color][color=#7a57d6]Unit[/color][color=#9887c4]4[/color] [color=#9887c4]${version}[/color][/center]"
+
+var _major :int
+var _minor :int
+var _patch :int
+
+
+func _init(major :int, minor :int, patch :int) -> void:
+ _major = major
+ _minor = minor
+ _patch = patch
+
+
+static func parse(value :String) -> GdUnit4Version:
+ var regex := RegEx.new()
+ @warning_ignore("return_value_discarded")
+ regex.compile("[a-zA-Z:,-]+")
+ var cleaned := regex.sub(value, "", true)
+ var parts := cleaned.split(".")
+ var major := parts[0].to_int()
+ var minor := parts[1].to_int()
+ var patch := parts[2].to_int() if parts.size() > 2 else 0
+ return GdUnit4Version.new(major, minor, patch)
+
+
+static func current() -> GdUnit4Version:
+ var config := ConfigFile.new()
+ @warning_ignore("return_value_discarded")
+ config.load('addons/gdUnit4/plugin.cfg')
+ @warning_ignore("unsafe_cast")
+ return parse(config.get_value('plugin', 'version') as String)
+
+
+func equals(other :GdUnit4Version) -> bool:
+ return _major == other._major and _minor == other._minor and _patch == other._patch
+
+
+func is_greater(other :GdUnit4Version) -> bool:
+ if _major > other._major:
+ return true
+ if _major == other._major and _minor > other._minor:
+ return true
+ return _major == other._major and _minor == other._minor and _patch > other._patch
+
+
+static func init_version_label(label :Control) -> void:
+ var config := ConfigFile.new()
+ @warning_ignore("return_value_discarded")
+ config.load('addons/gdUnit4/plugin.cfg')
+ var version :String = config.get_value('plugin', 'version')
+ if label is RichTextLabel:
+ (label as RichTextLabel).text = VERSION_PATTERN.replace('${version}', version)
+ else:
+ (label as Label).text = "gdUnit4 " + version
+
+
+func _to_string() -> String:
+ return "v%d.%d.%d" % [_major, _minor, _patch]
+
+
+func documentation_version() -> String:
+ return "v%d.%d.x" % [_major, _minor]
diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd.uid b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid
new file mode 100644
index 00000000..2b7462fe
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid
@@ -0,0 +1 @@
+uid://bbaqjhpbxce3u
diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd
new file mode 100644
index 00000000..f6db5b4a
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd
@@ -0,0 +1,232 @@
+class_name GdUnitFileAccess
+extends RefCounted
+
+const GDUNIT_TEMP := "user://tmp"
+
+
+static func current_dir() -> String:
+ return ProjectSettings.globalize_path("res://")
+
+
+static func clear_tmp() -> void:
+ delete_directory(GDUNIT_TEMP)
+
+
+# Creates a new file under
+static func create_temp_file(relative_path :String, file_name :String, mode := FileAccess.WRITE) -> FileAccess:
+ var file_path := create_temp_dir(relative_path) + "/" + file_name
+ var file := FileAccess.open(file_path, mode)
+ if file == null:
+ push_error("Error creating temporary file at: %s, %s" % [file_path, error_string(FileAccess.get_open_error())])
+ return file
+
+
+static func temp_dir() -> String:
+ if not DirAccess.dir_exists_absolute(GDUNIT_TEMP):
+ @warning_ignore("return_value_discarded")
+ 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):
+ @warning_ignore("return_value_discarded")
+ DirAccess.make_dir_recursive_absolute(new_folder)
+ return new_folder
+
+
+static func copy_file(from_file :String, to_dir :String) -> GdUnitResult:
+ var dir := DirAccess.open(to_dir)
+ if dir != null:
+ var to_file := to_dir + "/" + from_file.get_file()
+ prints("Copy %s to %s" % [from_file, to_file])
+ var error := dir.copy(from_file, to_file)
+ if error != OK:
+ return GdUnitResult.error("Can't copy file form '%s' to '%s'. Error: '%s'" % [from_file, to_file, error_string(error)])
+ return GdUnitResult.success(to_file)
+ return GdUnitResult.error("Directory not found: " + to_dir)
+
+
+static func copy_directory(from_dir :String, to_dir :String, recursive :bool = false) -> bool:
+ if not DirAccess.dir_exists_absolute(from_dir):
+ push_error("Source directory not found '%s'" % from_dir)
+ return false
+
+ # check if destination exists
+ if not DirAccess.dir_exists_absolute(to_dir):
+ # create it
+ var err := DirAccess.make_dir_recursive_absolute(to_dir)
+ if err != OK:
+ push_error("Can't create directory '%s'. Error: %s" % [to_dir, error_string(err)])
+ return false
+ var source_dir := DirAccess.open(from_dir)
+ var dest_dir := DirAccess.open(to_dir)
+ if source_dir != null:
+ @warning_ignore("return_value_discarded")
+ source_dir.list_dir_begin()
+ var next := "."
+
+ while next != "":
+ next = source_dir.get_next()
+ if next == "" or next == "." or next == "..":
+ continue
+ var source := source_dir.get_current_dir() + "/" + next
+ var dest := dest_dir.get_current_dir() + "/" + next
+ if source_dir.current_is_dir():
+ if recursive:
+ @warning_ignore("return_value_discarded")
+ copy_directory(source + "/", dest, recursive)
+ continue
+ var err := source_dir.copy(source, dest)
+ if err != OK:
+ push_error("Error checked copy file '%s' to '%s'" % [source, dest])
+ return false
+
+ return true
+ else:
+ push_error("Directory not found: " + from_dir)
+ return false
+
+
+static func delete_directory(path :String, only_content := false) -> void:
+ var dir := DirAccess.open(path)
+ if dir != null:
+ dir.include_hidden = true
+ @warning_ignore("return_value_discarded")
+ dir.list_dir_begin()
+ var file_name := "."
+ while file_name != "":
+ file_name = dir.get_next()
+ if file_name.is_empty() or file_name == "." or file_name == "..":
+ continue
+ var next := path + "/" +file_name
+ if dir.current_is_dir():
+ delete_directory(next)
+ else:
+ # delete file
+ var err := dir.remove(next)
+ if err:
+ push_error("Delete %s failed: %s" % [next, error_string(err)])
+ if not only_content:
+ var err := dir.remove(path)
+ if err:
+ push_error("Delete %s failed: %s" % [path, error_string(err)])
+
+
+static func delete_path_index_lower_equals_than(path :String, prefix :String, index :int) -> int:
+ var dir := DirAccess.open(path)
+ if dir == null:
+ return 0
+ var deleted := 0
+ @warning_ignore("return_value_discarded")
+ dir.list_dir_begin()
+ var next := "."
+ while next != "":
+ next = dir.get_next()
+ if next.is_empty() or next == "." or next == "..":
+ continue
+ if next.begins_with(prefix):
+ var current_index := next.split("_")[1].to_int()
+ if current_index <= index:
+ deleted += 1
+ delete_directory(path + "/" + next)
+ return deleted
+
+
+# scans given path for sub directories by given prefix and returns the highest index numer
+# e.g.
+static func find_last_path_index(path :String, prefix :String) -> int:
+ var dir := DirAccess.open(path)
+ if dir == null:
+ return 0
+ var last_iteration := 0
+ @warning_ignore("return_value_discarded")
+ dir.list_dir_begin()
+ var next := "."
+ while next != "":
+ next = dir.get_next()
+ if next.is_empty() or next == "." or next == "..":
+ continue
+ if next.begins_with(prefix):
+ var iteration := next.split("_")[1].to_int()
+ if iteration > last_iteration:
+ last_iteration = iteration
+ return last_iteration
+
+
+static func as_resource_path(value: String) -> String:
+ if value.begins_with("res://"):
+ return value
+ return "res://" + value.trim_prefix("//").trim_prefix("/").trim_suffix("/")
+
+
+static func scan_dir(path :String) -> PackedStringArray:
+ var dir := DirAccess.open(path)
+ if dir == null or not dir.dir_exists(path):
+ return PackedStringArray()
+ var content := PackedStringArray()
+ dir.include_hidden = true
+ @warning_ignore("return_value_discarded")
+ dir.list_dir_begin()
+ var next := "."
+ while next != "":
+ next = dir.get_next()
+ if next.is_empty() or next == "." or next == "..":
+ continue
+ @warning_ignore("return_value_discarded")
+ content.append(next)
+ return content
+
+
+static func resource_as_array(resource_path :String) -> PackedStringArray:
+ var file := FileAccess.open(resource_path, FileAccess.READ)
+ if file == null:
+ push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
+ return PackedStringArray()
+ var file_content := PackedStringArray()
+ while not file.eof_reached():
+ @warning_ignore("return_value_discarded")
+ file_content.append(file.get_line())
+ return file_content
+
+
+static func resource_as_string(resource_path :String) -> String:
+ var file := FileAccess.open(resource_path, FileAccess.READ)
+ if file == null:
+ push_error("ERROR: Can't read resource '%s'. %s" % [resource_path, error_string(FileAccess.get_open_error())])
+ return ""
+ return file.get_as_text(true)
+
+
+static func make_qualified_path(path :String) -> String:
+ if path.begins_with("res://"):
+ return path
+ if path.begins_with("//"):
+ return path.replace("//", "res://")
+ if path.begins_with("/"):
+ return "res:/" + path
+ return path
+
+
+static func extract_zip(zip_package :String, dest_path :String) -> GdUnitResult:
+ var zip: ZIPReader = ZIPReader.new()
+ var err := zip.open(zip_package)
+ if err != OK:
+ return GdUnitResult.error("Extracting `%s` failed! Please collect the error log and report this. Error Code: %s" % [zip_package, err])
+ var zip_entries: PackedStringArray = zip.get_files()
+ # Get base path and step over archive folder
+ var archive_path := zip_entries[0]
+ zip_entries.remove_at(0)
+
+ for zip_entry in zip_entries:
+ var new_file_path: String = dest_path + "/" + zip_entry.replace(archive_path, "")
+ if zip_entry.ends_with("/"):
+ @warning_ignore("return_value_discarded")
+ DirAccess.make_dir_recursive_absolute(new_file_path)
+ continue
+ var file: FileAccess = FileAccess.open(new_file_path, FileAccess.WRITE)
+ file.store_buffer(zip.read_file(zip_entry))
+ @warning_ignore("return_value_discarded")
+ zip.close()
+ return GdUnitResult.success(dest_path)
diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid
new file mode 100644
index 00000000..14695c19
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid
@@ -0,0 +1 @@
+uid://dflqb5germp5n
diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd b/addons/gdUnit4/src/core/GdUnitProperty.gd
new file mode 100644
index 00000000..3d050b19
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitProperty.gd
@@ -0,0 +1,81 @@
+class_name GdUnitProperty
+extends RefCounted
+
+
+var _name :String
+var _help :String
+var _type :int
+var _value :Variant
+var _value_set :PackedStringArray
+var _default :Variant
+
+
+func _init(p_name :String, p_type :int, p_value :Variant, p_default_value :Variant, p_help :="", p_value_set := PackedStringArray()) -> void:
+ _name = p_name
+ _type = p_type
+ _value = p_value
+ _value_set = p_value_set
+ _default = p_default_value
+ _help = p_help
+
+
+func name() -> String:
+ return _name
+
+
+func type() -> int:
+ return _type
+
+
+func value() -> Variant:
+ return _value
+
+
+func int_value() -> int:
+ return _value
+
+func value_as_string() -> String:
+ return _value
+
+
+func value_set() -> PackedStringArray:
+ return _value_set
+
+
+func is_selectable_value() -> bool:
+ return not _value_set.is_empty()
+
+
+func set_value(p_value: Variant) -> void:
+ match _type:
+ TYPE_STRING:
+ _value = str(p_value)
+ TYPE_BOOL:
+ _value = type_convert(p_value, TYPE_BOOL)
+ TYPE_INT:
+ _value = type_convert(p_value, TYPE_INT)
+ TYPE_FLOAT:
+ _value = type_convert(p_value, TYPE_FLOAT)
+ TYPE_DICTIONARY:
+ _value = type_convert(p_value, TYPE_DICTIONARY)
+ _:
+ _value = p_value
+
+
+func default() -> Variant:
+ return _default
+
+
+func category() -> String:
+ var elements := _name.split("/")
+ if elements.size() > 3:
+ return elements[2]
+ return ""
+
+
+func help() -> String:
+ return _help
+
+
+func _to_string() -> String:
+ return "%-64s %-10s %-10s (%s) help:%s set:%s" % [name(), type(), value(), default(), help(), _value_set]
diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd.uid b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid
new file mode 100644
index 00000000..de104010
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid
@@ -0,0 +1 @@
+uid://cqndh0nuu8ltx
diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd b/addons/gdUnit4/src/core/GdUnitResult.gd
new file mode 100644
index 00000000..42392a5e
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitResult.gd
@@ -0,0 +1,109 @@
+class_name GdUnitResult
+extends RefCounted
+
+enum {
+ SUCCESS,
+ WARN,
+ ERROR,
+ EMPTY
+}
+
+var _state: int
+var _warn_message := ""
+var _error_message := ""
+var _value :Variant = null
+
+
+static func empty() -> GdUnitResult:
+ var result := GdUnitResult.new()
+ result._state = EMPTY
+ return result
+
+
+static func success(p_value: Variant = "") -> GdUnitResult:
+ assert(p_value != null, "The value must not be NULL")
+ var result := GdUnitResult.new()
+ result._value = p_value
+ result._state = SUCCESS
+ return result
+
+
+static func warn(p_warn_message: String, p_value: Variant = null) -> GdUnitResult:
+ assert(not p_warn_message.is_empty()) #,"The message must not be empty")
+ var result := GdUnitResult.new()
+ result._value = p_value
+ result._warn_message = p_warn_message
+ result._state = WARN
+ return result
+
+
+static func error(p_error_message: String) -> GdUnitResult:
+ assert(not p_error_message.is_empty(), "The message must not be empty")
+ var result := GdUnitResult.new()
+ result._value = null
+ result._error_message = p_error_message
+ result._state = ERROR
+ return result
+
+
+func is_success() -> bool:
+ return _state == SUCCESS
+
+
+func is_warn() -> bool:
+ return _state == WARN
+
+
+func is_error() -> bool:
+ return _state == ERROR
+
+
+func is_empty() -> bool:
+ return _state == EMPTY
+
+
+func value() -> Variant:
+ return _value
+
+
+func value_as_string() -> String:
+ return _value
+
+
+func or_else(p_value: Variant) -> Variant:
+ if not is_success():
+ return p_value
+ return value()
+
+
+func error_message() -> String:
+ return _error_message
+
+
+func warn_message() -> String:
+ return _warn_message
+
+
+func _to_string() -> String:
+ return str(GdUnitResult.serialize(self))
+
+
+static func serialize(result: GdUnitResult) -> Dictionary:
+ if result == null:
+ push_error("Can't serialize a Null object from type GdUnitResult")
+ return {
+ "state" : result._state,
+ "value" : var_to_str(result._value),
+ "warn_msg" : result._warn_message,
+ "err_msg" : result._error_message
+ }
+
+
+static func deserialize(config: Dictionary) -> GdUnitResult:
+ var result := GdUnitResult.new()
+ var cfg_value: String = config.get("value", "")
+ result._value = str_to_var(cfg_value)
+ result._warn_message = config.get("warn_msg", null)
+ result._error_message = config.get("err_msg", null)
+ result._state = config.get("state")
+ return result
diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd.uid b/addons/gdUnit4/src/core/GdUnitResult.gd.uid
new file mode 100644
index 00000000..2835c400
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitResult.gd.uid
@@ -0,0 +1 @@
+uid://cnvq3nb61ei76
diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd
new file mode 100644
index 00000000..9f36354d
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd
@@ -0,0 +1,126 @@
+class_name GdUnitRunnerConfig
+extends Resource
+
+const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+const CONFIG_VERSION = "5.0"
+const VERSION = "version"
+const TESTS = "tests"
+const SERVER_PORT = "server_port"
+const EXIT_FAIL_FAST = "exit_on_first_fail"
+
+const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg"
+
+var _config := {
+ VERSION : CONFIG_VERSION,
+ # a set of directories or testsuite paths as key and a optional set of testcases as values
+
+ TESTS : Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase),
+
+ # the port of running test server for this session
+ SERVER_PORT : -1
+ }
+
+
+func version() -> String:
+ return _config[VERSION]
+
+
+func clear() -> GdUnitRunnerConfig:
+ _config[TESTS] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)
+ return self
+
+
+func set_server_port(port: int) -> GdUnitRunnerConfig:
+ _config[SERVER_PORT] = port
+ return self
+
+
+func server_port() -> int:
+ return _config.get(SERVER_PORT, -1)
+
+
+func add_test_cases(tests: Array[GdUnitTestCase]) -> GdUnitRunnerConfig:
+ test_cases().append_array(tests)
+ return self
+
+
+func test_cases() -> Array[GdUnitTestCase]:
+ return _config.get(TESTS, [])
+
+
+func save_config(path: String = CONFIG_FILE) -> GdUnitResult:
+ var file := FileAccess.open(path, FileAccess.WRITE)
+ if file == null:
+ var error := FileAccess.get_open_error()
+ return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)])
+
+ var to_save := {
+ VERSION : CONFIG_VERSION,
+ SERVER_PORT : _config.get(SERVER_PORT),
+ TESTS : Array()
+ }
+
+ var tests: Array = to_save.get(TESTS)
+ for test in test_cases():
+ tests.append(inst_to_dict(test))
+ file.store_string(JSON.stringify(to_save, "\t"))
+ return GdUnitResult.success(path)
+
+
+func load_config(path: String = CONFIG_FILE) -> GdUnitResult:
+ if not FileAccess.file_exists(path):
+ return GdUnitResult.warn("Can't find test runner configuration '%s'! Please select a test to run." % path)
+ var file := FileAccess.open(path, FileAccess.READ)
+ if file == null:
+ var error := FileAccess.get_open_error()
+ return GdUnitResult.error("Can't load test runner configuration '%s'! ERROR: %s." % [path, error_string(error)])
+ var content := file.get_as_text()
+ if not content.is_empty() and content[0] == '{':
+ # Parse as json
+ var test_json_conv := JSON.new()
+ var error := test_json_conv.parse(content)
+ if error != OK:
+ return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
+ var config: Dictionary = test_json_conv.get_data()
+ if not config.has(VERSION):
+ return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path)
+
+ var default: Array[Dictionary] = Array([], TYPE_DICTIONARY, "", null)
+ var tests_as_json: Array = config.get(TESTS, default)
+ _config = config
+ _config[TESTS] = convert_test_json_to_test_cases(tests_as_json)
+
+
+ fix_value_types()
+ return GdUnitResult.success(path)
+
+
+func convert_test_json_to_test_cases(jsons: Array) -> Array[GdUnitTestCase]:
+ if jsons.is_empty():
+ return []
+ var tests := jsons.map(func(d: Dictionary) -> GdUnitTestCase:
+ var test: GdUnitTestCase = dict_to_inst(d)
+ # we need o covert manually to the corect type becaus JSON do not handle typed values
+ test.guid = GdUnitGUID.new(str(d["guid"]))
+ test.attribute_index = test.attribute_index as int
+ test.line_number = test.line_number as int
+ return test
+ )
+ return Array(tests, TYPE_OBJECT, "RefCounted", GdUnitTestCase)
+
+
+func fix_value_types() -> void:
+ # fix float value to int json stores all numbers as float
+ var server_port_: int = _config.get(SERVER_PORT, -1)
+ _config[SERVER_PORT] = server_port_
+
+
+func convert_Array_to_PackedStringArray(data: Dictionary) -> void:
+ for key in data.keys() as Array[String]:
+ var values :Array = data[key]
+ data[key] = PackedStringArray(values)
+
+
+func _to_string() -> String:
+ return str(_config)
diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid
new file mode 100644
index 00000000..60443d84
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid
@@ -0,0 +1 @@
+uid://ltvpkh3ayklf
diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd
new file mode 100644
index 00000000..f2718537
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd
@@ -0,0 +1,622 @@
+# This class provides a runner for scense to simulate interactions like keyboard or mouse
+class_name GdUnitSceneRunnerImpl
+extends GdUnitSceneRunner
+
+
+var GdUnitFuncAssertImpl: GDScript = ResourceLoader.load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd", "GDScript", ResourceLoader.CACHE_MODE_REUSE)
+
+
+# mapping of mouse buttons and his masks
+const MAP_MOUSE_BUTTON_MASKS := {
+ MOUSE_BUTTON_LEFT : MOUSE_BUTTON_MASK_LEFT,
+ MOUSE_BUTTON_RIGHT : MOUSE_BUTTON_MASK_RIGHT,
+ MOUSE_BUTTON_MIDDLE : MOUSE_BUTTON_MASK_MIDDLE,
+ # https://github.com/godotengine/godot/issues/73632
+ MOUSE_BUTTON_WHEEL_UP : 1 << (MOUSE_BUTTON_WHEEL_UP - 1),
+ MOUSE_BUTTON_WHEEL_DOWN : 1 << (MOUSE_BUTTON_WHEEL_DOWN - 1),
+ MOUSE_BUTTON_XBUTTON1 : MOUSE_BUTTON_MASK_MB_XBUTTON1,
+ MOUSE_BUTTON_XBUTTON2 : MOUSE_BUTTON_MASK_MB_XBUTTON2,
+}
+
+var _is_disposed := false
+var _current_scene: Node = null
+var _awaiter: GdUnitAwaiter = GdUnitAwaiter.new()
+var _verbose: bool
+var _simulate_start_time: LocalTime
+var _last_input_event: InputEvent = null
+var _mouse_button_on_press := []
+var _key_on_press := []
+var _action_on_press := []
+var _curent_mouse_position: Vector2
+# holds the touch position for each touch index
+# { index: int = position: Vector2}
+var _current_touch_position: Dictionary = {}
+# holds the curretn touch drag position
+var _current_touch_drag_position: Vector2 = Vector2.ZERO
+
+# time factor settings
+var _time_factor := 1.0
+var _saved_iterations_per_second: float
+var _scene_auto_free := false
+
+
+func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> void:
+ _verbose = p_verbose
+ _saved_iterations_per_second = Engine.get_physics_ticks_per_second()
+ @warning_ignore("return_value_discarded")
+ set_time_factor(1)
+ # handle scene loading by resource path
+ if typeof(p_scene) == TYPE_STRING:
+ @warning_ignore("unsafe_cast")
+ if !ResourceLoader.exists(p_scene as String):
+ if not p_hide_push_errors:
+ push_error("GdUnitSceneRunner: Can't load scene by given resource path: '%s'. The resource does not exists." % p_scene)
+ return
+ if !str(p_scene).ends_with(".tscn") and !str(p_scene).ends_with(".scn") and !str(p_scene).begins_with("uid://"):
+ if not p_hide_push_errors:
+ push_error("GdUnitSceneRunner: The given resource: '%s'. is not a scene." % p_scene)
+ return
+ @warning_ignore("unsafe_cast")
+ _current_scene = (load(p_scene as String) as PackedScene).instantiate()
+ _scene_auto_free = true
+ else:
+ # verify we have a node instance
+ if not p_scene is Node:
+ if not p_hide_push_errors:
+ push_error("GdUnitSceneRunner: The given instance '%s' is not a Node." % p_scene)
+ return
+ _current_scene = p_scene
+ if _current_scene == null:
+ if not p_hide_push_errors:
+ push_error("GdUnitSceneRunner: Scene must be not null!")
+ return
+
+ _scene_tree().root.add_child(_current_scene)
+ # do finally reset all open input events when the scene is removed
+ @warning_ignore("return_value_discarded")
+ _scene_tree().root.child_exiting_tree.connect(func f(child :Node) -> void:
+ if child == _current_scene:
+ # we need to disable the processing to avoid input flush buffer errors
+ _current_scene.process_mode = Node.PROCESS_MODE_DISABLED
+ _reset_input_to_default()
+ )
+ _simulate_start_time = LocalTime.now()
+ # we need to set inital a valid window otherwise the warp_mouse() is not handled
+ move_window_to_foreground()
+
+ # set inital mouse pos to 0,0
+ var max_iteration_to_wait := 0
+ while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100:
+ Input.warp_mouse(Vector2.ZERO)
+ max_iteration_to_wait += 1
+
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_PREDELETE and is_instance_valid(self):
+ # reset time factor to normal
+ __deactivate_time_factor()
+ if is_instance_valid(_current_scene):
+ move_window_to_background()
+ _scene_tree().root.remove_child(_current_scene)
+ # do only free scenes instanciated by this runner
+ if _scene_auto_free:
+ _current_scene.free()
+ _is_disposed = true
+ _current_scene = null
+
+
+func _scene_tree() -> SceneTree:
+ return Engine.get_main_loop() as SceneTree
+
+
+func await_input_processed() -> void:
+ if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED:
+ Input.flush_buffered_events()
+ await (Engine.get_main_loop() as SceneTree).process_frame
+ await (Engine.get_main_loop() as SceneTree).physics_frame
+
+
+@warning_ignore("return_value_discarded")
+func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner:
+ simulate_action_press(action, event_index)
+ simulate_action_release(action, event_index)
+ return self
+
+
+func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner:
+ __print_current_focus()
+ var event := InputEventAction.new()
+ event.pressed = true
+ event.action = action
+ event.event_index = event_index
+ _action_on_press.append(action)
+ return _handle_input_event(event)
+
+
+func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner:
+ __print_current_focus()
+ var event := InputEventAction.new()
+ event.pressed = false
+ event.action = action
+ event.event_index = event_index
+ _action_on_press.erase(action)
+ return _handle_input_event(event)
+
+
+@warning_ignore("return_value_discarded")
+func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
+ simulate_key_press(key_code, shift_pressed, ctrl_pressed)
+ await _scene_tree().process_frame
+ simulate_key_release(key_code, shift_pressed, ctrl_pressed)
+ return self
+
+
+func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
+ __print_current_focus()
+ var event := InputEventKey.new()
+ event.pressed = true
+ event.keycode = key_code as Key
+ event.physical_keycode = key_code as Key
+ event.unicode = key_code
+ event.alt_pressed = key_code == KEY_ALT
+ event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
+ event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
+ _apply_input_modifiers(event)
+ _key_on_press.append(key_code)
+ return _handle_input_event(event)
+
+
+func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner:
+ __print_current_focus()
+ var event := InputEventKey.new()
+ event.pressed = false
+ event.keycode = key_code as Key
+ event.physical_keycode = key_code as Key
+ event.unicode = key_code
+ event.alt_pressed = key_code == KEY_ALT
+ event.shift_pressed = shift_pressed or key_code == KEY_SHIFT
+ event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL
+ _apply_input_modifiers(event)
+ _key_on_press.erase(key_code)
+ return _handle_input_event(event)
+
+
+func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner:
+ var event := InputEventMouseMotion.new()
+ event.position = pos
+ event.global_position = get_global_mouse_position()
+ _apply_input_modifiers(event)
+ return _handle_input_event(event)
+
+
+func get_mouse_position() -> Vector2:
+ if _last_input_event is InputEventMouse:
+ return (_last_input_event as InputEventMouse).position
+ var current_scene := scene()
+ if current_scene != null:
+ return current_scene.get_viewport().get_mouse_position()
+ return Vector2.ZERO
+
+
+func get_global_mouse_position() -> Vector2:
+ return (Engine.get_main_loop() as SceneTree).root.get_mouse_position()
+
+
+func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner:
+ var event := InputEventMouseMotion.new()
+ event.position = position
+ event.relative = position - get_mouse_position()
+ event.global_position = get_global_mouse_position()
+ _apply_input_mouse_mask(event)
+ _apply_input_modifiers(event)
+ return _handle_input_event(event)
+
+
+@warning_ignore("return_value_discarded")
+func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
+ var tween := _scene_tree().create_tween()
+ _curent_mouse_position = get_mouse_position()
+ var final_position := _curent_mouse_position + relative
+ tween.tween_property(self, "_curent_mouse_position", final_position, time).set_trans(trans_type)
+ tween.play()
+
+ while not get_mouse_position().is_equal_approx(final_position):
+ simulate_mouse_move(_curent_mouse_position)
+ await _scene_tree().process_frame
+ return self
+
+
+@warning_ignore("return_value_discarded")
+func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
+ var tween := _scene_tree().create_tween()
+ _curent_mouse_position = get_mouse_position()
+ tween.tween_property(self, "_curent_mouse_position", position, time).set_trans(trans_type)
+ tween.play()
+
+ while not get_mouse_position().is_equal_approx(position):
+ simulate_mouse_move(_curent_mouse_position)
+ await _scene_tree().process_frame
+ return self
+
+
+@warning_ignore("return_value_discarded")
+func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
+ simulate_mouse_button_press(button_index, double_click)
+ simulate_mouse_button_release(button_index)
+ return self
+
+
+func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner:
+ var event := InputEventMouseButton.new()
+ event.button_index = button_index
+ event.pressed = true
+ event.double_click = double_click
+ _apply_input_mouse_position(event)
+ _apply_input_mouse_mask(event)
+ _apply_input_modifiers(event)
+ _mouse_button_on_press.append(button_index)
+ return _handle_input_event(event)
+
+
+func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner:
+ var event := InputEventMouseButton.new()
+ event.button_index = button_index
+ event.pressed = false
+ _apply_input_mouse_position(event)
+ _apply_input_mouse_mask(event)
+ _apply_input_modifiers(event)
+ _mouse_button_on_press.erase(button_index)
+ return _handle_input_event(event)
+
+
+@warning_ignore("return_value_discarded")
+func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
+ simulate_screen_touch_press(index, position, double_tap)
+ simulate_screen_touch_release(index)
+ return self
+
+
+@warning_ignore("return_value_discarded")
+func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner:
+ if is_emulate_mouse_from_touch():
+ # we need to simulate in addition to the touch the mouse events
+ set_mouse_position(position)
+ simulate_mouse_button_press(MOUSE_BUTTON_LEFT)
+ # push touch press event at position
+ var event := InputEventScreenTouch.new()
+ event.window_id = scene().get_window().get_window_id()
+ event.index = index
+ event.position = position
+ event.double_tap = double_tap
+ event.pressed = true
+ _current_scene.get_viewport().push_input(event)
+ # save current drag position by index
+ _current_touch_position[index] = position
+ return self
+
+
+@warning_ignore("return_value_discarded")
+func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner:
+ if is_emulate_mouse_from_touch():
+ # we need to simulate in addition to the touch the mouse events
+ simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
+ # push touch release event at position
+ var event := InputEventScreenTouch.new()
+ event.window_id = scene().get_window().get_window_id()
+ event.index = index
+ event.position = get_screen_touch_drag_position(index)
+ event.pressed = false
+ event.double_tap = (_last_input_event as InputEventScreenTouch).double_tap if _last_input_event is InputEventScreenTouch else double_tap
+ _current_scene.get_viewport().push_input(event)
+ return self
+
+
+func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
+ var current_position: Vector2 = _current_touch_position[index]
+ return await _do_touch_drag_at(index, current_position + relative, time, trans_type)
+
+
+func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
+ return await _do_touch_drag_at(index, position, time, trans_type)
+
+
+@warning_ignore("return_value_discarded")
+func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner:
+ simulate_screen_touch_press(index, position)
+ return await _do_touch_drag_at(index, drop_position, time, trans_type)
+
+
+@warning_ignore("return_value_discarded")
+func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner:
+ if is_emulate_mouse_from_touch():
+ simulate_mouse_move(position)
+ var event := InputEventScreenDrag.new()
+ event.window_id = scene().get_window().get_window_id()
+ event.index = index
+ event.position = position
+ event.relative = _get_screen_touch_drag_position_or_default(index, position) - position
+ event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
+ event.pressure = 1.0
+ _current_touch_position[index] = position
+ _current_scene.get_viewport().push_input(event)
+ return self
+
+
+func get_screen_touch_drag_position(index: int) -> Vector2:
+ if _current_touch_position.has(index):
+ return _current_touch_position[index]
+ push_error("No touch drag position for index '%d' is set!" % index)
+ return Vector2.ZERO
+
+
+func is_emulate_mouse_from_touch() -> bool:
+ return ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch", true)
+
+
+func _get_screen_touch_drag_position_or_default(index: int, default_position: Vector2) -> Vector2:
+ if _current_touch_position.has(index):
+ return _current_touch_position[index]
+ return default_position
+
+
+@warning_ignore("return_value_discarded")
+func _do_touch_drag_at(index: int, drag_position: Vector2, time: float, trans_type: Tween.TransitionType) -> GdUnitSceneRunner:
+ # start draging
+ var event := InputEventScreenDrag.new()
+ event.window_id = scene().get_window().get_window_id()
+ event.index = index
+ event.position = get_screen_touch_drag_position(index)
+ event.pressure = 1.0
+ _current_touch_drag_position = event.position
+
+ var tween := _scene_tree().create_tween()
+ tween.tween_property(self, "_current_touch_drag_position", drag_position, time).set_trans(trans_type)
+ tween.play()
+
+ while not _current_touch_drag_position.is_equal_approx(drag_position):
+ if is_emulate_mouse_from_touch():
+ # we need to simulate in addition to the drag the mouse move events
+ simulate_mouse_move(event.position)
+ # send touche drag event to new position
+ event.relative = _current_touch_drag_position - event.position
+ event.velocity = event.relative / _scene_tree().root.get_process_delta_time()
+ event.position = _current_touch_drag_position
+ _current_scene.get_viewport().push_input(event)
+ await _scene_tree().process_frame
+
+ # finaly drop it
+ if is_emulate_mouse_from_touch():
+ simulate_mouse_move(drag_position)
+ simulate_mouse_button_release(MOUSE_BUTTON_LEFT)
+ var touch_drop_event := InputEventScreenTouch.new()
+ touch_drop_event.window_id = event.window_id
+ touch_drop_event.index = event.index
+ touch_drop_event.position = drag_position
+ touch_drop_event.pressed = false
+ _current_scene.get_viewport().push_input(touch_drop_event)
+ await _scene_tree().process_frame
+ return self
+
+
+func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner:
+ _time_factor = min(9.0, time_factor)
+ __activate_time_factor()
+ __print("set time factor: %f" % _time_factor)
+ __print("set physics physics_ticks_per_second: %d" % (_saved_iterations_per_second*_time_factor))
+ return self
+
+
+func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner:
+ var time_shift_frames :int = max(1, frames / _time_factor)
+ for frame in time_shift_frames:
+ if delta_milli == -1:
+ await _scene_tree().process_frame
+ else:
+ await _scene_tree().create_timer(delta_milli * 0.001).timeout
+ return self
+
+
+func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner:
+ await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000)
+ return self
+
+
+func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner:
+ await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000)
+ return self
+
+
+func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert:
+ return GdUnitFuncAssertImpl.new(scene(), func_name, args)
+
+
+func await_func_on(instance: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert:
+ return GdUnitFuncAssertImpl.new(instance, func_name, args)
+
+
+func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void:
+ await _awaiter.await_signal_on(scene(), signal_name, args, timeout)
+
+
+func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void:
+ await _awaiter.await_signal_on(source, signal_name, args, timeout)
+
+
+func move_window_to_foreground() -> GdUnitSceneRunner:
+ if not Engine.is_embedded_in_editor():
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
+ DisplayServer.window_move_to_foreground()
+ return self
+
+
+func move_window_to_background() -> GdUnitSceneRunner:
+ if not Engine.is_embedded_in_editor():
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
+ DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
+ return self
+
+
+func _property_exists(name: String) -> bool:
+ return scene().get_property_list().any(func(properties :Dictionary) -> bool: return properties["name"] == name)
+
+
+func get_property(name: String) -> Variant:
+ if not _property_exists(name):
+ return "The property '%s' not exist checked loaded scene." % name
+ return scene().get(name)
+
+
+func set_property(name: String, value: Variant) -> bool:
+ if not _property_exists(name):
+ push_error("The property named '%s' cannot be set, it does not exist!" % name)
+ return false;
+ scene().set(name, value)
+ return true
+
+
+func invoke(name: String, ...args: Array) -> Variant:
+ if scene().has_method(name):
+ return await scene().callv(name, args)
+ return "The method '%s' not exist checked loaded scene." % name
+
+
+func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node:
+ return scene().find_child(name, recursive, owned)
+
+
+func _scene_name() -> String:
+ var scene_script :GDScript = scene().get_script()
+ var scene_name :String = scene().get_name()
+ if not scene_script:
+ return scene_name
+ if not scene_name.begins_with("@"):
+ return scene_name
+ return scene_script.resource_name.get_basename()
+
+
+func __activate_time_factor() -> void:
+ Engine.set_time_scale(_time_factor)
+ Engine.set_physics_ticks_per_second((_saved_iterations_per_second * _time_factor) as int)
+
+
+func __deactivate_time_factor() -> void:
+ Engine.set_time_scale(1)
+ Engine.set_physics_ticks_per_second(_saved_iterations_per_second as int)
+
+
+# copy over current active modifiers
+func _apply_input_modifiers(event: InputEvent) -> void:
+ if _last_input_event is InputEventWithModifiers and event is InputEventWithModifiers:
+ var last_input_event := _last_input_event as InputEventWithModifiers
+ var _event := event as InputEventWithModifiers
+ _event.meta_pressed = _event.meta_pressed or last_input_event.meta_pressed
+ _event.alt_pressed = _event.alt_pressed or last_input_event.alt_pressed
+ _event.shift_pressed = _event.shift_pressed or last_input_event.shift_pressed
+ _event.ctrl_pressed = _event.ctrl_pressed or last_input_event.ctrl_pressed
+ # this line results into reset the control_pressed state!!!
+ #event.command_or_control_autoremap = event.command_or_control_autoremap or _last_input_event.command_or_control_autoremap
+
+
+# copy over current active mouse mask and combine with curren mask
+func _apply_input_mouse_mask(event: InputEvent) -> void:
+ # first apply last mask
+ if _last_input_event is InputEventMouse and event is InputEventMouse:
+ (event as InputEventMouse).button_mask |= (_last_input_event as InputEventMouse).button_mask
+ if event is InputEventMouseButton:
+ var _event := event as InputEventMouseButton
+ var button_mask :int = MAP_MOUSE_BUTTON_MASKS.get(_event.get_button_index(), 0)
+ if _event.is_pressed():
+ _event.button_mask |= button_mask
+ else:
+ _event.button_mask ^= button_mask
+
+
+# copy over last mouse position if need
+func _apply_input_mouse_position(event: InputEvent) -> void:
+ if _last_input_event is InputEventMouse and event is InputEventMouseButton:
+ (event as InputEventMouseButton).position = (_last_input_event as InputEventMouse).position
+
+
+## handle input action via Input modifieres
+func _handle_actions(event: InputEventAction) -> bool:
+ if not InputMap.event_is_action(event, event.action, true):
+ return false
+ __print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()])
+ if event.is_pressed():
+ Input.action_press(event.action, event.get_strength())
+ else:
+ Input.action_release(event.action)
+ return true
+
+
+# for handling read https://docs.godotengine.org/en/stable/tutorials/inputs/inputevent.html?highlight=inputevent#how-does-it-work
+@warning_ignore("return_value_discarded")
+func _handle_input_event(event: InputEvent) -> GdUnitSceneRunner:
+ if event is InputEventMouse:
+ Input.warp_mouse((event as InputEventMouse).position as Vector2)
+ Input.parse_input_event(event)
+
+ if event is InputEventAction:
+ _handle_actions(event as InputEventAction)
+
+ var current_scene := scene()
+ if is_instance_valid(current_scene):
+ # do not flush events if node processing disabled otherwise we run into errors at tree removed
+ if _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
+ Input.flush_buffered_events()
+ __print(" process event %s (%s) <- %s" % [current_scene, _scene_name(), event.as_text()])
+ if(current_scene.has_method("_gui_input")):
+ (current_scene as Control)._gui_input(event)
+ if(current_scene.has_method("_unhandled_input")):
+ current_scene._unhandled_input(event)
+ current_scene.get_viewport().set_input_as_handled()
+
+ # save last input event needs to be merged with next InputEventMouseButton
+ _last_input_event = event
+ return self
+
+
+@warning_ignore("return_value_discarded")
+func _reset_input_to_default() -> void:
+ # reset all mouse button to inital state if need
+ for m_button :int in _mouse_button_on_press.duplicate():
+ if Input.is_mouse_button_pressed(m_button):
+ simulate_mouse_button_release(m_button)
+ _mouse_button_on_press.clear()
+
+ for key_scancode :int in _key_on_press.duplicate():
+ if Input.is_key_pressed(key_scancode):
+ simulate_key_release(key_scancode)
+ _key_on_press.clear()
+
+ for action :String in _action_on_press.duplicate():
+ if Input.is_action_pressed(action):
+ simulate_action_release(action)
+ _action_on_press.clear()
+
+ if is_instance_valid(_current_scene) and _current_scene.process_mode != Node.PROCESS_MODE_DISABLED:
+ Input.flush_buffered_events()
+ _last_input_event = null
+
+
+func __print(message: String) -> void:
+ if _verbose:
+ prints(message)
+
+
+func __print_current_focus() -> void:
+ if not _verbose:
+ return
+ var focused_node := scene().get_viewport().gui_get_focus_owner()
+ if focused_node:
+ prints(" focus checked %s" % focused_node)
+ else:
+ prints(" no focus set")
+
+
+func scene() -> Node:
+ if is_instance_valid(_current_scene):
+ return _current_scene
+ if not _is_disposed:
+ push_error("The current scene instance is not valid anymore! check your test is valid. e.g. check for missing awaits.")
+ return null
diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid
new file mode 100644
index 00000000..152eeef5
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid
@@ -0,0 +1 @@
+uid://7a566a4kfreu
diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd
new file mode 100644
index 00000000..f6bff127
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSettings.gd
@@ -0,0 +1,435 @@
+@tool
+class_name GdUnitSettings
+extends RefCounted
+
+
+const MAIN_CATEGORY = "gdunit4"
+# Common Settings
+const COMMON_SETTINGS = MAIN_CATEGORY + "/settings"
+
+const GROUP_COMMON = COMMON_SETTINGS + "/common"
+const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled"
+const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes"
+
+const GROUP_HOOKS = MAIN_CATEGORY + "/hooks"
+const SESSION_HOOKS = GROUP_HOOKS + "/session_hooks"
+
+const GROUP_TEST = COMMON_SETTINGS + "/test"
+const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds"
+const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder"
+const TEST_SUITE_NAMING_CONVENTION = GROUP_TEST + "/test_suite_naming_convention"
+const TEST_DISCOVER_ENABLED = GROUP_TEST + "/test_discovery"
+const TEST_FLAKY_CHECK = GROUP_TEST + "/flaky_check_enable"
+const TEST_FLAKY_MAX_RETRIES = GROUP_TEST + "/flaky_max_retries"
+
+
+# Report Setiings
+const REPORT_SETTINGS = MAIN_CATEGORY + "/report"
+const GROUP_GODOT = REPORT_SETTINGS + "/godot"
+const REPORT_PUSH_ERRORS = GROUP_GODOT + "/push_error"
+const REPORT_SCRIPT_ERRORS = GROUP_GODOT + "/script_error"
+const REPORT_ORPHANS = REPORT_SETTINGS + "/verbose_orphans"
+const GROUP_ASSERT = REPORT_SETTINGS + "/assert"
+const REPORT_ASSERT_WARNINGS = GROUP_ASSERT + "/verbose_warnings"
+const REPORT_ASSERT_ERRORS = GROUP_ASSERT + "/verbose_errors"
+const REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE = GROUP_ASSERT + "/strict_number_type_compare"
+
+# Godot debug stdout/logging settings
+const CATEGORY_LOGGING := "debug/file_logging/"
+const STDOUT_ENABLE_TO_FILE = CATEGORY_LOGGING + "enable_file_logging"
+const STDOUT_WITE_TO_FILE = CATEGORY_LOGGING + "log_path"
+
+
+# GdUnit Templates
+const TEMPLATES = MAIN_CATEGORY + "/templates"
+const TEMPLATES_TS = TEMPLATES + "/testsuite"
+const TEMPLATE_TS_GD = TEMPLATES_TS + "/GDScript"
+const TEMPLATE_TS_CS = TEMPLATES_TS + "/CSharpScript"
+
+
+# UI Setiings
+const UI_SETTINGS = MAIN_CATEGORY + "/ui"
+const GROUP_UI_INSPECTOR = UI_SETTINGS + "/inspector"
+const INSPECTOR_NODE_COLLAPSE = GROUP_UI_INSPECTOR + "/node_collapse"
+const INSPECTOR_TREE_VIEW_MODE = GROUP_UI_INSPECTOR + "/tree_view_mode"
+const INSPECTOR_TREE_SORT_MODE = GROUP_UI_INSPECTOR + "/tree_sort_mode"
+
+
+# Shortcut Setiings
+const SHORTCUT_SETTINGS = MAIN_CATEGORY + "/Shortcuts"
+const GROUP_SHORTCUT_INSPECTOR = SHORTCUT_SETTINGS + "/inspector"
+const SHORTCUT_INSPECTOR_RERUN_TEST = GROUP_SHORTCUT_INSPECTOR + "/rerun_test"
+const SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG = GROUP_SHORTCUT_INSPECTOR + "/rerun_test_debug"
+const SHORTCUT_INSPECTOR_RUN_TEST_OVERALL = GROUP_SHORTCUT_INSPECTOR + "/run_test_overall"
+const SHORTCUT_INSPECTOR_RUN_TEST_STOP = GROUP_SHORTCUT_INSPECTOR + "/run_test_stop"
+
+const GROUP_SHORTCUT_EDITOR = SHORTCUT_SETTINGS + "/editor"
+const SHORTCUT_EDITOR_RUN_TEST = GROUP_SHORTCUT_EDITOR + "/run_test"
+const SHORTCUT_EDITOR_RUN_TEST_DEBUG = GROUP_SHORTCUT_EDITOR + "/run_test_debug"
+const SHORTCUT_EDITOR_CREATE_TEST = GROUP_SHORTCUT_EDITOR + "/create_test"
+
+const GROUP_SHORTCUT_FILESYSTEM = SHORTCUT_SETTINGS + "/filesystem"
+const SHORTCUT_FILESYSTEM_RUN_TEST = GROUP_SHORTCUT_FILESYSTEM + "/run_test"
+const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_test_debug"
+
+
+# Toolbar Setiings
+const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar"
+const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall"
+
+# Feature flags
+const GROUP_FEATURE = MAIN_CATEGORY + "/feature"
+
+
+# defaults
+# server connection timeout in minutes
+const DEFAULT_SERVER_TIMEOUT :int = 30
+# test case runtime timeout in seconds
+const DEFAULT_TEST_TIMEOUT :int = 60*5
+# the folder to create new test-suites
+const DEFAULT_TEST_LOOKUP_FOLDER := "test"
+
+# help texts
+const HELP_TEST_LOOKUP_FOLDER := "Subfolder where test suites are located (or empty to use source folder directly)"
+
+enum NAMING_CONVENTIONS {
+ AUTO_DETECT,
+ SNAKE_CASE,
+ PASCAL_CASE,
+}
+
+
+const _VALUE_SET_SEPARATOR = "\f" # ASCII Form-feed character (AKA page break)
+
+
+static func setup() -> void:
+ create_property_if_need(UPDATE_NOTIFICATION_ENABLED, true, "Show notification if new gdUnit4 version is found")
+ # test settings
+ create_property_if_need(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT, "Server connection timeout in minutes")
+ create_property_if_need(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT, "Test case runtime timeout in seconds")
+ create_property_if_need(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER, HELP_TEST_LOOKUP_FOLDER)
+ create_property_if_need(TEST_SUITE_NAMING_CONVENTION, NAMING_CONVENTIONS.AUTO_DETECT, "Naming convention to use when generating testsuites", NAMING_CONVENTIONS.keys())
+ create_property_if_need(TEST_DISCOVER_ENABLED, false, "Automatically detect new tests in test lookup folders at runtime")
+ create_property_if_need(TEST_FLAKY_CHECK, false, "Rerun tests on failure and mark them as FLAKY")
+ create_property_if_need(TEST_FLAKY_MAX_RETRIES, 3, "Sets the number of retries for rerunning a flaky test")
+ # report settings
+ create_property_if_need(REPORT_PUSH_ERRORS, false, "Report push_error() as failure")
+ create_property_if_need(REPORT_SCRIPT_ERRORS, true, "Report script errors as failure")
+ create_property_if_need(REPORT_ORPHANS, true, "Report orphaned nodes after tests finish")
+ create_property_if_need(REPORT_ASSERT_ERRORS, true, "Report assertion failures as errors")
+ create_property_if_need(REPORT_ASSERT_WARNINGS, true, "Report assertion failures as warnings")
+ create_property_if_need(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true, "Compare number values strictly by type (real vs int)")
+ # inspector
+ create_property_if_need(INSPECTOR_NODE_COLLAPSE, true,
+ "Close testsuite node after a successful test run.")
+ create_property_if_need(INSPECTOR_TREE_VIEW_MODE, GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE,
+ "Inspector panel presentation mode", GdUnitInspectorTreeConstants.TREE_VIEW_MODE.keys())
+ create_property_if_need(INSPECTOR_TREE_SORT_MODE, GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED,
+ "Inspector panel sorting mode", GdUnitInspectorTreeConstants.SORT_MODE.keys())
+ create_property_if_need(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, false,
+ "Show 'Run overall Tests' button in the inspector toolbar")
+ create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use")
+ create_shortcut_properties_if_need()
+ create_property_if_need(SESSION_HOOKS, {} as Dictionary[String,bool])
+ migrate_properties()
+
+
+static func migrate_properties() -> void:
+ var TEST_ROOT_FOLDER := "gdunit4/settings/test/test_root_folder"
+ if get_property(TEST_ROOT_FOLDER) != null:
+ migrate_property(TEST_ROOT_FOLDER,\
+ TEST_LOOKUP_FOLDER,\
+ DEFAULT_TEST_LOOKUP_FOLDER,\
+ HELP_TEST_LOOKUP_FOLDER,\
+ func(value :Variant) -> String: return DEFAULT_TEST_LOOKUP_FOLDER if value == null else value)
+
+
+static func create_shortcut_properties_if_need() -> void:
+ # inspector
+ create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS), "Rerun the most recently executed tests")
+ create_property_if_need(SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG), "Rerun the most recently executed tests (Debug mode)")
+ create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_OVERALL, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL), "Runs all tests (Debug mode)")
+ create_property_if_need(SHORTCUT_INSPECTOR_RUN_TEST_STOP, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.STOP_TEST_RUN), "Stop the current test execution")
+ # script editor
+ create_property_if_need(SHORTCUT_EDITOR_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE), "Run the currently selected test")
+ create_property_if_need(SHORTCUT_EDITOR_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG), "Run the currently selected test (Debug mode).")
+ create_property_if_need(SHORTCUT_EDITOR_CREATE_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.CREATE_TEST), "Create a new test case for the currently selected function")
+ # filesystem
+ create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file")
+ create_property_if_need(SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG, GdUnitShortcut.default_keys(GdUnitShortcut.ShortCut.NONE), "Run all test suites in the selected folder or file (Debug)")
+
+
+static func create_property_if_need(name :String, default :Variant, help :="", value_set := PackedStringArray()) -> void:
+ if not ProjectSettings.has_setting(name):
+ #prints("GdUnit4: Set inital settings '%s' to '%s'." % [name, str(default)])
+ ProjectSettings.set_setting(name, default)
+
+ ProjectSettings.set_initial_value(name, default)
+ help = help if value_set.is_empty() else "%s%s%s" % [help, _VALUE_SET_SEPARATOR, value_set]
+ set_help(name, default, help)
+
+
+static func set_help(property_name :String, value :Variant, help :String) -> void:
+ ProjectSettings.add_property_info({
+ "name": property_name,
+ "type": typeof(value),
+ "hint": PROPERTY_HINT_TYPE_STRING,
+ "hint_string": help
+ })
+
+
+static func get_setting(name :String, default :Variant) -> Variant:
+ if ProjectSettings.has_setting(name):
+ return ProjectSettings.get_setting(name)
+ return default
+
+
+static func is_update_notification_enabled() -> bool:
+ if ProjectSettings.has_setting(UPDATE_NOTIFICATION_ENABLED):
+ return ProjectSettings.get_setting(UPDATE_NOTIFICATION_ENABLED)
+ return false
+
+
+static func set_update_notification(enable :bool) -> void:
+ ProjectSettings.set_setting(UPDATE_NOTIFICATION_ENABLED, enable)
+ @warning_ignore("return_value_discarded")
+ ProjectSettings.save()
+
+
+static func get_log_path() -> String:
+ return ProjectSettings.get_setting(STDOUT_WITE_TO_FILE)
+
+
+static func set_log_path(path :String) -> void:
+ ProjectSettings.set_setting(STDOUT_ENABLE_TO_FILE, true)
+ ProjectSettings.set_setting(STDOUT_WITE_TO_FILE, path)
+ @warning_ignore("return_value_discarded")
+ ProjectSettings.save()
+
+
+static func get_session_hooks() -> Dictionary[String, bool]:
+ var property := get_property(SESSION_HOOKS)
+ if property == null:
+ return {}
+ var hooks: Dictionary[String, bool] = property.value()
+ return hooks
+
+
+static func set_session_hooks(hooks: Dictionary[String, bool]) -> void:
+ var property := get_property(SESSION_HOOKS)
+ property.set_value(hooks)
+ update_property(property)
+
+
+static func set_inspector_tree_sort_mode(sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void:
+ var property := get_property(INSPECTOR_TREE_SORT_MODE)
+ property.set_value(sort_mode)
+ update_property(property)
+
+
+static func get_inspector_tree_sort_mode() -> GdUnitInspectorTreeConstants.SORT_MODE:
+ var property := get_property(INSPECTOR_TREE_SORT_MODE)
+ return property.value() if property != null else GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED
+
+
+static func set_inspector_tree_view_mode(tree_view_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void:
+ var property := get_property(INSPECTOR_TREE_VIEW_MODE)
+ property.set_value(tree_view_mode)
+ update_property(property)
+
+
+static func get_inspector_tree_view_mode() -> GdUnitInspectorTreeConstants.TREE_VIEW_MODE:
+ var property := get_property(INSPECTOR_TREE_VIEW_MODE)
+ return property.value() if property != null else GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE
+
+
+# the configured server connection timeout in ms
+static func server_timeout() -> int:
+ return get_setting(SERVER_TIMEOUT, DEFAULT_SERVER_TIMEOUT) * 60 * 1000
+
+
+# the configured test case timeout in ms
+static func test_timeout() -> int:
+ return get_setting(TEST_TIMEOUT, DEFAULT_TEST_TIMEOUT) * 1000
+
+
+# the root folder to store/generate test-suites
+static func test_root_folder() -> String:
+ return get_setting(TEST_LOOKUP_FOLDER, DEFAULT_TEST_LOOKUP_FOLDER)
+
+
+static func is_verbose_assert_warnings() -> bool:
+ return get_setting(REPORT_ASSERT_WARNINGS, true)
+
+
+static func is_verbose_assert_errors() -> bool:
+ return get_setting(REPORT_ASSERT_ERRORS, true)
+
+
+static func is_verbose_orphans() -> bool:
+ return get_setting(REPORT_ORPHANS, true)
+
+
+static func is_strict_number_type_compare() -> bool:
+ return get_setting(REPORT_ASSERT_STRICT_NUMBER_TYPE_COMPARE, true)
+
+
+static func is_report_push_errors() -> bool:
+ return get_setting(REPORT_PUSH_ERRORS, false)
+
+
+static func is_report_script_errors() -> bool:
+ return get_setting(REPORT_SCRIPT_ERRORS, true)
+
+
+static func is_inspector_node_collapse() -> bool:
+ return get_setting(INSPECTOR_NODE_COLLAPSE, true)
+
+
+static func is_inspector_toolbar_button_show() -> bool:
+ return get_setting(INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL, true)
+
+
+static func is_test_discover_enabled() -> bool:
+ return get_setting(TEST_DISCOVER_ENABLED, false)
+
+
+static func is_test_flaky_check_enabled() -> bool:
+ return get_setting(TEST_FLAKY_CHECK, false)
+
+
+static func is_feature_enabled(feature: String) -> bool:
+ return get_setting(feature, false)
+
+
+static func get_flaky_max_retries() -> int:
+ return get_setting(TEST_FLAKY_MAX_RETRIES, 3)
+
+
+static func set_test_discover_enabled(enable :bool) -> void:
+ var property := get_property(TEST_DISCOVER_ENABLED)
+ property.set_value(enable)
+ update_property(property)
+
+
+static func is_log_enabled() -> bool:
+ return ProjectSettings.get_setting(STDOUT_ENABLE_TO_FILE)
+
+
+static func list_settings(category: String) -> Array[GdUnitProperty]:
+ var settings: Array[GdUnitProperty] = []
+ for property in ProjectSettings.get_property_list():
+ var property_name :String = property["name"]
+ if property_name.begins_with(category):
+ settings.append(build_property(property_name, property))
+ return settings
+
+
+static func extract_value_set_from_help(value :String) -> PackedStringArray:
+ var split_value := value.split(_VALUE_SET_SEPARATOR)
+ if not split_value.size() > 1:
+ return PackedStringArray()
+
+ var regex := RegEx.new()
+ @warning_ignore("return_value_discarded")
+ regex.compile("\\[(.+)\\]")
+ var matches := regex.search_all(split_value[1])
+ if matches.is_empty():
+ return PackedStringArray()
+ var values: String = matches[0].get_string(1)
+ return values.replacen(" ", "").replacen("\"", "").split(",", false)
+
+
+static func extract_help_text(value :String) -> String:
+ return value.split(_VALUE_SET_SEPARATOR)[0]
+
+
+static func update_property(property :GdUnitProperty) -> Variant:
+ var current_value :Variant = ProjectSettings.get_setting(property.name())
+ if current_value != property.value():
+ var error :Variant = validate_property_value(property)
+ if error != null:
+ return error
+ ProjectSettings.set_setting(property.name(), property.value())
+ GdUnitSignals.instance().gdunit_settings_changed.emit(property)
+ _save_settings()
+ return null
+
+
+static func reset_property(property :GdUnitProperty) -> void:
+ ProjectSettings.set_setting(property.name(), property.default())
+ GdUnitSignals.instance().gdunit_settings_changed.emit(property)
+ _save_settings()
+
+
+static func validate_property_value(property :GdUnitProperty) -> Variant:
+ match property.name():
+ TEST_LOOKUP_FOLDER:
+ return validate_lookup_folder(property.value_as_string())
+ _: return null
+
+
+static func validate_lookup_folder(value :String) -> Variant:
+ if value.is_empty() or value == "/":
+ return null
+ if value.contains("res:"):
+ return "Test Lookup Folder: do not allowed to contains 'res://'"
+ if not value.is_valid_filename():
+ return "Test Lookup Folder: contains invalid characters! e.g (: / \\ ? * \" | % < >)"
+ return null
+
+
+static func save_property(name :String, value :Variant) -> void:
+ ProjectSettings.set_setting(name, value)
+ _save_settings()
+
+
+static func _save_settings() -> void:
+ var err := ProjectSettings.save()
+ if err != OK:
+ push_error("Save GdUnit4 settings failed : %s" % error_string(err))
+ return
+
+
+static func has_property(name :String) -> bool:
+ return ProjectSettings.get_property_list().any(func(property :Dictionary) -> bool: return property["name"] == name)
+
+
+static func get_property(name :String) -> GdUnitProperty:
+ for property in ProjectSettings.get_property_list():
+ var property_name :String = property["name"]
+ if property_name == name:
+ return build_property(name, property)
+ return null
+
+
+static func build_property(property_name: String, property: Dictionary) -> GdUnitProperty:
+ var value: Variant = ProjectSettings.get_setting(property_name)
+ var value_type: int = property["type"]
+ var default: Variant = ProjectSettings.property_get_revert(property_name)
+ var help: String = property["hint_string"]
+ var value_set := extract_value_set_from_help(help)
+ return GdUnitProperty.new(property_name, value_type, value, default, extract_help_text(help), value_set)
+
+
+static func migrate_property(old_property :String, new_property :String, default_value :Variant, help :String, converter := Callable()) -> void:
+ var property := get_property(old_property)
+ if property == null:
+ prints("Migration not possible, property '%s' not found" % old_property)
+ return
+ var value :Variant = converter.call(property.value()) if converter.is_valid() else property.value()
+ ProjectSettings.set_setting(new_property, value)
+ ProjectSettings.set_initial_value(new_property, default_value)
+ set_help(new_property, value, help)
+ ProjectSettings.clear(old_property)
+ prints("Successfully migrated property '%s' -> '%s' value: %s" % [old_property, new_property, value])
+
+
+static func dump_to_tmp() -> void:
+ @warning_ignore("return_value_discarded")
+ ProjectSettings.save_custom("user://project_settings.godot")
+
+
+static func restore_dump_from_tmp() -> void:
+ @warning_ignore("return_value_discarded")
+ DirAccess.copy_absolute("user://project_settings.godot", "res://project.godot")
diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid
new file mode 100644
index 00000000..cb7e30e1
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid
@@ -0,0 +1 @@
+uid://coby4unvmd3eh
diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd
new file mode 100644
index 00000000..528e133f
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd
@@ -0,0 +1,81 @@
+class_name GdUnitSignalAwaiter
+extends RefCounted
+
+signal signal_emitted(action :Variant)
+
+const NO_ARG :Variant = GdUnitConstants.NO_ARG
+
+var _wait_on_idle_frame := false
+var _interrupted := false
+var _time_left :float = 0
+var _timeout_millis :int
+
+
+func _init(timeout_millis :int, wait_on_idle_frame := false) -> void:
+ _timeout_millis = timeout_millis
+ _wait_on_idle_frame = wait_on_idle_frame
+
+
+func _on_signal_emmited(
+ arg0 :Variant = NO_ARG,
+ arg1 :Variant = NO_ARG,
+ arg2 :Variant = NO_ARG,
+ arg3 :Variant = NO_ARG,
+ arg4 :Variant = NO_ARG,
+ arg5 :Variant = NO_ARG,
+ arg6 :Variant = NO_ARG,
+ arg7 :Variant = NO_ARG,
+ arg8 :Variant = NO_ARG,
+ arg9 :Variant = NO_ARG) -> void:
+ var signal_args :Variant = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG)
+ signal_emitted.emit(signal_args)
+
+
+func is_interrupted() -> bool:
+ return _interrupted
+
+
+func elapsed_time() -> float:
+ return _time_left
+
+
+func on_signal(source :Object, signal_name :String, expected_signal_args :Array) -> Variant:
+ # register checked signal to wait for
+ @warning_ignore("return_value_discarded")
+ source.connect(signal_name, _on_signal_emmited)
+ # install timeout timer
+ var scene_tree := Engine.get_main_loop() as SceneTree
+ var timer := Timer.new()
+ scene_tree.root.add_child(timer)
+ timer.add_to_group("GdUnitTimers")
+ timer.set_one_shot(true)
+ @warning_ignore("return_value_discarded")
+ timer.timeout.connect(_do_interrupt, CONNECT_DEFERRED)
+ timer.start(_timeout_millis * 0.001 * Engine.get_time_scale())
+
+ # holds the emited value
+ var value :Variant
+ # wait for signal is emitted or a timeout is happen
+ while true:
+ value = await signal_emitted
+ if _interrupted:
+ break
+ if not (value is Array):
+ value = [value]
+ if expected_signal_args.size() == 0 or GdObjects.equals(value, expected_signal_args):
+ break
+ await scene_tree.process_frame
+
+ source.disconnect(signal_name, _on_signal_emmited)
+ _time_left = timer.time_left
+ timer.queue_free()
+ await scene_tree.process_frame
+ @warning_ignore("unsafe_cast")
+ if value is Array and (value as Array).size() == 1:
+ return value[0]
+ return value
+
+
+func _do_interrupt() -> void:
+ _interrupted = true
+ signal_emitted.emit(null)
diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid
new file mode 100644
index 00000000..8eaf88b7
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid
@@ -0,0 +1 @@
+uid://ckx5jnr3ip6vp
diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd
new file mode 100644
index 00000000..d15d3843
--- /dev/null
+++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd
@@ -0,0 +1,129 @@
+# 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