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" + 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 0000000..c08b3dd --- /dev/null +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid @@ -0,0 +1 @@ +uid://cca26e4thsgr2 diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg new file mode 100644 index 0000000..6f73418 --- /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.0" +script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd new file mode 100644 index 0000000..822c4d5 --- /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 0000000..1d7d106 --- /dev/null +++ b/addons/gdUnit4/plugin.gd.uid @@ -0,0 +1 @@ +uid://ddael08u8cd37 diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd new file mode 100644 index 0000000..ad5da4d --- /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 0000000..f0269ef --- /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 0000000..096088a --- /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 0000000..de2e00c --- /dev/null +++ b/addons/gdUnit4/src/Comparator.gd.uid @@ -0,0 +1 @@ +uid://diowb66hireor diff --git a/addons/gdUnit4/src/Fuzzers.gd b/addons/gdUnit4/src/Fuzzers.gd new file mode 100644 index 0000000..f61ba6e --- /dev/null +++ b/addons/gdUnit4/src/Fuzzers.gd @@ -0,0 +1,34 @@ +## A fuzzer implementation to provide default implementation +class_name Fuzzers +extends Resource + + +## Generates an random string with min/max length and given charset +static func rand_str(min_length: int, max_length :int, charset := StringFuzzer.DEFAULT_CHARSET) -> Fuzzer: + return StringFuzzer.new(min_length, max_length, charset) + + +## Generates an random integer in a range form to +static func rangei(from: int, to: int) -> Fuzzer: + return IntFuzzer.new(from, to) + +## Generates a randon float within in a given range +static func rangef(from: float, to: float) -> Fuzzer: + return FloatFuzzer.new(from, to) + +## Generates an random Vector2 in a range form to +static func rangev2(from: Vector2, to: Vector2) -> Fuzzer: + return Vector2Fuzzer.new(from, to) + + +## Generates an random Vector3 in a range form to +static func rangev3(from: Vector3, to: Vector3) -> Fuzzer: + return Vector3Fuzzer.new(from, to) + +## Generates an integer in a range form to that can be divided exactly by 2 +static func eveni(from: int, to: int) -> Fuzzer: + return IntFuzzer.new(from, to, IntFuzzer.EVEN) + +## Generates an integer in a range form to that cannot be divided exactly by 2 +static func oddi(from: int, to: int) -> Fuzzer: + 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 0000000..b11ddee --- /dev/null +++ b/addons/gdUnit4/src/Fuzzers.gd.uid @@ -0,0 +1 @@ +uid://b5k74b3q0djbr diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd b/addons/gdUnit4/src/GdUnitArrayAssert.gd new file mode 100644 index 0000000..eeb7ca6 --- /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 0000000..c523c27 --- /dev/null +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid @@ -0,0 +1 @@ +uid://b7jtmrldpoyys diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd new file mode 100644 index 0000000..41382d9 --- /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 0000000..705ed5f --- /dev/null +++ b/addons/gdUnit4/src/GdUnitAssert.gd.uid @@ -0,0 +1 @@ +uid://b8ypyyuevakpd diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd b/addons/gdUnit4/src/GdUnitAwaiter.gd new file mode 100644 index 0000000..51385e8 --- /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